
Sanity + Next.js: Backend'siz Full-Stack Deneyim
Diyelim ki içeriklerinizi dinamik olarak yönetmek istediğiniz bir web siteniz var. Ancak bunun için ayrıca bir server kurmak istemiyorsunuz. Çünkü bu; yeni bir proje, yeni bir mimari, ayrı bir veritabanı ve eğer hazırda yok ise bir CDN servisi demek. Üstelik her ortam (local, test, production) için ayrı veritabanları kurmanız, yapılandırmanız ve senkron tutmanız gerekiyor.
Tüm bunlara ek olarak, revalidate ve cache yönetimi gibi sistemleri de sıfırdan kurmanız gerekiyor. Süreç; maliyet, zaman ve bakım açısından öyle büyüyor ki, bazen içeriği doğrudan kodun içine statik olarak eklemek bile daha kolay geliyor. Peki Sanity bu noktada neyi çözmeye çalışıyor?
Sanity Nedir?
Sanity, bulut tabanlı bir headless CMS platformu; yani içeriklerinizi yönetebileceğiniz bir arayüz, bu içeriklere erişebileceğiniz güçlü bir API ve bu verileri sunan bir altyapı sağlıyor. Modern ve temiz bir arayüze sahiptir.
En önemli farkı, kendi backend'inizi kurmadan çalışabilmenizdir. Sanity; veri saklama, CDN üzerinden hızlı medya dağıtımı, sorgulama ve içerik düzenleme arayüzünü tek bir servis altında sunuyor.
Temel Özellikleri
- Headless mimari: Next.js ve içerik yönetimi tamamen ayrı çalışır; veri alışverişi yalnızca Sanity'nin sunduğu API üzerinden gerçekleşir.
- CDN destekli medya yönetimi: Görselleriniz ve dosyalarınız otomatik olarak Sanity'nin global CDN ağı üzerinden dağıtılır. (Bunu yaparken de resimleri arka planda otomatik olarak WebP'ye dönüştürdüğünü unutmayalım.)
- React tabanlı özelleştirilebilir arayüz: Modern ve temiz arayüzünü, React bileşenleri ekleyerek veya mevcut görünümleri değiştirerek kendi ihtiyaçlarınıza göre uyarlayabilirsiniz.
- GROQ ile esnek veri sorgulama: İçeriklerinize özel sorgular yazarak yalnızca ihtiyacınız olan veriyi çekebilirsiniz.
- Webhook desteği: İçerikteki değişiklikleri (create, update, delete) otomatik olarak tespit ederek belirttiğiniz sistemleri tetikleyebilir, cache revalidation veya dış servisleri kolayca yönetebilir.
Kurulum
Başlamadan önce, burada anlattığım her şey GitHub reposunda yapılmış bir şekilde var. Direkt projeyi çekip oradan takip edebilirsin.
Sanity Studio'yu projeye entegre etmenin iki yolu vardır:
- Studio ve Next.js uygulamasını ayrı klasörlerde yönetmek (Sanity dokümantasyonunda bu yöntem anlatılır).
- Studio'yu doğrudan Next.js projesine gömerek bir route olarak çalıştırmak.
Ben ikinci yöntemi tercih ediyorum. Tek repo ve tek deploy ile her şeyin bir arada olması, yönetimi çok daha kolay hale getiriyor.
Sanity Hesabı Oluşturma
- Sanity.io adresine gidin.
- "Get started" butonuna tıklayın.
- GitHub, Google veya e-posta ile kayıt olabilirsiniz.
- İlk girişte sizin için özelleştirilmiş bir proje oluşturmak amacıyla adım adım sorular var; bir proje adı ve veri seti (dataset) ismi isteyecek (varsayılan
productionolabilir).
Next.js Projesi ve Sanity Paket Kurulumu
Bu rehberi Next.js v15.4.6 App Router ile hazırladım. Farklı sürümlerde bazı bağımlılıkların versiyon uyumsuzluğu nedeniyle kurulum sırasında hata alabilirsiniz. Kuruluma başlamadan önce, kullandığınız Next.js sürümü ile sanity, next-sanity, @sanity/vision, @sanity/image-url gibi paketlerin uyumluluğunu kontrol etmenizi öneririm.
# Yeni Next.js projesi oluştur (veya mevcut projeye gir)
npx create-next-app@latest my-app --typescript
cd my-app
# Bağımlılıkları yükle
npm i sanity next-sanity @sanity/vision @sanity/image-url
npm i -D @sanity/types
Kök dizinde bir sanity.config.ts oluşturuyoruz.
Varsayılan desk yapısı yerine structureTool kullanarak yönetim panelini özelleştiriyorum. İsterseniz, plugins içindeki structureTool tanımını kaldırarak varsayılan desk yapısını kullanabilirsiniz.
'use client';
import { defineConfig } from 'sanity';
import { visionTool } from '@sanity/vision';
import { structureTool } from 'sanity/structure';
import { defineType, defineField } from 'sanity';
const post = defineType({
name: 'post',
type: 'document',
title: 'Post',
fields: [
defineField({ name: 'title', type: 'string', title: 'Title' }),
defineField({
name: 'slug',
type: 'slug',
title: 'Slug',
options: { source: 'title' },
}),
defineField({ name: 'coverImage', type: 'image', title: 'Cover Image' }),
],
});
export default defineConfig({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, // Sanity.io panelinde oluşturduğunuz projenin id'si.
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, // Değiştirmediyseniz 'production' olacak.
title: 'My Sanity Studio',
schema: { types: [post] },
basePath: '/studio', // Belirlenen rotanın adıyla aynı olmalı.
// Desk yapısını özelleştir
plugins: [
structureTool({
structure: (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Posts')
.child(
S.documentTypeList('post')
.title('Posts')
.defaultOrdering([{ field: '_createdAt', direction: 'desc' }])
),
]),
}),
visionTool(), // GROQ sorgularını canlı olarak yazıp test etmemizi sağlıyor.
],
});
Lib klasöründe bir sanity.client.ts oluşturuyoruz.
Burada, GROQ sorgularımızın Sanity ile iletişim kurabilmesi için bir client oluşturuyoruz. apiVersion değerini belirtmezsek en güncel versiyon kullanılır. Ancak bu, ileride yapılacak güncellemelerde mevcut sorguların beklenmedik şekilde bozulma riskini artırır. Bu nedenle, API versiyonunu sabit bir tarihle tanımlamak ve yeni sürümleri önce test edip ardından yükseltmek daha sağlıklı bir yaklaşım olacaktır.
// lib/sanity.client.ts
import { createClient } from 'next-sanity';
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!;
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!;
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-01-01';
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: false, // Tag-based revalidation için CDN kapalı
perspective: 'published',
});
Lib klasöründe bir sanity.image.ts oluşturuyoruz.
Sanity, medya dosyalarını kendi global CDN'i üzerinden servis eder. Görseller üzerinde boyutlandırma, kırpma, format değiştirme (örn. otomatik WebP) gibi optimizasyonları yapabilmek için @sanity/image-url paketini kullanıyoruz.
// lib/sanity.image.ts
import imageUrlBuilder from '@sanity/image-url';
import { client } from './sanity.client';
const builder = imageUrlBuilder(client);
export function urlFor(source: any) {
return builder.image(source);
}
/*
* Örnek kullanım:
* <img src={urlFor(post.coverImage).width(800).auto("format").url()} alt={post.title} />
*/
💡 Not: Next.js'in next/image bileşenini kullanacaksanız, next.config.ts dosyasında Sanity'nin CDN domainine izin vermeniz gerekir:
//next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.sanity.io',
port: '',
pathname: '/**',
},
],
},
};
export default nextConfig;
Studio Route Oluşturma
Sanity Studio'yu Next.js projemize gömerek /studio rotasında çalıştırıyoruz. Bunun için app/studio/[[...tool]]/page.tsx dosyasını oluşturuyoruz:
// app/studio/[[...tool]]/page.tsx
import { NextStudio } from "next-sanity/studio";
import config from "../../../../sanity.config";
export { metadata, viewport } from "next-sanity/studio"; // Studio için gerekli <head> bilgilerini ekler.
export default function StudioPage() {
return <NextStudio config={config} />;
}
Studio'ya İlk Giriş
Projeyi başlatın. Tarayıcıdan http://localhost:3001/studio (kendi portunuza göre) adresine gidin. İlk açılışta Studio, bulunduğunuz origin'i http://localhost:3001 CORS allowlist'e eklemenizi ister. Daha sonrasında Sanity hesabınızla oturum açın. Girişten sonra özelleştirdiğiniz Studio (sol menüde "Posts") açılacaktır. Burada test için bir data yaratalım.

Next.js ile Veri Çekme
posts.ts servisini oluşturuyoruz
Aşağıdaki servis, GROQ sorgusuyla post'ları çekip Next.js'in tag-based cache mekanizmasıyla etiketleyerek döner. Böylece Sanity'de içerik değiştiğinde, webhook'tan yalnızca bu etiketi revalidate ederek sayfaları akıllıca yenileyebiliriz.
// lib/services/posts.ts
import { groq } from 'next-sanity';
import { client } from '../sanity.client';
export type Post = {
_id: string;
title: string;
slug: string;
coverImage?: any;
};
export const postsQuery = groq`*[_type == "post"] | order(_createdAt desc){
_id,
title,
"slug": slug.current,
coverImage
}`;
export async function getPosts(): Promise<Post[]> {
return client.fetch(
postsQuery,
{},
{
next: { tags: ['posts'] },
}
);
}
page.tsx sayfasını düzenleyelim
Aşağıdaki sayfa, server component içinde getPosts() ile veriyi GROQ üzerinden çeker ve tag-based cache sayesinde "posts" etiketiyle cache'lenir.
import { getPosts, type Post } from "@/lib/services/posts";
import { urlFor } from "@/lib/sanity.image";
import Image from "next/image";
export default async function Home() {
const posts: Post[] = await getPosts();
return (
<div className="font-sans min-h-screen p-8">
<main className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8 text-center">Blog Posts</h1>
{posts.length === 0 ? (
<p className="text-gray-600 text-center">No posts found.</p>
) : (
<div className="grid gap-6">
{posts.map((post) => (
<article key={post._id} className="max-w-sm mx-auto">
{post.coverImage && (
<div>
<Image
src={urlFor(post.coverImage).url()}
alt={post.title}
width={300}
height={200}
className="w-full h-auto rounded-lg"
/>
<div className="mt-3">
<h2 className="text-lg font-semibold mb-1">
{post.title}
</h2>
<p className="text-sm">Slug: {post.slug}</p>
</div>
</div>
)}
</article>
))}
</div>
)}
</main>
</div>
);
}
Code snippet'ini yerleştirdikten sonra npm run build ve npm run start ile projenizi production modunda çalıştırın. Not: Development modunda (npm run dev) Next.js tag cache çalışmaz.
Tarayıcıda sayfayı açın ve getPosts() ile gelen post'ların listelendiğinden emin olun. /studio adresine gidin, herhangi bir post'ta küçük bir değişiklik yapın ve Publish edin.
Sayfayı yenileyin, içeriğin değişmediğini göreceksiniz. Network panelinde Document isteğinin Response Headers kısmında şunu görebilirsiniz:
cache-control: s-maxage=31536000, stale-while-revalidate
Buradaki s-maxage=31536000, 31.536.000 saniye (yaklaşık 1 yıl) anlamına gelir. Yani belge, CDN/proxy katmanında uzun süre cache'lenecektir. Peki bu durumda içeriği nasıl güncel tutacağız?
Sanity Webhook Kurulumu
Bu bölümde webhook akışını üç adımda kuracağız. Öncelikle lokalde test edebilmek için uygulamayı ngrok ile internete açalım.
npm i -g ngrok
# veya: brew install ngrok (macOS)
ngrok config add-authtoken <senin-ngrok-authtoken'in>
ngrok http 3001 # Projenin çalıştığı port olduğuna dikkat et.

Ngrok'ın verdiği HTTPS adresi, webhook'ta kullanacağınız temel host'tur. Sanity.io panelinde post create/update/delete olaylarında tetiklenecek custom bir webhook tanımlayalım. API > Webhooks sayfası içerisinden yeni bir webhook yaratalım.

Trigger on bölümünde Create, Update ve Delete seçeneklerini etkinleştirelim. URL alanına, bir sonraki adımda ekleyeceğimiz /api/revalidate rotasını, bir önceki adımda oluşturduğumuz ngrok adresiyle birlikte yazalım.
Örnek: https://<ngrok-subdomain>.ngrok-free.app/api/revalidate?secret=YOUR_SECRET_KEY
Next.js tarafında gizli anahtar doğrulaması yapan ve ilgili etiketleri revalidateTag ile yenileyen basit bir endpoint ekleyelim. /api/revalidate/route.ts dosyası oluşturalım.
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const secret = process.env.SANITY_WEBHOOK_SECRET; //"YOUR_SECRET_KEY"
// Parse edilen token'ın secret ile aynı olması gerekiyor.
const token = new URL(request.url).searchParams.get('secret');
if (!secret || secret !== token) {
return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
}
const body = await request.json();
const { _type } = body;
let tag: string | null = null;
switch (_type) {
case 'post':
tag = 'posts';
break;
default:
return NextResponse.json({ message: `Unknown document type: ${_type}` }, { status: 400 });
}
revalidateTag(tag);
console.log(`✅ Revalidated cache for: ${tag}`);
return NextResponse.json({
revalidated: true,
tag,
now: Date.now(),
});
} catch (error) {
console.error('❌ Revalidation error:', error);
return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
}
}
Tüm değişiklikleri yaptıysanız projeyi production modunda yeniden başlatın:
npm run build && npm run start
Ardından Studio üzerinden ilgili post'u güncelleyip Publish edin. Daha sonra Next.js uygulamanızın terminalinde buna benzer bir çıktı görmelisiniz:
✅ Revalidated cache for: posts
POST /api/revalidate?secret=YOUR_SECRET_KEY 200 in 167ms
Revalidation tamam! Şimdi tarayıcıya dönüp sayfayı yenileyin ve güncel değerlerin yansıdığını doğrulayın.
Not: Logları görmüyorsanız secret eşleşmesini, webhook URL'sinin (ngrok adresiyle) doğru olduğunu ve etiket adının (posts) servisinizde kullandığınızla aynı olduğunu kontrol edin.
Öneriler
- Küçük ve orta ölçekli projeler (blog, dokümantasyon, pazarlama siteleri, MVP'ler) için Sanity + Next.js kombinasyonu çok ideal: kurulum hafif, bakım maliyeti düşük, performans yüksek.
- Sanity'nin ücretsiz planı çoğu senaryoda fazlasıyla yeterli. İçerik modelleme, CDN üzerinden medya, GROQ, webhook'lar gibi ihtiyaçları rahatlıkla karşılar.
- Proje büyüdükçe (trafik/artan içerik hacmi, daha karmaşık iş akışları, SLA beklentisi) ücretli planlara geçiş değerlendirilebilir.
- Büyük ekipler/kurumsal süreçler için ücretsiz plan; ekip içi yetkilendirme, eşzamanlı çalışma ve yönetişim tarafında sınırlı kalabilir; bu durumda ücretli planlar veya farklı çözümler düşünülmelidir.
Kapanış
Özetle Sanity + Next.js ile ayrı bir backend kurmadan "full-stack" bir akış kurduk: Studio'yu projeye /studio rotasıyla gömdük, görselleri Sanity CDN üzerinden otomatik format/optimizasyon (çoğunlukla WebP) ile servis ettik. Ardından getPosts() servisiyle Next.js tag-based cache kullandık ve Sanity webhooks → Next.js revalidateTag hattını kurarak prod modda uzun cache ömrüne rağmen değişiklikleri anında yansıttık.
Bu mimari; içerik ekibinin Studio üzerinden bağımsız ilerlemesini, geliştiricinin ise performansı (cache/CDN) ve bakım maliyetini kontrol altında tutmasını sağlıyor. Tek repo/tek deploy ile yönetilebilirlik artarken, kurulum ve operasyon yükü minimuma iniyor.
Buraya kadar okuduysanız, teşekkür ederim; bir tanesiniz.