@@ -878,9 +895,18 @@
Chaos - Stranger Missions
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();
})();
diff --git a/apps_data.json b/apps_data.json
index 2d92f41..1c5bec2 100644
--- a/apps_data.json
+++ b/apps_data.json
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -699,4 +703,4 @@
"desc": "Daily missions from strangers to break your routine and rediscover the world",
"installs": null
}
-]
\ No newline at end of file
+]
diff --git a/scripts/build_catalog.py b/scripts/build_catalog.py
index 9e34827..7830f90 100644
--- a/scripts/build_catalog.py
+++ b/scripts/build_catalog.py
@@ -51,6 +51,21 @@
"""
+# 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 = """
+

+
+
+"""
+
LANGS = ["en", "ja", "zh", "zh_TW", "ko", "es", "pt", "fr", "de", "it", "ru", "ar",
"th", "id", "tr", "nl", "vi"]
@@ -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'(
\n).*?(
\n )',
lambda m: m.group(1) + cards + m.group(2), t, "apps.html の grid", re.S)
diff --git a/tests/catalog-growth.test.mjs b/tests/catalog-growth.test.mjs
index 32e5fa1..5422788 100644
--- a/tests/catalog-growth.test.mjs
+++ b/tests/catalog-growth.test.mjs
@@ -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(/
/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"/);
@@ -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(/