Ana içeriğe atla
OZAN ALTINBAŞ
Performans ve SEO

SFCC Önbellek Rehberi: Page Cache, Custom Cache ve eCDN

Yazar: 11 dk okuma

Salesforce B2C Commerce'te önbellek tek bir ayardan ibaret değildir. Bir istek eCDN'den, web katmanındaki page cache'ten (sayfa önbelleği) ya da uygulama sunucusundan yanıtlanabilir; her katmanın kendi API'si, kendi geçersiz kılma (invalidation) kuralları ve kendine özgü hata biçimleri vardır. Salesforce'un Caching Strategies for Salesforce B2C Commerce dokümanı yaygın yanılgıyı açıkça adlandırır: önbellek, "birkaç template'e <iscache> eklemekten" ibaret değildir.

Bu rehber bir isteği katman katman takip ediyor; SFRA cache middleware, dw.system.Response, caches.json ve dw.system.CacheMgr için minimal ve doğru kod veriyor ve neyin nerede önbelleğe alınacağını gösteren bir tabloyla bitiyor. Aktarılan her limit Salesforce dokümantasyonundan geliyor. Platformun genel resmi için SFCC rehberiyle başlayın.


Önbellek katmanlarına genel bakış

Salesforce'un kuralı, önbellekteki içeriği alışverişçiye olabildiğince yakından sunmaktır; çünkü isteğin geçtiği her katman zaman ve kapasite maliyeti demektir.

KatmanNe tutarNasıl kontrol edilirNasıl temizlenir
eCDNEdge'de statik storefront içeriğiCache-Control TTL, fingerprint'li asset URL'leriTTL'in dolması veya CDN Zones API purge
Static content cache (web katmanı)Görseller, CSS, JSBusiness Manager'da sitenin Cache sekmesindeki TTLBusiness Manager, InvalidateCache job step'i, replikasyon
Page cache (web katmanı)Controller yanıtlarının tamamı ve remote include parçalarıresponse.setExpires(), SFRA cache middleware veya <iscache>TTL'in dolması, replikasyon, Business Manager
SCAPI web katmanı önbelleğiUygun Shopper API GET yanıtlarıPage caching açık; Custom API'lerde setExpires()Kendi TTL ve invalidation kuralları (web katmanı önbellek dokümanına bakın)
Custom cache (uygulama sunucusu)Pahalı hesaplamaların veya dış çağrıların sonuçlarıcaches.json ve dw.system.CacheMgrexpireAfterSeconds, kod aktivasyonu, replikasyon
Request ve sessionİstek veya alışverişçi bazlı veriModül kapsamı, request.custom, session.privacyİsteğin veya oturumun sonu

Custom object'ler bu listede bilerek yok: uygulama sunucuları arasında tutarlıdırlar ama her okuma tüm katmanlardan geçer; yani önbellek değil, depolamadırlar.


SFCC page cache nasıl çalışır?

Page cache, web katmanında, uygulama sunucularının önünde durur. Salesforce onu, anahtarı query string dahil tam URL olan bir key-value deposu olarak tanımlar. Hit durumunda web katmanı saklanan yanıtı sunar ve içindeki remote include'ları aynı şekilde çözer; miss durumunda uygulama sunucusunu çağırır ve yanıt önbelleğe alınabilir olarak işaretlenmişse saklar. Bundan üç sonuç çıkar:

  1. Hit durumunda kodunuz çalışmaz. Controller, middleware, hook veya loglama yoktur; uygulama ve depolama katmanlarına yük binmez.
  2. Her farklı URL ayrı bir kayıttır. Farklı bir parametre değeri, fazladan bir parametre ya da farklı bir parametre sırası yeni kayıtlar oluşturur. Salesforce'un adını koyduğu anti-pattern, ürün tile'larına pozisyon parametresi eklemektir: aynı tile, arama sonucundaki her pozisyon için ayrı ayrı önbelleğe alınır.
  3. Varsayılan olarak hiçbir şey önbelleğe alınmaz. dw.system.Response referansına göre yanıtlar yalnızca kod bunu açıkça istediğinde önbelleğe alınır.

Trailhead'in Implement Caching in the Web-Server Layer ünitesine göre Administration > Global Preferences > Feature Switches altındaki iki feature switch işe yarar: Web Adapter Cache Key Ignore by Query String, kampanya ID'si gibi anlamsız parametreleri anahtardan çıkarır; Enable 404 response caching overrides ise 404 yanıtlarının önbelleğe alınmasını sağlar.

Business Manager'da page caching'i etkinleştirme

Page caching, site bazında Administration > Sites > Manage Sites > (site) > Cache altında Enable Page Caching ile ayarlanır; kapalıyken <iscache> yok sayılır. Content Cache dokümantasyonu ayarları instance türüne göre kısıtlar:

Instance türüKısıtlama
Sandbox veya developmentYok
StagingProduction ayarına ait değerler değiştirilemez
ProductionPage cache kapatılamaz ve Cache sekmesindeki TTL en az 24 saattir

Salesforce, değişiklikleri anında görmek için sandbox ve staging'de page caching'i kapatmayı önerir. Yeni bir endpoint'i test ederken tekrar açın; aksi hâlde önbellek hataları ancak production'da ortaya çıkar.


Önbellek sürelerini belirleme: SFRA middleware, script API ve <iscache>

Önbellek süresini iki API belirler: dw.system.Response#setExpires() ve ISML <iscache> tag'i. İkisi de tek bir template'e değil yanıtın tamamına uygulanır ve süre birden fazla kez ayarlanırsa en düşük değer kazanır. Salesforce script API'yi güçlü biçimde önerir; çünkü iç içe template'lerde süreyi hangisinin belirlediğini anlamak zordur.

SFRA: cache middleware

SFRA'nın app_storefront_base cartridge'ı cartridge/scripts/middleware/cache.js dosyasıyla gelir. SFRA 6.1'de bu dosya dört fonksiyon export eder:

MiddlewareÖnbellek süresiKişiselleştirilmiş (price_promotion)Base SFRA'daki örnekler
applyDefaultCache24 saatHayırHome-Show, Page-Show, Page-Include
applyPromotionSensitiveCache24 saatEvetProduct-Show, Tile-Show
applyShortPromotionSensitiveCache1 saatEvetSearch-Show, Search-ShowAjax
applyInventorySensitiveCache30 dakikaHayırKendi route'larınız için hazır

Middleware yalnızca res.cachePeriod, res.cachePeriodUnit ve res.personalized değerlerini ayarlar; route tamamlandığında SFRA'nın server.js dosyası bunları setExpires() çağrısına ve kişiselleştirme varsa setVaryBy('price_promotion') çağrısına çevirir:

'use strict';

var server = require('server');
var cache = require('*/cartridge/scripts/middleware/cache');

// Brand-Show: haftada birkaç kez değişen bir landing sayfası.
// applyDefaultCache, res.cachePeriod = 24 (saat) ayarlar.
server.get('Show', cache.applyDefaultCache, function (req, res, next) {
    res.render('brand/landing', { brandId: req.querystring.bid });
    next();
});

module.exports = server.exports();

Hazır fonksiyonların karşılamadığı bir TTL için res.cachePeriod ve res.cachePeriodUnit değerlerini ayarlayan küçük bir middleware yazın (SFRA 'minutes' değerini okur, geri kalan her şeyi saat sayar); böylece politika route imzasında görünür kalır.

SFRA dışında: setExpires() ve setVaryBy()

SFRA dışı bir controller'da veya bir Custom API script'inde response API'sini doğrudan çağırın. setExpires(), epoch'tan bu yana geçen milisaniyeyi ya da bir Date nesnesini alır:

// Bu yanıtı 30 dakika önbellekte tut
response.setExpires(Date.now() + 30 * 60 * 1000);

// Yalnızca çıktı gerçekten price book veya promosyona göre değişiyorsa
response.setVaryBy('price_promotion');

Önbellek süreleri sabit olmak zorunda değil: Caching Strategies dokümanı, ürün sayfasının TTL'ini availabilityModel.isOrderable(n) ile kademelendirir; stok derinse uzun, tükenmek üzereyse kısa.

Template'lerde <iscache>

Bu tag'e SiteGenesis'te ve eski cartridge'larda hâlâ rastlarsınız. iscache element referansına göre relative sayfayı belirli bir süre sonra, daily ise günün sabit bir saatinde geçersiz kılar:

<iscomment>Sayfa oluşturulduktan 24 saat sonra geçersiz olur</iscomment>
<iscache type="relative" hour="24" />

<iscomment>Her gün 03:30'da geçersiz olur</iscomment>
<iscache type="daily" hour="3" minute="30" />

<iscomment>Kişiselleştirilmiş: her price book ve promosyon kombinasyonu için bir kayıt</iscomment>
<iscache type="relative" hour="1" varyby="price_promotion" />

Tuzak local include'lardadır: template'teki ve local olarak include edilen tüm template'lerdeki en düşük ayar sayfanın tamamını belirler. Paylaşılan bir partial'da unutulmuş kısa bir <iscache>, onu kullanan her sayfanın süresini sessizce kısaltır.


Remote include: kişiselleştirilmiş parçalar içeren önbellekli sayfalar

<isinclude template="..."> bir local include'dur: aynı istekte render edilir ve aynı önbellek kaydının parçası olur. <isinclude url="..."> ise bir remote include'dur: web katmanı onu kendi önbellek politikasıyla ayrı bir istek olarak çeker ve sonucu önbellekteki çerçevenin içine yerleştirir. Bir gün boyunca önbellekte duran bir sayfanın alışverişçinin oturum durumunu yine de gösterebilmesi bu sayede olur. SFRA'nın header'ı tam olarak bunu yapar:

<iscomment>pageHeader.isml: bu satırların çevresindeki sayfa önbellektedir; her include ayrı bir istektir</iscomment>
<isinclude url="${URLUtils.url('Account-Header')}" />
<isinclude url="${URLUtils.url('Cart-MiniCart')}" />
<isinclude url="${URLUtils.url('Page-IncludeHeaderMenu')}" />

Account-Header ve Cart-MiniCart, server.middleware.include kullanır (doğrudan gelen istekleri reddeder) ve cache middleware'leri yoktur; dolayısıyla her istekte render edilirler. Page-IncludeHeaderMenu ise applyDefaultCache kullanır, çünkü menü herkes için aynıdır.

  • Yalnızca çıktıyı değiştiren parametreleri geçirin. Include URL'leri de birer önbellek anahtarıdır.
  • İç içe geçmeyi sığ tutun. Salesforce'un troubleshooting ünitesi üç ile yedi seviye arasını önerir; ne kadar az, o kadar iyi.
  • Geçersiz kılma pipeline bazındadır. Home-Show'u geçersiz kılmak, içindeki remote include'ları yenilemez.
  • Mevcut dinamik include'ları yeniden kullanın. Salesforce'un örneği: her tile için bir wishlist kalbi render etmek yerine wishlist'i zaten dinamik olan bir include'da data attribute olarak sunun ve ikonları client tarafında güncelleyin.

Page Designer da aynı modeli kullanır: PageMgr.renderPage(), iç içe iki remote include üzerinden render eder: görünürlük parmak izini (visibility fingerprint) hesaplayan __SYSTEM__Page-Include ve __SYSTEM__Page-Render. Bu yüzden her görünürlük varyasyonu ayrı önbelleğe alınır. Content slot'lar da remote include içinde sunulur.


Kişiselleştirilmiş önbellek: price_promotion neye göre ayrışır?

setVaryBy() referansı nettir: platform varyantları price book, promosyon, sıralama kuralı (sorting rule) ve A/B test segmenti kombinasyonuna göre ayırır ve desteklenen tek değer price_promotion'dır. Başka bir yolla kişiselleştirilmiş bir sayfa önbelleğe alınmamalıdır, çünkü alışverişçiler yanlış varyantı görür. İstekteki herhangi bir şey vary-by ayarladığında yanıtın tamamı kişiselleştirilmiş sayılır ve kayıt sayısı her price book ve promosyon kombinasyonuyla katlanır.

  • HTML'de fiyat veya promosyon ibaresi varsa: SFRA'nın PDP, tile ve arama için yaptığı gibi promosyona duyarlı bir önbellek kullanın.
  • Başka bir şeye göre değişen çıktı (alışverişçi adı, sepet, session verisi): önbelleksiz bir remote include'a veya client tarafı bir çağrıya taşıyın.
  • Sayfada fiyat yoksa vary-by da olmasın. Hit oranından yer, karşılığında hiçbir şey vermez.

dw.system.CacheMgr ile custom cache

Bir yanıt page cache'e alınamıyorsa (sepet, checkout, hesap, önbelleksiz include'lar) içindeki pahalı işi önbelleğe alın. Custom Caches dokümantasyonu tipik adayları sayar: birleştirilmiş konfigürasyon, mağaza stoku gibi dış sistem yanıtları ve "bu ürünün herhangi bir varyantı promosyonda mı" gibi türetilmiş veriler.

Limitler ve davranış

  • Tüm custom cache'ler için uygulama sunucusu başına yaklaşık 20 MB ayrılır; bir code version en fazla 100 cache destekler.
  • Kayıtlar 128 KB ile sınırlıdır. Daha büyük bir loader sonucu kodunuza döner ama saklanmaz ve custom warn log'a bir uyarı düşer.
  • Cache'ler uygulama sunucusu başınadır ve senkronize edilmez; invalidate(key) yalnızca mevcut sunucudaki kaydı siler.
  • Aktif code version'da bir dosya değiştiğinde, bir code version aktive edildiğinde ve veri ya da kod replikasyonundan sonra tüm custom cache'ler temizlenir.
  • Yalnızca primitive değerler ve ağaç yapılı nesneler saklanabilir ve okumalar değiştirilemez (immutable) kopyalar döndürür; undefined ve Script API nesnelerine izin verilmez (dw.system.Cache referansı).

Cache tanımlama: package.json ve caches.json

Cartridge kök dizinindeki package.json, tanım dosyasını kendisine göreli bir yolla gösterir. Cartridge zaten hook kaydediyorsa caches anahtarını aynı dosyaya ekleyin:

{
  "hooks": "./hooks.json",
  "caches": "./caches.json"
}

Ardından cache'leri caches.json içinde tanımlayın:

{
  "caches": [
    { "id": "StorefrontConfig", "expireAfterSeconds": 3600 },
    { "id": "ExternalStoreStock", "expireAfterSeconds": 60 }
  ]
}

Her id code version genelinde benzersiz olmalıdır (tekrar eden bir ID cache'i devre dışı bırakır ve hata loglar); expireAfterSeconds isteğe bağlıdır. CacheMgr.getCache(), ID hiçbir caches.json içinde tanımlı değilse exception fırlatır. Cartridge yapısı için cartridge geliştirme rehberine bakın.

Önbellek üzerinden okuma

get(key, loader) kullanın: önbellekteki değeri döndürür ya da miss durumunda loader'ı çalıştırıp sonucu saklar.

'use strict';

var CacheMgr = require('dw/system/CacheMgr');
var Site = require('dw/system/Site');

function getStorefrontConfig() {
    var siteId = Site.getCurrent().getID();
    // Anahtarı site ve locale ile kapsamlayın; anahtara asla kişisel veri koymayın (anahtarlar loglarda görünebilir)
    var key = siteId + '_' + request.locale + '_config';

    return CacheMgr.getCache('StorefrontConfig').get(key, function loadConfig() {
        // Yalnızca miss durumunda, bu uygulama sunucusunda çalışır
        var raw = Site.getCurrent().getCustomPreferenceValue('storefrontConfigJson');
        return raw ? JSON.parse(raw) : null; // yalnızca düz veri; asla undefined döndürmeyin
    });
}

module.exports = { getStorefrontConfig: getStorefrontConfig };

storefrontConfigJson, elinizdeki pahalı kaynağın yerini tutan bir örnektir. Miss'lere göre tasarlayın: kayıtlar her an düşebilir ve her sunucu kendi kopyasını ısıtır. Tek bir istek içinde tekrar kullanılan değerler için modül düzeyinde bir değişken ya da request.custom yeterlidir; alışverişçiye özel küçük veriler için session.privacy'yi tercih edin, çünkü session.custom'a yazmak platformun dinamik customer group'ları yeniden değerlendirmesine yol açar.


Siteyi yormadan geçersiz kılma

Önbellekteki sayfalar TTL dolduğunda, bir veri ya da kod replikasyonu çalıştığında (site önbelleğini otomatik olarak geçersiz kılar) veya biri Administration > Sites > Manage Sites > (site) > Cache altında sitenin tamamını ya da tek bir page cache partition'ını geçersiz kıldığında yenilenir.

  • Temizleme kademelidir. 15 dakika sürer; dokümantasyon buna "Clear Period" der. Bu süre içinde yeniden tetiklemek süreyi sıfırlar ve önbelleğin hiçbir zaman tam temizlenmemesine yol açabilir.
  • Partition'lar etki alanını daraltır. Bir partition, Home-Show gibi controller'ları gruplar; her biri en fazla 20 pipeline içeren en fazla 10 partition oluşturabilirsiniz. Remote include pipeline'larını ayrı bir partition'a koyun.
  • Replikasyon hedeflenebilir. Bir replikasyon süreci Do not invalidate veya Invalidate Impacted Cache Partitions olarak ayarlanabilir; böylece acil bir düzeltme replike edilip yalnızca ilgili partition geçersiz kılınabilir.
  • Job'lar da bunu yapabilir. InvalidateCache job step'i InvalidatePageCache, InvalidateStaticCache ve isteğe bağlı bir PageCachePartitionIDs listesi alır.

Veriler sık sık bayat görünüyorsa nedenini bulun: Salesforce, geçersiz kılmayı tetikleyen zamanlanmış job'ları kontrol etmeyi ve sık fiyat değişiklikleri için granular replikasyon kullanmayı önerir.


eCDN: edge önbelleği ve purge

eCDN statik storefront içeriğini önbelleğe alır. Trailhead'in Clear eCDN Cache and Configure HSTS ünitesine göre bir dosya, Cache-Control TTL'i dolana kadar önbellekte kalır; header yoksa varsayılan süre 30 gündür. Statik asset URL'leri bir fingerprint taşır; değişen bir asset yeni bir URL alır ve normal bir deployment purge gerektirmez.

Gerektiğinde CDN Zones API, SCAPI short code host'unuzda POST /cdn/zones/v1/organizations/{organizationId}/zones/{zoneId}/cachepurge endpoint'ini sunar. Bir path ya da bir tags listesi alır (ikisi birden değil) ve sfcc.cdn-zones.rw scope'u gerektirir. B2C CLI bu endpoint'i sarmalar:

b2c ecdn cache purge --tenant-id zzxy_prd --zone my-zone --path "www.example.com/dw/image/v2/realm_instance/*"

eCDN purge ile page cache geçersiz kılma ayrı işlemlerdir; bir şeyi temizlemeden önce bayat yanıtı hangi katmanın tuttuğunu bulun.


Headless: SCAPI ve PWA Kit önbelleği

SCAPI, uygun GET yanıtlarını (aralarında Shopper Products, Search, SEO, Stores ve Experience var) server-side web katmanında önbelleğe alır; bunun için sitede page caching açık olmalıdır.

  • Önbellek anahtarı herhangi bir hook çalışmadan önce hesaplanır. Price book veya promosyonu değiştiren bir hook anahtarı değiştirmez; bu da hit oranını düşürür ya da yanlış kaydın sunulmasına yol açar. Kişiselleştirmeyi Shopper Context API üzerinden yapın; hook mantığının nereye ait olduğunu SFCC hook rehberi anlatıyor.
  • Custom API'ler önbelleğe açıkça dahil edilir. Custom API caching sayfasına göre page caching'i açın, ardından script'te response.setExpires() çağırın.
  • personalized=none sizin verdiğiniz bir sözdür. Shopper API spesifikasyonları bunu, yanıtın CDN katmanında önbelleğe alınabilmesi için kişiselleştirmeden çıkmak olarak tanımlar. Gereklidir ama yeterli değildir: endpoint'in web katmanında da önbelleğe alınabilir olması gerekir.

PWA Kit'te render edilen HTML, uygulamanın ayarladığı Cache-Control header'ına göre Managed Runtime CDN'inde önbelleğe alınır; Retail React App'in ana sayfası s-maxage ile birlikte stale-while-revalidate gönderir. Daha geniş karşılaştırma için SFRA vs PWA Kit yazısına bakın.


Önbellek hit oranını ölçme

Reports & Dashboards içinde Technical sekmesini açın ve toplam işlem süresindeki paya göre sıralayın. Son üç sütun; önbellekten okunan, hesaplanıp önbelleğe yazılan ve her zaman dinamik hesaplanan istekleri gösterir.

  • Son sütunda %100 görmek, endpoint'in önbelleğe alınmadığı anlamına gelir; nedeni genellikle yeni bir route'ta unutulmuş bir direktiftir.
  • Önbelleğe alınan bir endpoint orada %0 göstermelidir. Aksi, hatalara ya da önbelleği atlayan kod yollarına işaret eder.
  • Kilit sayfaları ve include'ları izleyin: Home-Show, Search-Show, Product-Show, Page-Show, ürün tile'ları, Page-Include ve slot'lar. Salesforce'a göre arama tile'ları için ideal değer en az %99 hit oranı ve 1 ms veya altında yanıt süresidir.

Ardından dinamik kalanları Administration > Operations > Code Profiler ile profilleyin (Extended Script Development modu, Category equals REQUEST filtresi, Elapsed Time > Sum sıralaması) ve canlıya çıkıştan birkaç gün sonra tüm endpoint'leri gözden geçirin. Daha hızlı yanıtlar Core Web Vitals'a da yansır; o tarafı SFCC SEO rehberi ele alıyor.


Sık yapılan hatalar

  • Yeni bir route'ta önbellek direktifi olmaması. Çalışır, sadece %100 dinamiktir.
  • Önbelleğe alınan bir controller'da yan etkiler. Session yazımları, cookie'ler veya analytics çağrıları yalnızca miss'e neden olan alışverişçi için çalışır, çünkü hit durumunda controller çalışmaz.
  • Önbellekteki bir yanıtta kişiselleştirilmiş çıktı. Dört vary-by boyutunun dışındaki her şey önbelleksiz bir remote include'a aittir.
  • Her yerde vary-by veya değişken include parametreleri. İkisi de önbellek kayıtlarını boşuna katlar.
  • Tazelik stratejisi olarak çok kısa TTL'ler. Partition'lar, granular replikasyon veya hedefli geçersiz kılma daha iyi sonuç verir.
  • Tekrarlanan manuel geçersiz kılma. Her tetikleme Clear Period'u yeniden başlatır.

Neyi nerede önbelleğe almalı?

İçerikKatmanMekanizmaBaşlangıç noktası
Görseller, CSS, JSeCDN ve static content cacheFingerprint'li URL'ler, Cache-ControlUzun TTL; rutin purge yok
Ana sayfa, içerik ve landing sayfalarıPage cachecache.applyDefaultCache24 saat
PDP ve ürün tile'larıPage cache, kişiselleştirilmişcache.applyPromotionSensitiveCache24 saat ve price_promotion
Arama ve kategori sonuçlarıPage cache, kişiselleştirilmişcache.applyShortPromotionSensitiveCache1 saat
Stoğa duyarlı parçalarPage cachecache.applyInventorySensitiveCache veya kademeli setExpires()30 dakika veya stoğa göre
Oturum durumu, mini sepet, hesapÖnbelleksiz remote includeserver.middleware.include, cache middleware yokÖnbelleğe alınmaz
Konfigürasyon, dış API sonuçlarıCustom cacheCacheMgr.get(key, loader)expireAfterSeconds = kabul edilebilir bayatlık
Headless ürün ve arama verisiSCAPI web katmanıPage caching açık; kişiselleştirme için Shopper ContextSCAPI web katmanı önbellek dokümanına göre

Önemli çıkarımlar

  • Page cache anahtarı tam URL'dir: URL'leri, özellikle include URL'lerini, çıktıyı değiştirmeyen parametrelerden arındırın.
  • <iscache> yerine SFRA middleware'ini veya response.setExpires()'ı tercih edin; bir yanıttaki en düşük süre kazanır.
  • Sayfa çerçevesini önbelleğe alın; alışverişçiye özel parçaları önbelleksiz remote include'larda tutun.
  • price_promotion price book, promosyon, sıralama kuralı ve A/B segmentini kapsar; başka hiçbir şeyi değil.
  • Custom cache'ler sunucu başınadır; toplamda yaklaşık 20 MB, kayıt başına 128 KB sınırı vardır ve kalıcılık garantisi yoktur.
  • Partition bazında geçersiz kılın, Clear Period'un bitmesini bekleyin ve her canlıya çıkıştan sonra hit oranlarını kontrol edin.

Yoğun satış döneminden önce bir storefront'un önbellek stratejisine ikinci bir göz isterseniz hizmet sayfasında nasıl çalıştığıma bakabilir ya da benimle iletişime geçebilirsiniz.

Ek kaynaklar