Kendi Adımı Arattım, Çıkmadım — App Store'da Bulunabilir Olmak
Onay maili geldiğinde işin bittiğini sanıyordum. Telefonu açıp "tessera" arattım — çıkmadı. O an anladım: onaylanmış olmak ile bulunabilir olmak iki ayrı iş. Bu yazı, altı ay boyunca hassasiyetle inşa ettiğim bir ürünün mağaza sayfasını tahmine bırakmış olmamın hikâyesi — ve onu veriyle yeniden kurarken öğrendiklerim.

Onaylandı. Yayında. Görünmez.
Tessera 20 Ağustos'ta App Store'da yayına girdi. Onay maili geldi, üç ürün de APPROVED, sürüm READY_FOR_SALE. Geriye tek bir şey kalmıştı: telefonu açıp kendi uygulamamı aratmak.
"tessera" yazdım. Çıkmadı.
Yirmi iki sonuç döndü: bir pasaport fotoğrafı uygulaması, bir hamburger zinciri, bir Latin ayin kartı uygulaması. Benimki yoktu. Sonradan bunun yeni uygulamalara özgü indeks gecikmesi olduğunu gördüm — birkaç gün sonra düzeldi. Ama o an bir şeyi netleştirdi: onaylanmış olmak ile bulunabilir olmak iki ayrı iştir. Birincisini bitirmiştim. İkincisine hiç başlamamıştım.
Üstüne: sıfır puan, sıfır yorum.
Aramanın iki fazı
App Store araması iki aşamada çalışıyor ve bu ayrım her şeyi belirliyor.
Faz 1 — indeksleme. Apple, bir sorgu için hangi uygulamaların aday olduğunu metin alanlarından çıkarır: başlık, alt başlık, anahtar kelime alanı. Bu alanlarda geçmeyen bir kelime için hiç yarışmazsın.
Faz 2 — sıralama. Adaylar arasında sırayı indirme hacmi ve puanlar belirler.
Yeni bir uygulamanın faz 2'de yapabileceği hiçbir şey yok. Elimde sıfır indirme, sıfır puan vardı; "habit tracker" gibi kategori tanımlayan bir kelimede 91 bin puanlı Productive'i geçme ihtimalim yoktu. Ama faz 1 tamamen benim kontrolümde: hangi sorgular için aday olduğuma ben karar veriyorum. Ve o güne kadar bu kararı tahminle vermiştim.
Tahmin etmeyi bırakmak
Metadata'yı ilk yazarken kelimeleri "kulağa doğru geldiği için" seçmiştim. Bu sefer üç ayrı araştırma kolunu paralel çalıştırdım: rakip taraması, İngilizce anahtar kelime + algoritma mekaniği, Türkiye pazarı. Hepsi canlı veriye dayandı — iTunes Search API herkese açık ve ücretsiz:
curl -s "https://itunes.apple.com/search?term=habit+tracker&country=us&entity=software&limit=15"
Üç bulgu işi değiştirdi.
"Tessera" markası sahipsiz. Yirmi iki sonucun içinde tek bir alışkanlık uygulaması yok; "Tessera" adını taşıyanların hepsi 0-23 puan aralığında. Marka terimimde kimseyle yarışmıyorum.
Mozaik rakiplerinin tamamı ölü. "Habit Mosaic", "Mosaic: Habit Tracker", "HabitPuzzle Mosaic"… hepsi 0 puanlı. Daha ilginci: hepsi kaçırılan günü "boş" ya da "kırık" olarak çerçeveliyor. Tessera'nın kintsugi açısı — kaçırılan günün silinmek yerine altınla onarılması — kategoride tümüyle sahipsiz. En yakın komşusu 6 bin puanlı (Not Boring) Habits ve o bile bunu yalnızca açıklama metninde söylüyor, başlıkta değil.
Başlık formülünü kaçırmışım. HabitKit'in geliştiricisi ASO stratejisini kamuya açık yazmış: indirmelerinin ~%98'i mağaza aramasından geliyor ve formül, "habit tracker" ikilisini başlığa koymak. Benim başlığımda "Habit" vardı, "Tracker" yoktu. En ağır alanda kategori kelimesinin yarısını taşıyordum.
Üç alan, tek sorgu

Asıl kavramsal sıçrama şu: alanlar ayrı ayrı indekslenmiyor, birbirleriyle birleşiyor.
Başlıkta "alışkanlık", alt başlıkta "takibi" geçiyorsa "alışkanlık takibi" sorgusu için indekslenirsin — o ifadeyi hiçbir yere tam olarak yazmasan bile. Bu, 30 + 30 + 100 karakterlik dar bütçeyi bambaşka bir şeye çeviriyor: kelimeleri değil, kombinasyonları planlıyorsun.
Buradan çıkan pratik kurallar:
- Anahtar kelime alanına ifade değil tek kelime yaz. "habit tracker" yazmak karakter israfı; "habit" ve "tracker" ayrı ayrı yazılınca ikisi zaten birleşiyor.
- 100 karakter, virgülle ve boşluksuz:
takip,hedef,disiplin. - Tekil yaz. Apple çoğulu kendisi kapsıyor.
- Başlıkta veya alt başlıkta geçen bir kelimeyi anahtar kelime alanında tekrarlama. Sıfır kazanç, saf israf.
Bu son kural benim eski metadata'mı doğrudan vuruyordu. Türkçe alt başlığım "Sanata dönüşen alışkanlık" idi. Başlıkta zaten "Alışkanlık" vardı — yani 30 karakterlik en değerli ikinci alanım, bana yalnızca "sanat" kelimesini kazandırıyordu. Yenisi "Günlük seri ve rutin takibi" oldu: dört yeni kelime, ve ana sorgu olan "alışkanlık takibi" artık başlık + alt başlık birleşiminden kuruluyor.
Vitrinlerin gizli dil haritası

Araştırmanın en şaşırtıcı kısmı buydu. Bir App Store vitrini yalnızca kendi dilini indekslemiyor — ikinci bir dili daha okuyor.
ABD vitrini en-US'in yanında es-MX'i de indeksliyor. Yani Meksika İspanyolcası lokalizasyonunun anahtar kelime alanı, ABD'de fazladan 100 karakter demek. Apple bu alanın dilini doğrulamıyor; oraya farklı (tekrar etmeyen) kelimeler koyarsan ABD aramasında da sayılıyorlar.
Türkiye vitrini ise tr ile birlikte en-GB okuyor — en-US değil. Bu benim için doğrudan bir hataydı: Türk vitrininde "habit tracker" arayan birine görünmek istiyorsam, İngilizce kelimelerimin en-US alanında olması hiçbir işe yaramıyor. en-GB lokalizasyonu açmadan o trafiğe erişmem mümkün değildi.
Bir de Türkçeye özgü bir tuzak var: App Store araması Türkçe aksanları normalize etmiyor. "alışkanlık takip" ile "aliskanlik takip" farklı sonuç kümeleri döndürüyor. İkisini de kapsamak gerekiyor, ama başlıkta ikisini birden yazamazsın — o yüzden aksanlı hâl başlıkta, aksansız aliskanlik anahtar kelime alanında duruyor.
Paketi kurmak ve doğrulamak
Sonuçta dört dilli bir paket çıktı: en-US, tr güncellendi; en-GB (ABD dışındaki İngilizce vitrinler ve Türkiye için) ve es-MX (hem gerçek bir İspanyolca liste hem ABD'ye ek karakter) sıfırdan açıldı.
| Alan | Önce | Sonra |
|---|---|---|
| en-US başlık | Tessera: Habit Mosaic | Tessera: Habit Mosaic Tracker |
| en-US alt başlık | Habit tracker that makes art | Daily streaks that become art |
| tr alt başlık | Sanata dönüşen alışkanlık | Günlük seri ve rutin takibi |
| Lokal sayısı | 2 | 4 |
Ama bu kadar alanı elle yönetirken bir kelimenin iki yerde tekrar etmediğinden nasıl emin olacaksın? Olamazsın — o yüzden bir script yazdım. Kritik kontrol, aynı vitrinde birlikte indekslenen setler arasındaki kesişim:
PAIRS = [("US", "en-US", "es-MX"), ("MX", "es-MX", "en-GB"), ("TR", "tr", "en-GB")]
for store, a, b in PAIRS:
dup = keywords(a) & keywords(b)
print(f"{store}: {a} ∩ {b} = {dup if dup else 'clean'}")
Aynı script karakter limitlerini ve her lokalin kendi içindeki başlık/alt başlık tekrarını da kontrol ediyor. Çıktı tek satırda okunuyor: ALL OK. Metadata'yı fastlane ile dosya olarak yönettiğim için (bunu daha önce yazmıştım) bu doğrulama commit'in bir parçası, mağaza panelinde göz kontrolü değil.
Metadata bitince asıl iş başlıyor
Anahtar kelimeler faz 1'i çözüyor. Ama sayfaya gelen insanın indirmesi ayrı bir iş ve orada iki eksiğim vardı.
Ekran görüntülerinde tek kelime yoktu

Mağazadaki altı karem çıplak simülatör görüntüsüydü. Oysa ziyaretçilerin yaklaşık %70'i ilk iki kareden öteye kaymıyor ve başlıklı görseller dönüşümü %20-35 artırıyor.
Kareleri yeniden dizdim — artık ilki eserin kendisi ("Alışkanlıkların sanata dönüşür"), ikincisi altın onarım ("Kaçırılan günler altınla onarılır") — ve her birinin üstüne uygulamanın kendi yazı tipiyle bir cümle koydum. Bunu elle Figma'da yapmak yerine pipeline'a bağladım: capture.sh ham kareleri yazıyor, compose.py başlıkları basıp mağaza setini üretiyor. Bir sonraki sürümde ekran değişirse tek komutla yeniden üretiliyor.
Uygulama hiç puan istemiyordu
Sıfır puanla faz 2'de hiçbir şey olamazsın ve Tessera kullanıcıdan hiç puan istemiyordu. Ama bu özelliği eklerken asıl soru "nasıl" değil, "ne zaman" idi.
Tessera'da bir tören var: bir eser tamamlandığında mozaik kendini yeniden dokuyor ve adını alıyor. Kullanıcının uygulamadan en memnun olduğu an orası. Puan istemi oraya bağlandı.
Bir sorun vardı: aynı ana paywall teklifi de bağlıydı. İki sheet üst üste açılırsa ikisi de değersizleşir. Çözüm, teklifin kendi sonucunu bildirmesi oldu:
final offered = await getIt<PaywallPresenter>().offer(context, request);
// Puan istemi aynı anı ancak teklif sessiz kaldıysa alır — bir anda tek sheet.
if (!offered && mounted) {
await getIt<ReviewPromptService>().onCeremonyShown();
}
Servisin kendisi de üç kurala uyuyor: kurulum başına tek istek, henüz eser bitirmemiş kullanıcılar için yedek tetik (7. tamamlama), ve muhafızın istekten önce yazılması — istem sırasında bir çökme olursa uygulama kendine ikinci bir hak satın almasın diye.
Future<void> _maybeAsk() async {
if (await _settings.read(AppStateKeys.reviewAskedOn) != null) return;
try {
if (!await _review.isAvailable()) return;
await _settings.write(AppStateKeys.reviewAskedOn, formatOffsetTimestamp(_clock.now()));
await _review.requestReview();
} on Exception {
// Puan istemi asla hataya dönüşmemeli.
}
}
Widget'tan yapılan tamamlamalar bu sayacı bilerek beslemiyor: sheet ancak uygulama önplandayken çizilebilir, o yüzden yalnızca uygulama içi anlar soruyor.
Neyin ne zaman değişebildiği
Pratik bir ayrım, çünkü planı bu belirliyor:
| Alan | Ne gerekiyor |
|---|---|
| Promotional text | Hiçbir şey — canlı sürümde anında değişir |
| Başlık, alt başlık, anahtar kelimeler | Yeni sürüm ve inceleme |
| Ekran görüntüleri, açıklama | Yeni sürüm |
Yani ASO değişikliği bir metadata düzenlemesi değil, bir sürüm sevkiyatı. Zaten yeni bir build gerekiyorsa, puan isteme akışı gibi kod işlerini de aynı sürüme koymak mantıklı — ben öyle yaptım. 1.0.1 (build 5) tek pakette gitti: dört dilli metadata, başlıklı görseller, puan istemi.
Geriye kalan
Sonuçları henüz bilmiyorum. Sürüm incelemede, sıralamalar birkaç hafta içinde oturacak ve bu yazının "işe yaradı" diyen bir devamı olmayabilir. Ama süreçten çıkardığım şey zaten sonuçtan bağımsızdı.
Ürünü altı ay boyunca hassasiyetle inşa ettim: mozaik geometrisinin sabitleri, altın dikişin nereye düştüğü, hangi anın satış anı olmadığı. Sonra o ürünü, kulağa hoş gelen on iki kelimeyle mağazaya bıraktım. Uygulamanın içindeki her karar veriye ve niyete dayanıyordu; uygulamanın bulunma yolu ise tahmine.
Mağaza sayfası ürünün dışında bir pazarlama yüzeyi değil — kullanıcının gördüğü ilk ekranı. Ona da geri kalanı kadar mühendislik borçlusun.