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
.vuedosyası 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 Yolu | URL | Açıklama |
|---|---|---|
pages/index.vue | / | Ana sayfa |
pages/about.vue | /about | Hakkında sayfası |
pages/blog/index.vue | /blog | Blog listesi |
pages/blog/[slug].vue | /blog/nuxt-kurulumu | Dinamik 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 defaultgerekmez- 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:
- ✅ Nuxt 4 projesi kurulumu
- ✅ App klasörü yapısı ve mantığı
- ✅ Otomatik routing sistemi
- ✅ Vue dosya anatomisi (
<template>,<script>,<style>) - ✅ Dinamik slug kullanımı
[slug].vue - ✅ Nuxt Content modülü ile Markdown yönetimi
- ✅ Tailwind CSS ile modern blog tasarımı
- ✅ SEO optimizasyonu
Artık kendi blog projenizi geliştirebilirsiniz! 🎉
📚 Faydalı Kaynaklar
Sorularınız varsa yorum bırakabilirsiniz! 💬