Nuxt 4 ile Sıfırdan Blog Kurulumu

Nuxt Content modülü kullanarak dosya tabanlı bir blog sistemi nasıl kurulur? Adım adım detaylı rehber.

Nuxt 4 ile Sıfırdan Blog Kurulumu

Merhaba! Bu yazıda, hiç Nuxt bilmeyen birine bile anlaşılır şekilde, sıfırdan bir blog projesi nasıl kurulur öğreneceğiz. Nuxt 4 ve Content modülü kullanarak modern, hızlı ve SEO dostu bir blog sistemi oluşturacağız.

📦 1. Proje Kurulumu

İlk olarak terminalinizi açın ve yeni bir Nuxt 4 projesi oluşturalım:

# Nuxt 4 projesi oluştur
npx nuxi@latest init my-blog

# Proje klasörüne gir
cd my-blog

# Bağımlılıkları yükle
npm install

Projeniz oluşturulduktan sonra, ihtiyacımız olan modülleri yükleyelim:

# Nuxt Content modülü (Markdown dosyalarını okumak için)
npm install @nuxt/content

# Tailwind CSS (Stil için)
npm install -D @nuxtjs/tailwindcss

# Icon desteği (İsteğe bağlı)
npm install @nuxt/icon

Nuxt.config.ts Yapılandırması

nuxt.config.ts dosyanızı açın ve modülleri ekleyin:

export default defineNuxtConfig({
  modules: [
    '@nuxt/content',
    '@nuxtjs/tailwindcss',
    '@nuxt/icon'
  ],
  
  content: {
    highlight: {
      theme: 'github-dark',
      preload: ['javascript', 'typescript', 'bash']
    }
  },
  
  compatibilityDate: '2024-11-01',
  devtools: { enabled: true }
})

📁 2. App Klasörü Mantığı

Nuxt 4'te app klasörü, projenizin temel yapı taşıdır. Bu klasör içindeki dosyalar otomatik olarak tanınır ve route'lara dönüştürülür.

Klasör Yapısı

my-blog/
├── app/
│   ├── pages/           # Sayfalar (otomatik routing)
│   │   ├── index.vue    # Ana sayfa (/)
│   │   └── blog/
│   │       ├── index.vue      # Blog listesi (/blog)
│   │       └── [slug].vue     # Blog detay (/blog/post-title)
│   ├── components/      # Vue bileşenleri
│   │   └── BlogCard.vue
│   ├── layouts/         # Sayfa şablonları
│   │   └── default.vue
│   ├── assets/          # CSS, resimler vs.
│   └── app.vue          # Ana uygulama dosyası
├── content/             # Markdown dosyaları
│   └── blog/
│       ├── first-post.md
│       └── second-post.md
├── public/              # Statik dosyalar
└── nuxt.config.ts       # Yapılandırma

App Klasörünün Önemi

  • pages/: Bu klasördeki her .vue dosyası otomatik olarak bir route'a dönüşür
  • components/: Yeniden kullanılabilir bileşenler (otomatik import edilir)
  • layouts/: Sayfa şablonları (header, footer gibi ortak alanlar)
  • app.vue: Tüm sayfaları saran ana dosya

🛣️ 3. Sayfalama (Routing) Nasıl Çalışır?

Nuxt'ta routing otomatiktir! Dosya yapınız URL'nizi belirler.

Dosya Yapısı → URL Eşleşmesi

Dosya YoluURLAçıklama
pages/index.vue/Ana sayfa
pages/about.vue/aboutHakkında sayfası
pages/blog/index.vue/blogBlog listesi
pages/blog/[slug].vue/blog/nuxt-kurulumuDinamik blog detay
pages/blog/[...slug].vue/blog/2024/nuxtÇok seviyeli dinamik

Sayfa Arası Navigasyon

Sayfalar arası geçiş için <NuxtLink> kullanılır:

<template>
  <nav>
    <NuxtLink to="/">Ana Sayfa</NuxtLink>
    <NuxtLink to="/blog">Blog</NuxtLink>
    <NuxtLink to="/about">Hakkımda</NuxtLink>
  </nav>
</template>

🎨 4. Vue Dosyası Anatomisi

Bir .vue dosyası üç ana bölümden oluşur:

<template>
  <!-- HTML yapısı (görsel kısım) -->
  <div class="container">
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </div>
</template>

<script setup lang="ts">
// JavaScript/TypeScript kodu (mantık kısmı)
// setup: Composition API kullanımı

// Reactive değişkenler
const title = ref('Blog Başlığı')
const content = ref('Blog içeriği...')

// Fonksiyonlar
const updateTitle = () => {
  title.value = 'Yeni Başlık'
}

// API çağrıları, lifecycle hooks vs.
onMounted(() => {
  console.log('Sayfa yüklendi')
})
</script>

<style scoped>
/* CSS stilleri (sadece bu component için) */
.container {
  max-width: 1200px;
  margin: 0 auto;
}
</style>

Script Setup Nedir?

<script setup> Vue 3'ün Composition API'si için kısayol syntax'ıdır:

  • Daha temiz kod yazarsınız
  • export default gerekmez
  • Değişkenler otomatik olarak template'e açılır
  • TypeScript desteği mükemmel

🔗 5. Dinamik Slug (URL Parametreleri)

Dinamik sayfalar oluşturmak için köşeli parantez [] kullanılır.

Örnek: Blog Detay Sayfası

Dosya: app/pages/blog/[slug].vue

<template>
  <div>
    <h1>Slug: {{ slug }}</h1>
    <p>Bu sayfa dinamik olarak oluşturuldu!</p>
  </div>
</template>

<script setup lang="ts">
// Route parametrelerini al
const route = useRoute()
const slug = route.params.slug as string

// URL: /blog/nuxt-kurulumu
// slug = "nuxt-kurulumu"

console.log('Slug:', slug)
</script>

Slug ile Veri Çekme

<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug as string

// Content API ile veri çek
const { data: post } = await useAsyncData(`blog-${slug}`, () => 
  queryContent('blog')
    .where({ _path: `/blog/${slug}` })
    .findOne()
)
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <ContentRenderer :value="post" />
  </article>
</template>

📝 6. Nuxt Content Modülü

Nuxt Content, Markdown dosyalarınızı okuyup Vue component'e dönüştürür. File-based CMS gibi çalışır!

Content Klasörü Yapısı

content/
└── blog/
    ├── nuxt-kurulumu.md
    ├── vue-temelleri.md
    └── typescript-rehberi.md

Markdown Dosyası Formatı

Dosya: content/blog/nuxt-kurulumu.md

---
title: 'Nuxt 4 Kurulumu'
description: 'Nuxt ile blog nasıl yapılır?'
date: '2024-11-25'
image: '/images/nuxt-logo.png'
tags: ['Nuxt', 'Vue']
---

# Başlık

Bu bir **markdown** dosyasıdır. İçeriği buraya yazıyoruz.

## Alt Başlık

- Liste öğesi 1
- Liste öğesi 2

\`\`\`javascript
console.log('Kod bloğu')
\`\`\`

Content API Kullanımı

// Tüm blog yazılarını getir
const { data: posts } = await useAsyncData('blog-posts', () =>
  queryContent('blog')
    .sort({ date: -1 })  // Tarihe göre sırala
    .limit(10)           // İlk 10 yazı
    .find()              // Çoklu sonuç
)

// Tek bir yazı getir
const { data: post } = await useAsyncData('single-post', () =>
  queryContent('blog')
    .where({ _path: '/blog/nuxt-kurulumu' })
    .findOne()  // Tek sonuç
)

// Arama yap
const { data: searchResults } = await useAsyncData('search', () =>
  queryContent('blog')
    .where({ 
      $or: [
        { title: { $contains: 'Nuxt' } },
        { tags: { $contains: 'Vue' } }
      ]
    })
    .find()
)

🎨 7. Tailwind CSS ile Blog Sayfası Tasarımı

Şimdi pratik olarak /blog ve /blog/[slug] sayfalarını tasarlayalım!

📄 Blog Listesi Sayfası: app/pages/blog/index.vue

<template>
  <div class="min-h-screen bg-gray-50 dark:bg-gray-900">
    <!-- Header -->
    <header class="bg-white dark:bg-gray-800 shadow-sm">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
        <h1 class="text-4xl font-bold text-gray-900 dark:text-white">
          📝 Blog Yazıları
        </h1>
        <p class="mt-2 text-gray-600 dark:text-gray-300">
          Web geliştirme, Vue.js ve Nuxt hakkında yazılar
        </p>
      </div>
    </header>

    <!-- Main Content -->
    <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      <!-- Loading State -->
      <div v-if="pending" class="text-center py-12">
        <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
        <p class="mt-4 text-gray-600 dark:text-gray-400">Yükleniyor...</p>
      </div>

      <!-- Error State -->
      <div v-else-if="error" class="text-center py-12 text-red-600">
        <Icon name="uil:exclamation-triangle" size="48" class="mx-auto mb-4" />
        <p>Bir hata oluştu: {{ error.message }}</p>
      </div>

      <!-- Blog Grid -->
      <div v-else-if="posts && posts.length > 0" class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
        <BlogCard 
          v-for="post in posts" 
          :key="post._path" 
          :post="post" 
        />
      </div>

      <!-- Empty State -->
      <div v-else class="text-center py-12">
        <Icon name="uil:folder-open" size="64" class="mx-auto text-gray-400 mb-4" />
        <p class="text-gray-600 dark:text-gray-400">Henüz blog yazısı yok</p>
      </div>
    </main>
  </div>
</template>

<script setup lang="ts">
// SEO Meta Tags
useSeoMeta({
  title: 'Blog | My Nuxt Blog',
  description: 'Web geliştirme, Vue.js ve Nuxt hakkında güncel blog yazıları',
  ogImage: '/images/blog-cover.jpg',
})

// Fetch blog posts
const { data: posts, pending, error } = await useAsyncData('blog-posts', () =>
  queryContent('blog')
    .only(['title', 'description', 'date', 'image', 'tags', '_path'])
    .sort({ date: -1 })
    .find()
)
</script>

🃏 Blog Kartı Component: app/components/BlogCard.vue

<template>
  <NuxtLink 
    :to="post._path" 
    class="group block bg-white dark:bg-gray-800 rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden"
  >
    <!-- Image -->
    <div class="relative h-48 bg-gradient-to-br from-blue-500 to-purple-600 overflow-hidden">
      <img 
        v-if="post.image" 
        :src="post.image" 
        :alt="post.title"
        class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
      />
      <div v-else class="absolute inset-0 flex items-center justify-center">
        <Icon name="uil:file-alt" size="64" class="text-white/50" />
      </div>
      
      <!-- Date Badge -->
      <div class="absolute top-4 right-4 bg-white dark:bg-gray-800 px-3 py-1 rounded-full text-sm font-medium">
        <Icon name="uil:calendar-alt" size="16" class="inline-block mr-1" />
        {{ formatDate(post.date) }}
      </div>
    </div>

    <!-- Content -->
    <div class="p-6">
      <h2 class="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
        {{ post.title }}
      </h2>
      
      <p class="text-gray-600 dark:text-gray-300 text-sm mb-4 line-clamp-3">
        {{ post.description }}
      </p>

      <!-- Tags -->
      <div class="flex flex-wrap gap-2">
        <span 
          v-for="tag in post.tags?.slice(0, 3)" 
          :key="tag"
          class="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 text-xs rounded-md font-medium"
        >
          #{{ tag }}
        </span>
      </div>

      <!-- Read More -->
      <div class="mt-4 flex items-center text-blue-600 dark:text-blue-400 font-medium text-sm group-hover:gap-2 transition-all">
        Devamını Oku
        <Icon name="uil:arrow-right" size="16" class="ml-1 group-hover:translate-x-1 transition-transform" />
      </div>
    </div>
  </NuxtLink>
</template>

<script setup lang="ts">
defineProps<{
  post: {
    title: string
    description: string
    date: string
    image?: string
    tags?: string[]
    _path: string
  }
}>()

const formatDate = (dateString: string) => {
  return new Date(dateString).toLocaleDateString('tr-TR', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
}
</script>

📖 Blog Detay Sayfası: app/pages/blog/[slug].vue

<template>
  <div class="min-h-screen bg-gray-50 dark:bg-gray-900">
    <div v-if="post" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      <article class="bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden">
        <!-- Featured Image -->
        <div v-if="post.image" class="relative h-96 bg-gradient-to-br from-blue-500 to-purple-600">
          <img 
            :src="post.image" 
            :alt="post.title"
            class="w-full h-full object-cover"
          />
        </div>

        <!-- Content -->
        <div class="p-8 lg:p-12">
          <!-- Meta Info -->
          <div class="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400 mb-6">
            <div class="flex items-center gap-1">
              <Icon name="uil:calendar-alt" size="16" />
              {{ formatDate(post.date) }}
            </div>
            <div class="flex items-center gap-1" v-if="readingTime">
              <Icon name="uil:clock" size="16" />
              {{ readingTime }} dk okuma
            </div>
          </div>

          <!-- Title -->
          <h1 class="text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-4">
            {{ post.title }}
          </h1>

          <!-- Description -->
          <p class="text-xl text-gray-600 dark:text-gray-300 mb-6">
            {{ post.description }}
          </p>

          <!-- Tags -->
          <div class="flex flex-wrap gap-2 mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
            <span 
              v-for="tag in post.tags" 
              :key="tag"
              class="px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 text-sm rounded-full font-medium"
            >
              #{{ tag }}
            </span>
          </div>

          <!-- Article Content -->
          <div class="prose prose-lg dark:prose-invert max-w-none">
            <ContentRenderer :value="post" />
          </div>

          <!-- Share Section -->
          <div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
            <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
              Bu yazıyı paylaş
            </h3>
            <div class="flex gap-4">
              <a 
                :href="`https://twitter.com/intent/tweet?url=${shareUrl}&text=${post.title}`"
                target="_blank"
                class="flex items-center gap-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
              >
                <Icon name="uil:twitter" size="20" />
                Twitter
              </a>
              <a 
                :href="`https://www.linkedin.com/shareArticle?mini=true&url=${shareUrl}&title=${post.title}`"
                target="_blank"
                class="flex items-center gap-2 px-4 py-2 bg-blue-700 hover:bg-blue-800 text-white rounded-lg transition-colors"
              >
                <Icon name="uil:linkedin" size="20" />
                LinkedIn
              </a>
            </div>
          </div>

          <!-- Back to Blog -->
          <div class="mt-8">
            <NuxtLink 
              to="/blog"
              class="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
            >
              <Icon name="uil:arrow-left" size="20" />
              Tüm Yazılara Dön
            </NuxtLink>
          </div>
        </div>
      </article>
    </div>

    <!-- Not Found -->
    <div v-else class="max-w-2xl mx-auto px-4 py-24 text-center">
      <Icon name="uil:exclamation-triangle" size="64" class="mx-auto text-red-500 mb-4" />
      <h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
        Yazı Bulunamadı
      </h1>
      <p class="text-gray-600 dark:text-gray-400 mb-8">
        Aradığınız blog yazısı mevcut değil veya kaldırılmış.
      </p>
      <NuxtLink 
        to="/blog"
        class="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
      >
        <Icon name="uil:arrow-left" size="20" />
        Blog'a Dön
      </NuxtLink>
    </div>
  </div>
</template>

<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug as string

// Fetch blog post
const { data: post } = await useAsyncData(`blog-${slug}`, () =>
  queryContent('blog')
    .where({ _path: `/blog/${slug}` })
    .findOne()
)

// Calculate reading time
const readingTime = computed(() => {
  if (!post.value?.body) return null
  const text = JSON.stringify(post.value.body)
  const wordCount = text.split(/\s+/).length
  return Math.ceil(wordCount / 200) // 200 words per minute
})

// Share URL
const shareUrl = computed(() => {
  if (typeof window === 'undefined') return ''
  return encodeURIComponent(window.location.href)
})

// SEO
useSeoMeta({
  title: post.value ? `${post.value.title} | My Blog` : 'Blog',
  description: post.value?.description,
  ogImage: post.value?.image,
  ogType: 'article',
  articlePublishedTime: post.value?.date,
})

// Date formatter
const formatDate = (dateString: string) => {
  return new Date(dateString).toLocaleDateString('tr-TR', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
}
</script>

🎨 Tailwind Yapılandırması

tailwind.config.ts dosyanızı güncelleyin:

import type { Config } from 'tailwindcss'

export default <Config>{
  darkMode: 'class',
  content: [
    './components/**/*.{js,vue,ts}',
    './layouts/**/*.vue',
    './pages/**/*.vue',
    './plugins/**/*.{js,ts}',
    './app.vue',
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
}

🚀 Projeyi Çalıştırma

# Geliştirme modu
npm run dev

# Production build
npm run build

# Production önizleme
npm run preview

Tarayıcınızda http://localhost:3000 adresine gidin ve blog'unuzu görün!

✅ Özet

Bu yazıda öğrendiklerimiz:

  1. ✅ Nuxt 4 projesi kurulumu
  2. ✅ App klasörü yapısı ve mantığı
  3. ✅ Otomatik routing sistemi
  4. ✅ Vue dosya anatomisi (<template>, <script>, <style>)
  5. ✅ Dinamik slug kullanımı [slug].vue
  6. ✅ Nuxt Content modülü ile Markdown yönetimi
  7. ✅ Tailwind CSS ile modern blog tasarımı
  8. ✅ SEO optimizasyonu

Artık kendi blog projenizi geliştirebilirsiniz! 🎉

📚 Faydalı Kaynaklar


Sorularınız varsa yorum bırakabilirsiniz! 💬