Skip to content
Merged
56 changes: 41 additions & 15 deletions apps.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
.app-card h3 { font-size:1rem; margin-bottom:.15rem; }
.app-card p { font-size:.82rem; color:var(--sub); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.store-badge { display:inline-block; margin-top:.4rem; font-size:.78rem; font-weight:600; color:var(--accent); }
.app-card-dual .app-card-body { min-width:0; }
.store-links { display:flex; flex-wrap:wrap; gap:.45rem; margin-top:.55rem; }
.store-link { color:var(--accent); text-decoration:underline; font-size:.78rem; font-weight:700; padding:.3rem .2rem; }
.store-link:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

footer { text-align:center; color:var(--sub); font-size:.85rem; padding:2rem; }
footer a { color:var(--accent); }
/* Browse purpose or search without hiding cards from non-JS crawlers. */
Expand Down Expand Up @@ -102,14 +107,17 @@ <h3>SheetDecode: Excel Formula Fix</h3>
<span class="store-badge">Google Play &rarr;</span>
</div>
</a>
<a class="app-card" href="https://play.google.com/store/apps/details?id=jp.appdevelopsk.debate_me&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<div class="app-card app-card-dual">
<img src="icons/apps/debate-me.png" alt="Debate Me - AI Debate Practice icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
<h3>Debate Me - AI Debate Practice</h3>
<p>Practice debating with an AI opponent, get feedback and share your results</p>
<span class="store-badge">Google Play &rarr;</span>
<div class="store-links">
<a class="store-link" href="https://play.google.com/store/apps/details?id=jp.appdevelopsk.debate_me&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" data-store="google_play" target="_blank" rel="noopener">Google Play &rarr;</a>
<a class="store-link" href="https://apps.apple.com/app/id6760968208?ct=apps_catalog_debate-me" data-store="app_store" target="_blank" rel="noopener">App Store &rarr;</a>
</div>
</div>
</a>
</div>
<a class="app-card" href="https://play.google.com/store/apps/details?id=jp.appdevelopsk.roast_me_app&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<img src="icons/apps/roast-me.png" alt="RoastMe - Community Roasts icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
Expand All @@ -126,14 +134,17 @@ <h3>Feels Like Weather &amp; Comfort</h3>
<span class="store-badge">Google Play &rarr;</span>
</div>
</a>
<a class="app-card" href="https://play.google.com/store/apps/details?id=com.dotchi.dotchi_app&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<div class="app-card app-card-dual">
<img src="icons/apps/twotaku.png" alt="Dotchi: This or That Polls icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
<h3>Dotchi: This or That Polls</h3>
<p>Pick A or B on everyday questions. Vote, see results and compare opinions with friends.</p>
<span class="store-badge">Google Play &rarr;</span>
<div class="store-links">
<a class="store-link" href="https://play.google.com/store/apps/details?id=com.dotchi.dotchi_app&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" data-store="google_play" target="_blank" rel="noopener">Google Play &rarr;</a>
<a class="store-link" href="https://apps.apple.com/app/id6760481027?ct=apps_catalog_twotaku" data-store="app_store" target="_blank" rel="noopener">App Store &rarr;</a>
</div>
</div>
</a>
</div>
<a class="app-card" href="https://play.google.com/store/apps/details?id=com.fileforce.fileforce_app&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<img src="icons/apps/fileforce.png" alt="FileForce - PDF &amp; File Manager icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
Expand Down Expand Up @@ -206,14 +217,17 @@ <h3>Trend - Anime &amp; Game Picker</h3>
<span class="store-badge">Google Play &rarr;</span>
</div>
</a>
<a class="app-card" href="https://play.google.com/store/apps/details?id=com.blurapp.blur_app&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<div class="app-card app-card-dual">
<img src="icons/apps/blur.png" alt="Blur: Guess the Image icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
<h3>Blur: Guess the Image</h3>
<p>Guess the photo as it slowly unblurs — a fun image guessing brain game</p>
<span class="store-badge">Google Play &rarr;</span>
<div class="store-links">
<a class="store-link" href="https://play.google.com/store/apps/details?id=com.blurapp.blur_app&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" data-store="google_play" target="_blank" rel="noopener">Google Play &rarr;</a>
<a class="store-link" href="https://apps.apple.com/app/id6759920723?ct=apps_catalog_blur" data-store="app_store" target="_blank" rel="noopener">App Store &rarr;</a>
</div>
</div>
</a>
</div>
<a class="app-card" href="https://play.google.com/store/apps/details?id=com.brainburst.brain_burst&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<img src="icons/apps/brain-burst.png" alt="BrainBurst - Daily Trivia Quiz icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
Expand Down Expand Up @@ -574,14 +588,17 @@ <h3>Travel Safety Map &amp; Scam Alert</h3>
<span class="store-badge">Google Play &rarr;</span>
</div>
</a>
<a class="app-card" href="https://play.google.com/store/apps/details?id=com.instantjudge.instant_judge&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<div class="app-card app-card-dual">
<img src="icons/apps/instant-judge.png" alt="Instant Judge: Vote in 3s icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
<h3>Instant Judge: Vote in 3s</h3>
<p>Vote on community dilemmas, post your own and see results when revealed</p>
<span class="store-badge">Google Play &rarr;</span>
<div class="store-links">
<a class="store-link" href="https://play.google.com/store/apps/details?id=com.instantjudge.instant_judge&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" data-store="google_play" target="_blank" rel="noopener">Google Play &rarr;</a>
<a class="store-link" href="https://apps.apple.com/app/id6759920352?ct=apps_catalog_instant-judge" data-store="app_store" target="_blank" rel="noopener">App Store &rarr;</a>
</div>
</div>
</a>
</div>
<a class="app-card" href="https://play.google.com/store/apps/details?id=com.keyclash.key_clash&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" target="_blank" rel="noopener">
<img src="icons/apps/key-clash.png" alt="KeyClash: Typing Battle icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
Expand Down Expand Up @@ -878,9 +895,18 @@ <h3>Chaos - Stranger Missions</h3>
buttons.forEach(b => b.setAttribute('aria-pressed', String(b === button)));
fire('catalog_filter_used', {goal, results: render()});
}));
cards.forEach(card => card.addEventListener('click', () => {
fire('catalog_store_click', {app: slug(card), goal, source: 'apps_catalog'});
}));
cards.forEach(card => {
const links = card.querySelectorAll?.('a[data-store]') || [];
if (links.length) {
links.forEach(link => link.addEventListener('click', () => {
fire('catalog_store_click', {app: slug(card), goal, source: 'apps_catalog', store: link.dataset.store});
}));
} else {
card.addEventListener('click', () => {
fire('catalog_store_click', {app: slug(card), goal, source: 'apps_catalog', store: 'google_play'});
});
}
});
render();
})();
</script>
Expand Down
14 changes: 9 additions & 5 deletions apps_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"pkg": "jp.appdevelopsk.debate_me",
"title": "Debate Me - AI Debate Practice",
"desc": "Practice debating with an AI opponent, get feedback and share your results",
"installs": 28
"installs": 28,
"ios_app_id": 6760968208
},
{
"app": "roast-me",
Expand All @@ -39,7 +40,8 @@
"pkg": "com.dotchi.dotchi_app",
"title": "Dotchi: This or That Polls",
"desc": "Pick A or B on everyday questions. Vote, see results and compare opinions with friends.",
"installs": 22
"installs": 22,
"ios_app_id": 6760481027
},
{
"app": "fileforce",
Expand Down Expand Up @@ -109,7 +111,8 @@
"pkg": "com.blurapp.blur_app",
"title": "Blur: Guess the Image",
"desc": "Guess the photo as it slowly unblurs — a fun image guessing brain game",
"installs": 15
"installs": 15,
"ios_app_id": 6759920723
},
{
"app": "brain-burst",
Expand Down Expand Up @@ -471,7 +474,8 @@
"pkg": "com.instantjudge.instant_judge",
"title": "Instant Judge: Vote in 3s",
"desc": "Vote on community dilemmas, post your own and see results when revealed",
"installs": 13
"installs": 13,
"ios_app_id": 6759920352
},
{
"app": "key-clash",
Expand Down Expand Up @@ -699,4 +703,4 @@
"desc": "Daily missions from strangers to break your routine and rediscover the world",
"installs": null
}
]
]
20 changes: 18 additions & 2 deletions scripts/build_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@
</a>
"""

# Two independent links only for apps with a verified owner-maintained App Store ID.
# Do not put an anchor inside another anchor; keep crawlers and keyboard users safe.
CARD_DUAL = """ <div class="app-card app-card-dual">
<img src="icons/apps/{app}.png" alt="{title} icon" loading="lazy" width="64" height="64">
<div class="app-card-body">
<h3>{title}</h3>
<p>{desc}</p>
<div class="store-links">
<a class="store-link" href="https://play.google.com/store/apps/details?id={pkg}&amp;utm_source=appdevelopsk&amp;utm_medium=web&amp;utm_campaign=apps_catalog&amp;referrer=utm_source%3Dappdevelopsk%26utm_medium%3Dweb%26utm_campaign%3Dapps_catalog" data-store="google_play" target="_blank" rel="noopener">Google Play &rarr;</a>
<a class="store-link" href="https://apps.apple.com/app/id{ios_app_id}?ct=apps_catalog_{app}" data-store="app_store" target="_blank" rel="noopener">App Store &rarr;</a>
</div>
</div>
</div>
"""

LANGS = ["en", "ja", "zh", "zh_TW", "ko", "es", "pt", "fr", "de", "it", "ru", "ar",
"th", "id", "tr", "nl", "vi"]

Expand Down Expand Up @@ -145,8 +160,9 @@ def load_counts(apps, refresh_total):


def build_apps_html(t, apps, n):
cards = "".join(CARD.format(pkg=a["pkg"], app=a["app"], title=html.escape(a["title"]),
desc=html.escape(a["desc"]))
cards = "".join((CARD_DUAL if a.get("ios_app_id") else CARD).format(
pkg=a["pkg"], app=a["app"], title=html.escape(a["title"]),
desc=html.escape(a["desc"]), ios_app_id=a.get("ios_app_id", ""))
for a in apps if not a.get("hidden"))
t = sub1(r'(<div class="grid">\n).*?( </div>\n </main>)',
lambda m: m.group(1) + cards + m.group(2), t, "apps.html の grid", re.S)
Expand Down
36 changes: 33 additions & 3 deletions tests/catalog-growth.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ test('catalog search retains 92 real indexed store cards and passes install refe
const visible=data.filter(x=>!x.hidden);
const tags=[...catalog.matchAll(/<a class="app-card" href="([^"]+)"/g)];
assert.equal(visible.length,JSON.parse(read('site_counts.json')).android_live);
assert.equal(tags.length,visible.length);
const dual=[...catalog.matchAll(/<div class="app-card app-card-dual">/g)];
assert.equal(tags.length+dual.length,visible.length);
for (const entry of visible) {
assert.ok(tags.some(t=>t[1].includes('id='+entry.pkg)),entry.app);
if (entry.ios_app_id) {
assert.ok(catalog.includes('id='+entry.pkg),entry.app+' Play destination missing');
assert.ok(catalog.includes('apps.apple.com/app/id'+entry.ios_app_id+'?ct=apps_catalog_'+entry.app),entry.app+' App Store destination missing');
} else {
assert.ok(tags.some(t=>t[1].includes('id='+entry.pkg)),entry.app);
}
}
assert.ok(tags.every(t=>t[1].includes('referrer=utm_source%3Dappdevelopsk')));
assert.match(catalog,/id="find-app"/);
Expand Down Expand Up @@ -51,6 +57,28 @@ test('filters work client-side without tracking raw search text',()=>{
assert.ok(events.some(e=>e[1]==='catalog_store_click'&&e[2].app==='blur'));
});

test('dual-store cards log distinct app-store and Play clicks, not a whole-card click',()=>{
const source=[...catalog.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]).find(x=>x.includes('catalog_store_click'));
assert.ok(source);
function el(extra={}){return {events:{},hidden:false,...extra,addEventListener(name,fn){this.events[name]=fn;},setAttribute(key,val){this[key]=val;}}}
const apple=el({dataset:{store:'app_store'}}), play=el({dataset:{store:'google_play'}});
const dual=el({
textContent:'Debate Me - AI Debate Practice',
querySelector:()=>({getAttribute:()=> 'icons/apps/debate-me.png'}),
querySelectorAll:()=>[play,apple],
});
const input=el({value:''}),count=el({textContent:''}),empty=el();
const buttons=['all','quick-games','habits','decisions','ai'].map(goal=>el({dataset:{goal}}));
const events=[];
runInNewContext(source,{document:{getElementById(id){return {'find-app':input,'finder-count':count,'finder-empty':empty}[id];},querySelectorAll(q){return q==='.grid .app-card'?[dual]:buttons;}},window:{gtag(...args){events.push(args);}}});
assert.equal(dual.events.click,undefined,'card body must not masquerade as a store click');
apple.events.click();
play.events.click();
const tracked=events.filter(e=>e[1]==='catalog_store_click');
assert.deepEqual(tracked.map(e=>e[2].store),['app_store','google_play']);
assert.ok(tracked.every(e=>e[2].app==='debate-me'&&e[2].source==='apps_catalog'));
});

test('guide landing pages link only to listed published applications and are discoverable',()=>{
for(const slug of guides){
const page=read('guides/'+slug+'/index.html');
Expand Down Expand Up @@ -101,7 +129,9 @@ test('public Dotchi catalog aligns with its real two-choice voting product', ()
assert.match(app.title, /Dotchi: This or That Polls/);
assert.match(app.desc, /Pick A or B on everyday questions/);
assert.doesNotMatch(app.title + app.desc, /anime|manga|Naruto|Goku/i);
const card = catalog.match(/<a class="app-card" href="[^"]+id=com\.dotchi\.dotchi_app[\s\S]+?<\/a>/)?.[0];
const img = catalog.indexOf('src="icons/apps/twotaku.png"');
const card = img >= 0 ? catalog.slice(catalog.lastIndexOf(' <div class="app-card app-card-dual">',img),img+800) : '';

assert.ok(card, 'missing indexed Dotchi catalog card');
assert.match(card, /Dotchi: This or That Polls/);
assert.match(card, /Vote, see results and compare opinions with friends/);
Expand Down
27 changes: 19 additions & 8 deletions tests/catalog-positioning-regression.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,27 @@ const highPriority = {
};
const encode = value => value.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#x27;');

test('catalog cards reproduce the canonical visible app titles and descriptions', () => {
test('catalog cards reproduce canonical titles with one or two valid store destinations', () => {
const visible = apps.filter(a=>!a.hidden);
const cards = [...page.matchAll(/<a class="app-card" href="([^"]+)"[\s\S]*?<h3>([^<]+)<\/h3>\s*<p>([^<]*)<\/p>[\s\S]*?<\/a>/g)];
assert.equal(cards.length,visible.length);
assert.equal((page.match(/class="app-card"/g)||[]).length+
(page.match(/class="app-card app-card-dual"/g)||[]).length,visible.length);
for (const a of visible) {
const match=cards.filter(m=>m[1].includes('id='+a.pkg));
assert.equal(match.length,1,a.app+' missing or duplicate');
assert.equal(match[0][2],encode(a.title),a.app+' title drift');
assert.equal(match[0][3],encode(a.desc),a.app+' description drift');
assert.match(match[0][1],/referrer=utm_source%3Dappdevelopsk/);
const image='src="icons/apps/'+a.app+'.png"';
const idx=page.indexOf(image);
assert.ok(idx>=0,a.app+' missing catalog icon/card');
assert.equal(page.split(image).length-1,1,a.app+' duplicate catalog card');
const start=page.lastIndexOf(a.ios_app_id ? '<div class="app-card app-card-dual">' : '<a class="app-card" href=',idx);
const end=page.indexOf(a.ios_app_id ? '\n </div>' : '\n </a>',idx);
assert.ok(start>=0&&end>idx,a.app+' card boundary missing');
const card=page.slice(start,end);
assert.ok(card.includes('<h3>'+encode(a.title)+'</h3>'),a.app+' title drift');
assert.ok(card.includes('<p>'+encode(a.desc)+'</p>'),a.app+' description drift');
assert.ok(card.includes('id='+a.pkg),a.app+' Play package drift');
assert.match(card,/referrer=utm_source%3Dappdevelopsk/);
if (a.ios_app_id){
assert.ok(card.includes('apps.apple.com/app/id'+a.ios_app_id+'?ct=apps_catalog_'+a.app),a.app+' iOS CTA missing');
assert.ok(card.includes('data-store="app_store"'),a.app+' iOS store label missing');
}
}
});
test('five high-traffic-intent entries describe genuinely implemented core value',()=>{
Expand Down
Loading