←Yunus Emre Alpak

Kendi Adımı Arattım, Çıkmadım — App Store'da Bulunabilir Olmak

2026 · 08 · 2210 dk okuma

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.

Aynı raftaki gri uygulama ikonları arasında mozaik desenli tek bir ikonu bulan büyüteç

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

Başlık, alt başlık ve anahtar kelime alanının ağırlıkları ve birleşerek sorgu kurması

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:

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ı

Hangi App Store vitrininin hangi ikinci dili indekslediğini gösteren harita

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ÖnceSonra
en-US başlıkTessera: Habit MosaicTessera: Habit Mosaic Tracker
en-US alt başlıkHabit tracker that makes artDaily streaks that become art
tr alt başlıkSanata dönüşen alışkanlıkGünlük seri ve rutin takibi
Lokal sayısı24

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

Ham simülatör karesi ile başlıklı mağaza karesinin karşılaştırması

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:

AlanNe gerekiyor
Promotional textHiçbir şey — canlı sürümde anında değişir
Başlık, alt başlık, anahtar kelimelerYeni sürüm ve inceleme
Ekran görüntüleri, açıklamaYeni 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.