History API, tarayıcının o sekmede tuttuğu oturum geçmişini JavaScript ile okumanızı ve değiştirmenizi sağlar. Tek sayfalı bir uygulamada (SPA) sayfayı yeniden yüklemeden içeriği değiştirdiğinizde adres çubuğunu güncellemek için history.pushState(), mevcut kaydı düzeltmek için history.replaceState(), kullanıcı Geri veya İleri düğmesine bastığında doğru görünümü geri getirmek için de window üzerindeki popstate olayını kullanırsınız.

Bu yazıda back(), forward() ve go() ile gezinmeyi, pushState() ile yeni kayıt eklemeyi, popstate olayının tam olarak ne zaman tetiklendiğini, replaceState() ile ilk sayfanın durumunu kaydetmeyi, scrollRestoration ayarını ve küçük bir SPA yönlendirme örneğini MDN belgelerine dayanarak anlatıyoruz. Sonunda daha yeni olan Navigation API ile kısa bir karşılaştırma ve sık yapılan hatalar var. Serinin geri kalanı için Web API öğrenme rehberi sayfasına bakabilirsiniz.

Oturum geçmişi ve History arayüzü

Oturum geçmişi ve History arayüzü: length, state, scrollRestoration, back()

Kullanıcı bir sekmede bağlantılara tıklayıp yeni sayfalar açtıkça tarayıcı bu adresleri sırayla bir listeye ekler. Bu listeye oturum geçmişi denir ve tarayıcının Geri ile İleri düğmeleri bu liste üzerinde dolaşır. JavaScript tarafında bu listeye erişim, global history nesnesi üzerinden olur. MDN bu nesneyi tekil (singleton) olarak tanımlar: sayfada yalnızca bir tane History örneği vardır ve kendiniz yeni bir tane oluşturmazsınız.

Önemli bir sınır şudur: History arayüzü yalnızca ana iş parçacığında, yani Window bağlamında vardır. Web Worker ya da Worklet içinde history nesnesine erişemezsiniz. Arka planda çalışan bir worker yönlendirme kararını hesaplayabilir, ama adresi değiştirmek için sonucu ana sayfaya göndermesi gerekir; bunun yolu Web Workers ve postMessage yazısında anlatılıyor.

ÜyeTürüNe yapar
lengthsalt okunur özellikOturum geçmişindeki kayıt sayısı, o anki sayfa dahil. Yeni sekmede 1 döner.
statesalt okunur özellikGeçerli kaydın durum nesnesi. pushState veya replaceState kullanılana kadar null.
scrollRestorationözellikGeçmişte gezinirken kaydırma konumunun geri getirilip getirilmeyeceği: auto veya manual.
back()metotBir önceki kayda gider, go(-1) ile aynıdır.
forward()metotBir sonraki kayda gider, go(1) ile aynıdır.
go(delta)metotGöreli konuma gider. Parametresiz veya 0 ile sayfayı yeniden yükler.
pushState(state, unused, url)metotGeçmişe yeni bir kayıt ekler, sayfayı yüklemez.
replaceState(state, unused, url)metotGeçerli kaydın durumunu ve adresini değiştirir.

back, forward ve go ile gezinme

İlk metot grubu, kullanıcının yapabileceği gezinmeyi kod ile tetikler. history.back() tarayıcının Geri düğmesine basmakla aynı sonucu verir, history.forward() İleri düğmesine basmakla. history.go() ise o anki sayfaya göre göreli bir konum alır: negatif değer geriye, pozitif değer ileriye gider. Geçerli sayfanın göreli konumu 0 kabul edilir, bu yüzden go(0) veya parametresiz go() çağrısı sayfayı yeniden yükler ve MDN bunun location.reload() ile aynı etkiyi verdiğini belirtir.

javascript
// Bir önceki sayfaya dön (go(-1) ile aynı)
history.back();

// İki sayfa ileri git
history.go(2);

// Geçmişteki kayıt sayısı
const kayitSayisi = history.length;

Bu metotlar eşzamansızdır. Çağrı döndüğünde gezinme henüz tamamlanmamış olabilir; MDN, gezinmenin bittiğini anlamak için popstate olayını dinlemenizi önerir. Sınır dışına çıkan çağrılar da sessizce yok sayılır: geçmişte önceki sayfa yokken back() çağırmak hata fırlatmaz, sadece hiçbir şey olmaz. Bu davranış, "geri dönülebiliyor mu?" sorusunu metot sonucuna bakarak cevaplayamayacağınız anlamına gelir.

Bir uç durum daha var: belge tam etkin değilse (örneğin artık görüntülenmeyen bir belgeden çağrılırsa) bu metotlar SecurityError türünde bir DOMException fırlatır. MDN ayrıca tarayıcıların çok sık yapılan gezinme çağrılarını kısıtlayabildiğini; bu durumda hata fırlatabileceklerini, uyarı verebileceklerini ya da çağrıyı yok sayabileceklerini söyler. Hata adlarının ayrıntısı için DOMException ve güvenli bağlam hataları yazısına bakın.

pushState() ile yeni geçmiş kaydı eklemek

pushState() ile yeni geçmiş kaydı eklemek: state, unused, (isteğe bağlı): Yeni kaydın adresi. Göreli verilirse geçerli adrese göre çözülür. Verilmezse belgenin mevcut...

pushState() oturum geçmişine yeni bir kayıt ekler ama sayfayı yüklemez. Üç parametresi vardır ve MDN her birini ayrıntılı açıklar:

  • state: Yeni kayda bağlanan durum nesnesi. Serileştirilebilen herhangi bir değer olabilir. Kullanıcı bu kayda geri döndüğünde popstate olayının state özelliğinde bu nesnenin bir kopyası gelir.
  • unused: Tarihsel nedenlerle duran ve atlanamayan parametre. Eskiden başlık için düşünülmüştü; MDN boş dize vermenin gelecekteki değişikliklere karşı güvenli olduğunu söyler.
  • url (isteğe bağlı): Yeni kaydın adresi. Göreli verilirse geçerli adrese göre çözülür. Verilmezse belgenin mevcut adresi kullanılır. Adres, geçerli sayfayla aynı kökenden olmak zorundadır; değilse metot hata fırlatır.

Burada çok karıştırılan nokta şudur: tarayıcı pushState() çağrısından sonra o adresi yüklemeye çalışmaz. Sadece adres çubuğu değişir ve geçmişe bir satır eklenir. Ancak MDN, tarayıcının bu adresi daha sonra, örneğin tarayıcı yeniden başlatıldığında yüklemeyi deneyebileceğini de belirtir. Bu yüzden ürettiğiniz her adresin sunucu tarafında da gerçekten açılabilir olması gerekir; yoksa kullanıcı sayfayı yenilediğinde veya bağlantıyı paylaştığında boş bir hata sayfasıyla karşılaşır. Aynı adres, sayfanın sonraki isteklerinde Referer başlığının değeri olarak da kullanılır.

Yeni adresi elle birleştirmek yerine URL nesnesiyle kurmak hem okunaklı hem de hatasızdır. MDN de sorgu parametresi değiştirme örneğinde bu yolu gösterir. Aşağıdaki kod, tarayıcıdan bağımsız URL API'sinin davranışını gösteriyor; çıktıları Node.js 24 ile çalıştırarak doğruladık:

javascript
const adres = new URL("https://example.com/urunler?sayfa=1");
adres.searchParams.set("sayfa", "2");
adres.searchParams.set("renk", "mavi");
console.log(adres.href); // https://example.com/urunler?sayfa=2&renk=mavi
console.log(adres.pathname + adres.search); // /urunler?sayfa=2&renk=mavi

// Göreli adresler geçerli adrese göre çözülür
console.log(new URL("?sekme=yorumlar", "https://example.com/urunler/42").href);
// https://example.com/urunler/42?sekme=yorumlar

// Farklı köken: pushState bu adresi kabul etmez
console.log(new URL("https://baska.example/x").origin === new URL("https://example.com/").origin); // false

Tarayıcıda aynı fikir, mevcut adresten yola çıkarak şöyle uygulanır. Bu kısım sayfa gerektirdiği için çalıştırılmadı, MDN'deki örneğin uyarlamasıdır:

javascript
const url = new URL(location);
url.searchParams.set("sayfa", "2");
history.pushState({ sayfa: 2 }, "", url);

Durum nesnesinin sınırları

Durum nesnesi saklanmadan önce yapılandırılmış klonlama (structured clone) ile kopyalanır. Klonlanamayan bir değer, örneğin bir fonksiyon içeren nesne verirseniz pushState() DataCloneError fırlatır. Aynı kuralı tarayıcıdan bağımsız structuredClone() ile kolayca görebilirsiniz:

javascript
console.log(JSON.stringify(structuredClone({ sekme: "yorumlar", sayfa: 2 })));
// {"sekme":"yorumlar","sayfa":2}

try {
  structuredClone({ f: () => 1 });
} catch (hata) {
  console.log(hata.name); // DataCloneError
}

Boyut da bir konudur. MDN, bazı tarayıcıların durum nesnelerini yeniden başlatmadan sonra geri yükleyebilmek için diske yazdığını ve serileştirilmiş boyuta bir üst sınır koyduğunu, bu sınırı aşan nesnelerde hata fırlattığını söyler. Büyük veriyi durum nesnesine koymak yerine küçük bir anahtar saklayıp asıl veriyi sessionStorage ya da localStorage içinde tutmanız önerilir; bu depolama yöntemleri localStorage ve sessionStorage yazısında anlatılıyor.

pushState ile adres parçası (hash) değiştirmenin farkı

window.location = "#bolum" yazmak da aynı belge için yeni bir geçmiş kaydı oluşturur. MDN, pushState() yönteminin üstünlüklerini üç maddede toplar: yeni adres aynı kökendeki herhangi bir yol olabilir, oysa location ile yalnızca parça değiştirdiğinizde aynı belgede kalırsınız; adresi değiştirmek isteğe bağlıdır; ayrıca kayda istediğiniz veriyi bağlayabilirsiniz, parça yönteminde ise bütün bilgiyi kısa bir dizeye sıkıştırmanız gerekir. Bir fark daha vardır: pushState() yeni adres eskisinden yalnızca parça bakımından farklı olsa bile hiçbir zaman hashchange olayı tetiklemez.

popstate olayı: Geri ve İleri düğmelerini yakalamak

popstate, kullanıcı oturum geçmişinde gezinirken etkin kayıt değiştiğinde window üzerinde tetiklenir. Olay nesnesi PopStateEvent türündedir ve state özelliği, etkinleşen kayıt pushState() ile oluşturulduysa veya replaceState() ile değiştirildiyse o kaydın durum nesnesinin bir kopyasını taşır. Diğer kayıtlarda state null olur.

En sık yanlış anlaşılan kural şudur: pushState() veya replaceState() çağırmak popstate tetiklemez. Olay yalnızca Geri ya da İleri düğmesine basmak veya history.back(), history.forward(), history.go() çağırmak gibi bir geçmiş dolaşımında gelir. Yani kendi kodunuzla yeni görünüme geçtiğinizde ekranı kendiniz güncellersiniz; popstate dinleyicisi ise yalnızca geri ve ileri dönüşleri karşılar.

MDN'nin http://example.com/example.html adresinde çalışan örneği bu sırayı çok iyi gösterir. Yorum satırlarındaki çıktılar MDN sayfasında verilen çıktılardır:

javascript
window.addEventListener("popstate", (event) => {
  console.log(
    `location: ${document.location}, state: ${JSON.stringify(event.state)}`,
  );
});
history.pushState({ page: 1 }, "title 1", "?page=1");
history.pushState({ page: 2 }, "title 2", "?page=2");
history.replaceState({ page: 3 }, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null"
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}"

Burada üç şey dikkat çekiyor. Birincisi, ilk üç satır hiçbir çıktı üretmez, çünkü ekleme ve değiştirme işlemleri olay tetiklemez. İkincisi, ?page=2 kaydı replaceState() ile ?page=3 olarak değiştirildiği için geçmişte artık hiç görünmez. Üçüncüsü, başlangıçtaki example.html kaydına hiçbir durum nesnesi bağlanmamış olmasına rağmen ona dönüldüğünde de popstate gelir, sadece state değeri null olur.

popstate ne zaman, hangi sırayla gelir?

MDN olayı gezinme sürecinin neredeyse en sonuna yerleştirir: yeni konum gerekiyorsa yüklendikten ve görünür hale geldikten, pageshow olayından sonra; tarayıcının sakladığı kullanıcı durumu (kaydırma konumu, form değerleri gibi) geri yüklenmeden ve hashchange olayından önce. Dinleyicinizin içinde window.location yeni adresi zaten gösterir, ama MDN document tarafının henüz tam yerine oturmamış olabileceğini not eder. Yeni belge durumunun tamamen hazır olduğu anı yakalamak istiyorsanız işlemi sıfır gecikmeli bir setTimeout() ile olay döngüsünün sonuna ertelemek önerilir. Olay döngüsünün nasıl çalıştığı JavaScript event loop yazısında, pageshow ve sayfa yaşam döngüsü ise DOMContentLoaded, load ve sayfa yaşam döngüsü yazısında anlatılıyor.

Bir kısıt daha var: MDN, istenmeyen açılır pencerelere karşı önlem olarak tarayıcıların sayfayla hiç etkileşim kurulmadıysa popstate olayını hiç göndermeyebileceğini belirtir. Bu yüzden yönlendirme mantığınızı yalnızca bu olaya güvenerek kurmayın; ilk yüklemede adresi okuyup görünümü oluşturan ayrı bir başlangıç fonksiyonu bulundurun.

replaceState() ve history.state

replaceState() yeni kayıt eklemez, geçerli kaydın durum nesnesini ve isteğe bağlı olarak adresini değiştirir. Parametreleri pushState() ile aynıdır; durum nesnesi null olabilir ve adres yine aynı kökenden olmalıdır. Kullanıcı bir filtre seçtiğinde ya da sekme değiştirdiğinde geçmişi şişirmeden adresi güncel tutmak için idealdir.

MDN bunu şu senaryoyla anlatır: https://www.mozilla.org/foo.html sayfası pushState(stateObj, "", "bar.html") çağırır, ardından bar.html adresindeyken replaceState(stateObj, "", "bar2.html") çağrılır. Adres çubuğu bar2.html gösterir, ama tarayıcı bu dosyayı yüklemez, hatta var olup olmadığını bile kontrol etmez. Kullanıcı başka bir siteye gidip Geri düğmesine bastığında bar2.html görür; bir kez daha bastığında doğrudan foo.html adresine döner ve bar.html tamamen atlanır.

SPA'larda replaceState() asıl olarak ilk sayfanın durumunu kaydetmek için gerekir. Sayfa normal yolla yüklendiğinde tarayıcının eklediği kaydın durum nesnesi yoktur. Kullanıcı birkaç uygulama içi bağlantıya tıklayıp başlangıca geri döndüğünde aynı belge içinde kaldığınız için sayfa yeniden yüklenmez, durum da olmadığı için popstate ile neyi geri getireceğinizi bilemezsiniz. Çözüm, sayfa açılır açılmaz başlangıç görünümünü bir nesneye toplayıp replaceState() ile mevcut kayda bağlamaktır.

history.state ise geçerli kaydın durumuna olay beklemeden bakmanızı sağlar. MDN değerin pushState() veya replaceState() kullanılana kadar null olduğunu söyler. Sayfa yenilendiğinde tarayıcının durumu geri yüklediği durumlarda başlangıç fonksiyonunuz bu özelliğe bakarak doğru görünümü kurabilir.

scrollRestoration ile kaydırma konumu

Geçmişte gezinirken tarayıcı varsayılan olarak kullanıcının kaydırdığı konumu geri getirir; bu history.scrollRestoration değerinin "auto" olduğu durumdur. Değeri "manual" yaparsanız konum geri getirilmez ve kaydırmayı siz yönetirsiniz. İçeriği eşzamansız yükleyen SPA'larda, içerik gelmeden yapılan otomatik kaydırma yanlış bir yere düşebileceği için bu ayar işe yarar.

javascript
if (history.scrollRestoration === "auto") {
  // Kaydırmayı içerik yüklendikten sonra kendimiz yapacağız
  history.scrollRestoration = "manual";
}

Çalışan mini örnek: SPA bağlantılarını geçmişe eklemek

MDN'nin rehberindeki örneği Türkçeleştirerek bütün parçaları bir araya getirelim. Sayfadaki bağlantılar data-hayvan özniteliği taşıyor; tıklamada yeni sayfa yüklemek yerine JSON içerik çekiliyor, ekran güncelleniyor ve pushState() ile geçmişe kayıt ekleniyor. Tarayıcı gerektirdiği için çalıştırılmadı; kod MDN örneğinin uyarlamasıdır. Önce HTML:

html
<nav>
  <a href="kartal" data-hayvan="kartal">Kartal</a>
  <a href="yunus" data-hayvan="yunus">Yunus</a>
</nav>
<h1 id="baslik">Ana sayfa</h1>
<p id="aciklama">Bir hayvan seçin.</p>
<img id="foto" src="images/ana.jpg" alt="Hayvan fotoğrafları">
javascript
function icerigiGoster(icerik) {
  document.title = `Hayvanlar: ${icerik.ad}`;
  document.querySelector("#baslik").textContent = icerik.ad;
  document.querySelector("#aciklama").textContent = icerik.aciklama;
  const foto = document.querySelector("#foto");
  foto.setAttribute("src", icerik.gorsel.src);
  foto.setAttribute("alt", icerik.gorsel.alt);
}

// 1) İlk kaydın durumunu kaydet
const foto = document.querySelector("#foto");
history.replaceState(
  {
    ad: "Ana sayfa",
    aciklama: document.querySelector("#aciklama").textContent,
    gorsel: { src: foto.getAttribute("src"), alt: foto.getAttribute("alt") },
  },
  "",
  document.location.href,
);

// 2) Uygulama içi bağlantılar: içerik çek, ekranı güncelle, geçmişe ekle
document.addEventListener("click", async (event) => {
  const hayvan = event.target.getAttribute("data-hayvan");
  if (!hayvan) return;
  event.preventDefault();
  try {
    const yanit = await fetch(`hayvanlar/${hayvan}.json`);
    const sonuc = await yanit.json();
    icerigiGoster(sonuc);
    history.pushState(sonuc, "", hayvan);
  } catch (hata) {
    console.error(hata);
  }
});

// 3) Geri ve İleri: kayıtlı durumu ekrana geri getir
window.addEventListener("popstate", (event) => {
  if (event.state) {
    icerigiGoster(event.state);
  }
});

Akış şöyle işler: kullanıcı önce Kartal, sonra Yunus bağlantısına tıklarsa geçmişe iki kayıt eklenir. Geri düğmesine bastığında tarayıcı Kartal kaydını etkinleştirir, popstate gelir ve event.state içinde Kartal için çekilen JSON'un kopyası bulunur. Bir kez daha Geri dendiğinde, birinci adımda replaceState() ile kaydettiğimiz başlangıç durumu gelir. O adım olmasaydı event.state null olur ve ana sayfa görünümü geri getirilemezdi. Tek bir belge dinleyicisiyle bütün bağlantıları yakalama tekniği olay delegasyonu yazısında, fetch() ve JSON okuma ise Fetch API kullanımı yazısında ayrıntılı anlatılıyor.

History API ile Navigation API karşılaştırması

History API ile Navigation API karşılaştırması: Giriş noktası, Gezinmeyi yakalama, Geri ve ileri bildirimi, Geçmiş listesi

MDN, Navigation API'yi History API ve window.location özelliklerinin eksiklerini gideren, özellikle SPA'lar için tasarlanmış bir halef olarak tanıtır. Giriş noktası window.navigation nesnesidir ve en önemli parçası, her türlü gezinme başladığında tetiklenen navigate olayıdır. Bu olayın intercept() metoduyla gezinmeyi kendi kodunuzla karşılarsınız; böylece bağlantı tıklamalarını tek tek yakalayıp preventDefault() ve pushState() çağırma zahmeti ortadan kalkar. Tarayıcı desteği için MDN uyumluluk tablosuna bakın.

KonuHistory APINavigation API
Giriş noktasıhistory nesnesiwindow.navigation
Gezinmeyi yakalamaTıklamaları elle dinleyip preventDefault ve pushStatenavigate olayı ve intercept()
Geri ve ileri bildirimipopstate (ekleme ve değiştirmede gelmez)navigate, currententrychange, navigatesuccess, navigateerror
Geçmiş listesiYalnızca lengthentries() ve currentEntry, aynı köken kayıtları
Durum okumahistory.stateNavigationHistoryEntry.getState()
Belirli kayda gitmego(delta) ile göreli konumtraverseTo(key) ile anahtar
Sonucu beklemeYok, metotlar sonuç döndürmezcommitted ve finished sözleri (promise)

MDN Navigation API için de bazı sınırlar listeler: ilk sayfa yüklemesinde navigate olayı tetiklenmez, API tek bir çerçeve içinde çalışır ve geçmiş listesini kod ile yeniden düzenleyemezsiniz. Mevcut bir projede History API ile yazılmış yönlendirme kodunuz varsa çalışmaya devam eder; yeni bir yönlendirici yazarken iki API'nin belgelerini birlikte okumak en sağlıklısıdır.

Sık yapılan hatalar

  • pushState() sonrası popstate beklemek. Ekleme ve değiştirme olay üretmez; yeni görünümü çağrıyı yaptığınız yerde kendiniz çizmelisiniz.
  • İlk kaydın durumunu kaydetmemek. replaceState() kullanılmazsa başlangıca dönüşte event.state null gelir.
  • Başka kökene ait adres vermek. Farklı alan adı, protokol veya port içeren adresler pushState() ve replaceState() tarafından reddedilir.
  • Sunucuda karşılığı olmayan adresler üretmek. Tarayıcı bu adresi yenilemede veya yeniden başlatmada yükleyebilir; sunucunuz aynı adrese anlamlı bir yanıt vermelidir.
  • Durum nesnesine fonksiyon veya büyük veri koymak. Klonlanamayan değerler DataCloneError verir, büyük nesneler tarayıcının boyut sınırına takılabilir.
  • Döngü içinde sık sık pushState() çağırmak. Tarayıcılar çok sık yapılan çağrıları kısıtlayabilir ve SecurityError fırlatabilir. Kaydırma veya yazma sırasında adres güncellemek için replaceState() kullanıp çağrıları seyreltin.
  • Worker içinde history aramak. Arayüz yalnızca Window bağlamında vardır.

Sık Sorulan Sorular

pushState çağırınca sayfa neden yenilenmiyor?

Bu beklenen davranıştır. pushState() yalnızca geçmişe kayıt ekler ve adres çubuğunu değiştirir; belirttiğiniz adresi yüklemez, var olup olmadığını da kontrol etmez. İçeriği değiştirmek sizin kodunuzun işidir.

İkinci parametreye ne yazmalıyım?

Boş dize yazın. MDN bu parametrenin tarihsel nedenlerle durduğunu, atlanamadığını ve boş dizenin gelecekteki değişikliklere karşı güvenli olduğunu söyler. Sayfa başlığını değiştirmek istiyorsanız document.title özelliğini kullanın.

popstate olayı ilk yüklemede tetiklenir mi?

Bu yazıda kullandığımız MDN örneklerinde olay yalnızca geçmişte dolaşıldığında gelir. İlk yüklemede görünümü kurmak için adresi ve history.state değerini okuyan bir başlangıç fonksiyonu yazın; olaya güvenmeyin.

hashchange ile popstate arasındaki fark nedir?

hashchange yalnızca adresin parça kısmı değiştiğinde gelir ve pushState() hiçbir zaman bu olayı tetiklemez. popstate ise geçmişte gezinirken etkin kayıt değiştiğinde gelir. Aynı belgede yalnızca parçası farklı iki kayıt arasında dolaşıldığında MDN'deki sıraya göre önce popstate, sonra hashchange gönderilir.

Kullanıcının geri dönebileceği bir sayfa olup olmadığını nasıl anlarım?

History API bunun için doğrudan bir yol sunmaz. history.length toplam kayıt sayısını verir ama geçerli kaydın listedeki yerini söylemez; back() de sınır dışında sessizce hiçbir şey yapmaz. Navigation API'deki entries() ve currentEntry bu konuda daha fazla bilgi verir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · Fetch API kullanımı · Sayfa yaşam döngüsü · localStorage ve sessionStorage · Olay delegasyonu · DOMException ve güvenli bağlam hataları · JSON.parse ve stringify