Ayarları koda çevir, mağazada doğrudan çalıştır

Pazarlamacının seçtiği ürün, renk ve başlığı tek kod dosyasına çeviren araç: gerçek kodla canlı önizleme ve mevcut kayıt sistemiyle entegrasyon.

Solda tek bir mağaza scriptine derlenen React ayar paneli, sağda canlı fırsat widget'ı önizlemesi

İstek basitti: bir pazarlamacı, Deal of the Day widget'ının (sayfaya eklenen küçük işlevsel bileşen) ürünlerini, başlığını ve renklerini tek satır kod yazmadan değiştirebilmeli.

Widget'ın kendisi ise mağaza sitesinde çalışan bir script (belirli bir işi yapan kod dosyası). Ocak 2025'te Revotas panelinin içinde Deal of the Day V2'ye bu yüzden başladım: en fazla 4 ürün seçtiğin, başlığı, renkleri ve butonu ayarladığın, kaydetmeden önce sonucu gördüğün bir React builder (kod yazmadan öğe oluşturmayı sağlayan düzenleme aracı).

Neyin etrafından dolaşmam gerekti

İlk kısıt mağazalar. Marka sitesine tek bir script etiketi yapıştırıyor, o kadar. Onların tarafında build (kaynak kodu çalıştırılabilir veya yayımlanabilir çıktıya dönüştürme) adımı yok, bundler (kod dosyalarını tarayıcıya gönderilecek paketlerde birleştiren araç) yok. Temaları hakkında "bir <body> var" dışında hiçbir şey varsayamıyorum.

İkincisi panel. Git geçmişinde yirmiyi aşkın isim olan, büyük ve ortak bir React kod tabanı. Countdown, spin wheel, social proof, sticky bar, upsell, exit intent gibi başka smart widget'lar da aynı yerde yaşıyor. Deal of the Day'in 1. sürümü hâlâ canlıydı ve bozulmamalıydı.

Kendime de baştan üç kural koydum:

  • yeni backend (uygulamanın sunucuda çalışan bölümü) endpoint'i (bir serviste istek gönderilen belirli adres) yok,
  • veritabanı değişikliği yok,
  • mevcut script widget kayıt akışı kullanılacak.

Sonuncusu her şeyi belirledi.

Karar 1: ayarları bir scripte derle

Akla ilk gelen tasarım genel bir runtime (kodun çalıştığı ortam). Her mağazaya tek bir loader gönderirsin, ayarları JSON (veriyi alan ve değer çiftleriyle taşıyan metin biçimi) olarak kaydedersin, loader sayfa açılınca ayarları çekip yorumlar.

O JSON'u koyacak yerim yoktu. Mevcut kayıt akışı her script widget için tek bir şey saklıyor: scriptCode adında bir metin alanı. Ayarlar için yeni bir kolon şema (verinin hangi alanları ve türleri içereceğini belirleyen yapı) değişikliği demekti. Ayarları okuyan bir loader da yeni bir endpoint.

Ben de builder'ı derleyici yaptım. React state'i değiştiğinde yaklaşık 160 satırlık bir builder fonksiyonu üç iş yapıyor:

  1. panel state'ini bir runtime config'e (uygulamanın davranışını belirleyen ayarlar) çeviriyor (deal'lar, ürünler, kaynaklar, tema, metinler),
  2. config'i JSON olarak serialize ediyor,
  3. bunu vanilla widget fonksiyonunun önüne yapıştırıp fonksiyonu çağırıyor.

Çıktı kabaca şöyle:

js
/*__REVOTAS_DOTD_V2_CONFIG_START__*/
window.__REVOTAS_DOTD_V2_CONFIG__ = {"containerTitle":"Deal Products", ...};
/*__REVOTAS_DOTD_V2_CONFIG_END__*/
(function dealOfTheDayNewDesign(config) { /* ~1.800 satır vanilla JS */ })(window.__REVOTAS_DOTD_V2_CONFIG__);

Bu metnin tamamı scriptCode. Mağaza, config'i içine gömülü, kendi kendine yeten tek bir script çalıştırıyor. Ayarları çekmek için ikinci bir istek atılmıyor, sunucuda da yeni bir şey yok.

Yorum işaretleri önemli. Pazarlamacı kayıtlı bir widget'ı tekrar açtığında panel bu iki işaretin arasındaki metni bulup JSON'u parse ediyor ve builder state'ini ondan yeniden kuruyor. Biri scripti elle düzenlemiş ve işaretler kaybolmuşsa builder ham script editörüne düşüyor. Böylece hiçbir şey sessizce ezilmiyor.

Builder, pazarlamacının ayarlarını tek bir script’e derliyor. Mağaza o script’i çalıştırıyor; sayfa yüklenirken çekilip yorumlanan bir şey yok.

Karar 2: gerçek kodu önizle

İlk önizleme fikrim formun yanında widget'ın React ile yapılmış bir taklidiydi. Bir hafta içinde gerçeğinden koparırdı. İki ayrı renderer, iki ayrı layout (öğelerin ekrandaki yerleşimi) bug'ı (yazılım hatası) listesi demek.

Bu yüzden önizleme derlenmiş scriptin ta kendisini çalıştırıyor. Statik bir srcDoc ile açılan bir iframe (sayfanın içinde ayrı bir sayfa çalıştıran çerçeve) var; bir önizleme bayrağı koyuyor ve tek bir mesaj tipini dinliyor. Üretilen script her değiştiğinde panel onu içeri gönderiyor:

js
iframe.contentWindow.postMessage({ type: 'REVOTAS_DOTD_V2_SCRIPT', script, isMobile }, '*');

iframe scripti çalıştırıyor ve hiç yeniden yüklenmiyor. Yani başlık alanına yazarken widget açık kalıyor. Pazarlamacının beklediği de bu.

Burada görmem gereken bir şey kırıldı. Vanilla script sayfa başına bir kez çalışacak şekilde yazılmıştı. Önizlemede ise her tuş vuruşunda çalışıyor. Launcher tıklamayı addEventListener ile bağlıyordu; birkaç düzenlemeden sonra tek bir tık birden fazla handler tetikliyor, panel aynı tıkta açılıp kapanıyordu. Çözüm sıkıcıydı: tekrar çalışınca handler'ın yerine geçsin diye onclick ile bağlamak, global listener'ları (bir olayı bekleyip gerçekleşince kod çalıştıran dinleyici) bir versiyon bayrağıyla korumak, mevcut DOM (tarayıcının sayfadaki öğeleri temsil ettiği yapı) node'larını yeniden kullanmak. Bu bug'ı, aynı kodu önizlemede çalıştırmak buldu, hiçbir mağaza görmeden önce.

1, 2, 3 ve 4 ürünlük layout'lar da vanilla scriptin içinde. Önizlemede ne görünüyorsa mağazaya giden de o.

Karar 3: serialize ederken temkinli ol

Config, bir metin kolonunun içindeki bir JavaScript dosyasının içindeki JSON. Her katmanın bir string'i (metin değeri) bozmak için kendi yolu var.

Türkçe başlıklar kaydedip tekrar açınca bozuk gelmeye başladı. Klasik mojibake: UTF-8 byte'lar (bilgisayarda sekiz bitlik veri birimi) Windows-1252 olarak okunmuş. Panel ile depolama arasındaki her katmanı ben kontrol etmiyordum, o yüzden iki uçtan da düzelttim:

  • serializer < karakterini ve ASCII dışındaki her karakteri \uXXXX olarak escape ediyor. Kayıtlı script tamamen ASCII kalıyor, başlıktaki başıboş bir </script> da etiketi kapatamıyor,
  • tekrar açılışta bir onarım fonksiyonu her parçayı geziyor, CP1252 karakterlerini byte'a geri çeviriyor ve en fazla 3 turda yeniden decode (modelin yanıtı parça parça üretmesi) ediyor.

Builder testleri bu gidiş dönüşü kapsıyor: script üret, geri parse et, state'i karşılaştır.

Karar 4: veriyi mağaza çeksin, yedek kaynaklarla

Manuel mod basit. Ürünler config'in içinde.

Otomatik modda bir kaynak seçiliyor (AI öneri, çok satanlar, yeni gelenler, trend olanlar, fiyatı düşenler) ve script çalışma anında platformun mevcut öneri servisine soruyor. İlk kaynak boş dönerse, pazarlamacının builder'da gördüğü yedek sıradaki bir sonrakini deniyor. Yine yeni endpoint yok; widget platformda zaten olanı çağırıyor.

Küçük bir sürpriz: ürün listesi cevabı text/html header'ıyla ama JSON gövdeyle geliyor. Parser (yanıtı beklenen veri yapısına dönüştüren ayrıştırıcı) bu yüzden önce JSON deniyor, olmazsa XML'e düşüyor.

Karar 5: ortak koda olabildiğince az dokun

V2 kendi klasöründe. Dışarıda sadece mecbur olduklarımı değiştirdim: widget listeleri, içerik sekmesi bağlantısı, üst bardaki kayıt yolu, önizleme alanı ve ürün seçici kendi class'ını alabilsin diye ortak dialog bileşenine bir prop.

Bu dosyalar diğer widget'larla ortak. Her değişiklik işe yarayan en küçük değişiklikti ve V1'e hiç dokunulmadı. Test sunucularının da kendi kuralı vardı: hedef makinede npm komutu çalışmaz. Sadece dokunduğum dosyaları senkronladım, önce uzaktaki kopyaların yedeğini aldım ve her kopyadan sonra SHA-256 hash'lerini karşılaştırdım.

Neyi farklı yapardım

Runtime'ı her kayıtlı scriptin içine gömmenin, hafife aldığım bir bedeli var. Her widget kodun kendi kopyasını taşıyor. Runtime'da bir bug düzelttiğimde mevcut widget'lar, biri onları tekrar kaydedene kadar eski kopyayla kalıyor.

Bu Ağustos 2026'da canımı yaktı. Widget'ın görüntülenme ve tıklama olayları aktivite takibine ulaşmıyordu, çünkü script mağaza runtime'ının hiç atamadığı bir popup id'si bekliyordu. Tıklama olayı yoksa attribution (tıklama veya satışın hangi kampanyadan geldiğini belirleme) cookie'si (tarayıcıda saklanan küçük kayıt, yani çerez) de yok, widget da hiç gelir raporlamıyor. Widget'ın kendi id'sini kayıt anında config'e gömerek düzelttim. Ama her mevcut widget'ın bunu alması için bir kez yeniden kaydedilmesi gerekti.

Bir dahaki sefere ayırırdım: config her widget için derlenmiş kalsın, runtime tek ve versiyonlu bir dosyadan yüklensin. Ayarları yine derlerdim. 1.800 satırlık kodu her widget kaydına kopyalamayı bırakırdım.

Önizleme sözleşmesini de (script iki kez çalışırsa ne olur) en başta yazardım. Üst üste binen listener'ları tek tek keşfetmek yerine.

Sonuç

Pazarlamacılar widget'ı kendileri kuruyor, düzenlerken gerçeğini görüyor ve diğer script widget'larla aynı akıştan kaydediyor. Veritabanı hiçbir şey fark etmedi.

Smart widget işinin geri kalanı Revotas sayfasında. Etkileşimli demoyu deneyin.

Bu yazı İngilizce aslından Türkçeye çevrilmiştir.

Çözmeye değer bir problemin mi var?Birlikte çözelim

Konuşmaya başla

Full-stack ve uygulamalı AI rollerine açığım, uzaktan veya hibrit.

Rolü, ekibi ve problemi anlat.