diff --git a/public/site-links-client.js b/public/site-links-client.js
new file mode 100644
index 0000000..c536de9
--- /dev/null
+++ b/public/site-links-client.js
@@ -0,0 +1,310 @@
+/**
+ * Mirrors talkpro static site: /api/site-links, APK default, in-app + App Store modals.
+ * Strings come from #site-links-i18n (JSON) rendered by DownloadCTA.astro.
+ */
+(function () {
+ const DEFAULT_APK = 'https://talkspro.xyz/download';
+ let apkDownloadUrl = DEFAULT_APK;
+
+ function getApkDownloadUrl() {
+ return apkDownloadUrl;
+ }
+
+ function getI18n() {
+ const el = document.getElementById('site-links-i18n');
+ if (!el || !el.textContent.trim()) return {};
+ try {
+ return JSON.parse(el.textContent);
+ } catch {
+ return {};
+ }
+ }
+
+ function detectInAppBrowser() {
+ const ua = navigator.userAgent || '';
+ if (/MicroMessenger/i.test(ua)) return 'wechat';
+ try {
+ if (typeof window.WeixinJSBridge !== 'undefined') return 'wechat';
+ } catch {
+ /* ignore */
+ }
+ if (/QQ\//i.test(ua) && /MQQBrowser/i.test(ua)) return 'qq';
+ if (/Weibo/i.test(ua)) return 'weibo';
+ return '';
+ }
+
+ function isIOSDevice() {
+ const ua = navigator.userAgent || '';
+ if (/iPhone|iPad|iPod/i.test(ua)) return true;
+ if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) return true;
+ return false;
+ }
+
+ function isAndroidDevice() {
+ const ua = navigator.userAgent || '';
+ return /Android/i.test(ua) && !isIOSDevice();
+ }
+
+ const MODAL_WRAP =
+ 'position:fixed;inset:0;z-index:10050;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;';
+ const MODAL_BACK = 'position:absolute;inset:0;background:rgba(0,0,0,.45);cursor:pointer;';
+ const MODAL_PANEL =
+ 'position:relative;max-width:420px;width:100%;margin:auto;padding:20px 22px;border-radius:12px;background:#fff;color:#1a1a1a;font:15px/1.45 system-ui,-apple-system,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.18);';
+ const MODAL_TITLE = 'margin:0 0 10px;font-size:18px;font-weight:700;line-height:1.25;';
+ const MODAL_BODY = 'margin:0 0 18px;font-size:14px;color:#444;';
+ const MODAL_ACTIONS = 'display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;';
+ const BTN =
+ 'cursor:pointer;border-radius:8px;padding:10px 14px;font-size:14px;font-weight:600;border:1px solid #ccc;background:#f5f5f5;color:#1a1a1a;';
+ const BTN_PRIMARY = 'border-color:#1a6cff;background:#1a6cff;color:#fff;';
+
+ function initInAppBrowserModal() {
+ if (document.getElementById('inapp-browser-modal')) return;
+ const wrap = document.createElement('div');
+ wrap.id = 'inapp-browser-modal';
+ wrap.setAttribute('role', 'dialog');
+ wrap.setAttribute('aria-modal', 'true');
+ wrap.setAttribute('aria-hidden', 'true');
+ wrap.style.cssText = MODAL_WRAP + 'display:none;';
+ wrap.innerHTML =
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
';
+ document.body.appendChild(wrap);
+
+ const close = () => {
+ wrap.style.display = 'none';
+ wrap.setAttribute('aria-hidden', 'true');
+ };
+ wrap.querySelectorAll('[data-inapp-close]').forEach((el) => {
+ el.addEventListener('click', close);
+ });
+
+ const i18n = () => getI18n();
+ document.getElementById('inapp-copy-apk').addEventListener('click', async () => {
+ const btn = document.getElementById('inapp-copy-apk');
+ const dict = i18n();
+ const done = dict.inapp_browser_copied || 'Copied';
+ try {
+ await navigator.clipboard.writeText(getApkDownloadUrl());
+ const prev = btn.textContent;
+ btn.textContent = done;
+ setTimeout(() => {
+ btn.textContent = dict.inapp_browser_copy || prev;
+ }, 1600);
+ } catch {
+ window.prompt(dict.inapp_browser_copy || 'Copy', getApkDownloadUrl());
+ }
+ });
+
+ document.getElementById('inapp-try-chrome').addEventListener('click', () => {
+ const enc = encodeURIComponent(getApkDownloadUrl());
+ const intent =
+ getApkDownloadUrl().replace(/^https:/i, 'intent:') +
+ '#Intent;' +
+ 'scheme=https;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;' +
+ 'package=com.android.chrome;S.browser_fallback_url=' +
+ enc +
+ ';end';
+ window.location.href = intent;
+ });
+ }
+
+ function openInAppBrowserModal() {
+ const modal = document.getElementById('inapp-browser-modal');
+ if (!modal) return;
+ const dict = getI18n();
+ const tryChrome = modal.querySelector('#inapp-try-chrome');
+ const showChrome = isAndroidDevice() && !!detectInAppBrowser();
+ if (tryChrome) {
+ tryChrome.hidden = !showChrome;
+ tryChrome.setAttribute('aria-hidden', showChrome ? 'false' : 'true');
+ }
+ modal.style.display = 'flex';
+ modal.setAttribute('aria-hidden', 'false');
+ const titleEl = modal.querySelector('#inapp-modal-title');
+ const bodyEl = modal.querySelector('#inapp-modal-body');
+ if (titleEl) titleEl.textContent = dict.inapp_browser_title || '';
+ if (bodyEl) {
+ const iosWechat = isIOSDevice() && detectInAppBrowser() === 'wechat';
+ const bodyKey = iosWechat ? 'inapp_browser_body_ios' : 'inapp_browser_body';
+ bodyEl.innerHTML =
+ dict[bodyKey] || dict.inapp_browser_body || '';
+ }
+ const copyBtn = modal.querySelector('#inapp-copy-apk');
+ const gotBtn = modal.querySelector('#inapp-got-it');
+ if (copyBtn) copyBtn.textContent = dict.inapp_browser_copy || '';
+ if (tryChrome) tryChrome.textContent = dict.inapp_browser_try_chrome || '';
+ if (gotBtn) gotBtn.textContent = dict.inapp_browser_got_it || 'OK';
+ }
+
+ function initAppStoreSoonModal() {
+ if (document.getElementById('app-store-soon-modal')) return;
+ const wrap = document.createElement('div');
+ wrap.id = 'app-store-soon-modal';
+ wrap.setAttribute('role', 'dialog');
+ wrap.setAttribute('aria-modal', 'true');
+ wrap.setAttribute('aria-hidden', 'true');
+ wrap.style.cssText = MODAL_WRAP + 'display:none;';
+ wrap.innerHTML =
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
';
+ document.body.appendChild(wrap);
+ const close = () => {
+ wrap.style.display = 'none';
+ wrap.setAttribute('aria-hidden', 'true');
+ };
+ wrap.querySelectorAll('[data-app-soon-close]').forEach((el) => {
+ el.addEventListener('click', close);
+ });
+ }
+
+ function openAppStoreSoonModal() {
+ const modal = document.getElementById('app-store-soon-modal');
+ if (!modal) return;
+ const dict = getI18n();
+ const tEl = modal.querySelector('#app-soon-title');
+ const bEl = modal.querySelector('#app-soon-body');
+ const ok = modal.querySelector('button[data-app-soon-close]');
+ if (tEl) tEl.textContent = dict.app_store_soon_title || '';
+ if (bEl) bEl.innerHTML = dict.app_store_soon_body || '';
+ if (ok) ok.textContent = dict.app_store_soon_ok || 'OK';
+ modal.style.display = 'flex';
+ modal.setAttribute('aria-hidden', 'false');
+ }
+
+ function bindBrowserDownloadLinks() {
+ document.querySelectorAll('a.store-badge--browser').forEach((a) => {
+ const openForInApp = (e) => {
+ if (!detectInAppBrowser()) return;
+ e.preventDefault();
+ e.stopPropagation();
+ openInAppBrowserModal();
+ };
+ a.addEventListener('click', openForInApp);
+ a.addEventListener(
+ 'touchend',
+ (e) => {
+ if (!detectInAppBrowser()) return;
+ e.preventDefault();
+ openInAppBrowserModal();
+ },
+ { passive: false }
+ );
+ });
+ }
+
+ function bindAppStoreAppleBadges() {
+ document.querySelectorAll('a.store-badge--apple[data-app-soon]').forEach((a) => {
+ const stopAndOpen = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openAppStoreSoonModal();
+ };
+ a.addEventListener('click', stopAndOpen);
+ a.addEventListener(
+ 'touchend',
+ (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openAppStoreSoonModal();
+ },
+ { passive: false }
+ );
+ });
+ }
+
+ function loadSiteLinksThenBind() {
+ if (window.location.protocol === 'file:') {
+ bindBrowserDownloadLinks();
+ bindAppStoreAppleBadges();
+ return;
+ }
+ fetch('/api/site-links', { credentials: 'same-origin' })
+ .then((r) => {
+ if (!r.ok) throw new Error('bad status');
+ return r.json();
+ })
+ .then((j) => {
+ const apk = String(j.apk_download_url || '').trim();
+ const ios = String(j.app_store_url || '').trim();
+ if (apk.startsWith('https://')) apkDownloadUrl = apk;
+ document.querySelectorAll('a.store-badge--browser').forEach((a) => {
+ if (apk.startsWith('https://')) a.setAttribute('href', apk);
+ });
+ document.querySelectorAll('a.store-badge--apple').forEach((a) => {
+ if (ios.startsWith('https://')) {
+ a.setAttribute('href', ios);
+ a.removeAttribute('data-app-soon');
+ a.setAttribute('rel', 'noopener noreferrer');
+ a.setAttribute('target', '_blank');
+ }
+ });
+ const meta = document.getElementById('site-links-meta');
+ if (meta) {
+ const dict = getI18n();
+ const u = String(j.updated_at || '').trim();
+ if (u) {
+ meta.textContent =
+ (dict.linksMetaPrefix || 'Store links file updated: ') +
+ u +
+ (dict.linksMetaSuffix || ' (GMT+8)');
+ meta.hidden = false;
+ } else {
+ meta.hidden = true;
+ }
+ }
+ })
+ .catch(() => {
+ /* keep defaults */
+ })
+ .finally(() => {
+ bindBrowserDownloadLinks();
+ bindAppStoreAppleBadges();
+ });
+ }
+
+ initInAppBrowserModal();
+ initAppStoreSoonModal();
+ loadSiteLinksThenBind();
+})();
diff --git a/src/components/DownloadCTA.astro b/src/components/DownloadCTA.astro
index 8166d5a..49923fc 100644
--- a/src/components/DownloadCTA.astro
+++ b/src/components/DownloadCTA.astro
@@ -3,14 +3,17 @@ import type { Translations } from '../i18n/translations'
export interface Props {
t: Translations['download']
+ siteLinks: Translations['siteLinks']
}
-const { t } = Astro.props
+const { t, siteLinks } = Astro.props
const bgPattern = "/assets/cta-bg-pattern.svg";
const talkproLogo = "/assets/cta-talkpro-logo.svg";
const androidIcon = "/assets/cta-android-icon.svg";
const appleIcon = "/assets/cta-apple-icon.svg";
const phoneArt = "/assets/cta-phone-art.png";
+const defaultApkHref = "https://talkspro.xyz/download";
+const siteLinksJson = JSON.stringify(siteLinks);
---
@@ -35,7 +38,12 @@ const phoneArt = "/assets/cta-phone-art.png";
-
-
+
+
@@ -63,3 +80,5 @@ const phoneArt = "/assets/cta-phone-art.png";
+
+
diff --git a/src/components/Hero.astro b/src/components/Hero.astro
index 293d7c1..005f944 100644
--- a/src/components/Hero.astro
+++ b/src/components/Hero.astro
@@ -11,6 +11,7 @@ const heroBg = "/assets/hero-bg.png";
const phoneMockup = "/assets/hero-phone.png";
const androidIcon = "/assets/cta-android-icon.svg";
const appleIcon = "/assets/cta-apple-icon.svg";
+const defaultApkHref = "https://talkspro.xyz/download";
---
@@ -39,7 +40,12 @@ const appleIcon = "/assets/cta-apple-icon.svg";
{t.description}
-
+
diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts
index 9a5612f..0bb5066 100644
--- a/src/i18n/translations.ts
+++ b/src/i18n/translations.ts
@@ -281,6 +281,23 @@ export const translations = {
appleAlt: "Apple",
phoneAlt: "TalkPro on phone",
},
+ siteLinks: {
+ linksMetaPrefix: "Store links file updated: ",
+ linksMetaSuffix: " (GMT+8)",
+ inapp_browser_title: "Open in your system browser",
+ inapp_browser_body:
+ "This page is opened inside an in-app browser. To download the APK, tap the menu (··· or ⋮) in the upper corner, then choose Open in default browser, Open in Safari, or Open in Chrome. Return here and tap the Android badge again—or paste the link you copied.",
+ inapp_browser_body_ios:
+ "You’re in WeChat’s built-in browser on iPhone or iPad. Apple and WeChat do not allow a website to jump to Safari automatically—this is normal. Tap ··· (top right) → Open in Safari or Open in default browser, then tap the Android badge again, or use Copy download link and paste it in Safari.",
+ inapp_browser_copy: "Copy download link",
+ inapp_browser_copied: "Copied",
+ inapp_browser_got_it: "OK",
+ inapp_browser_try_chrome: "Try opening in Chrome (Android)",
+ app_store_soon_title: "Coming soon",
+ app_store_soon_body:
+ "The App Store download is still in development and is not available yet. Please use the Android download for now.",
+ app_store_soon_ok: "OK",
+ },
footer: {
logoAlt: "TalkPro",
description:
@@ -536,6 +553,23 @@ export const translations = {
appleAlt: "Apple",
phoneAlt: "手机上的 TalkPro",
},
+ siteLinks: {
+ linksMetaPrefix: "Store links file updated: ",
+ linksMetaSuffix: " (GMT+8)",
+ inapp_browser_title: "请在系统浏览器中打开",
+ inapp_browser_body:
+ "当前页面在应用内置浏览器中打开,无法可靠下载 APK。请点击右上角「···」或「⋮」菜单,选择「在浏览器打开」「用默认浏览器打开」「在 Safari 打开」或「在 Chrome 打开」,在系统浏览器中再次点击 Android 徽章;或使用下方按钮复制链接,粘贴到系统浏览器地址栏打开。",
+ inapp_browser_body_ios:
+ "您正在 iPhone/iPad 的微信内置浏览器中。微信与 iOS 不允许网页自动跳转到系统 Safari,这是系统限制,不是网站故障。请点击右上角「···」,选择「在 Safari 中打开」或「在默认浏览器中打开」,再在 Safari 里点 Android 徽章;或先复制下载链接,到 Safari 地址栏粘贴打开。",
+ inapp_browser_copy: "复制下载链接",
+ inapp_browser_copied: "已复制",
+ inapp_browser_got_it: "知道了",
+ inapp_browser_try_chrome: "尝试用 Chrome 打开(Android)",
+ app_store_soon_title: "开发中",
+ app_store_soon_body:
+ "App Store 下载尚在开发与上架准备中,暂时未开放。请先使用 Android 下载。带来不便敬请谅解。",
+ app_store_soon_ok: "知道了",
+ },
footer: {
logoAlt: "TalkPro",
description:
@@ -699,6 +733,23 @@ export const translations = {
appleAlt: "Apple",
phoneAlt: "手機上的 TalkPro",
},
+ siteLinks: {
+ linksMetaPrefix: "Store links file updated: ",
+ linksMetaSuffix: " (GMT+8)",
+ inapp_browser_title: "請在系統瀏覽器中開啟",
+ inapp_browser_body:
+ "目前頁面在應用程式內建瀏覽器中開啟,無法可靠下載 APK。請點選右上角「···」或「⋮」選單,選擇「在瀏覽器開啟」「用預設瀏覽器開啟」「在 Safari 開啟」或「在 Chrome 開啟」,在系統瀏覽器中再次點選 Android 徽章;或使用下方按鈕複製連結,貼到系統瀏覽器網址列開啟。",
+ inapp_browser_body_ios:
+ "您正在 iPhone/iPad 的微信內建瀏覽器中。微信與 iOS 不允許網頁自動跳轉到系統 Safari,這是系統限制,不是網站故障。請點選右上角「···」,選擇「在 Safari 中開啟」或「在預設瀏覽器中開啟」,再在 Safari 裡點 Android 徽章;或先複製下載連結,到 Safari 網址列貼上開啟。",
+ inapp_browser_copy: "複製下載連結",
+ inapp_browser_copied: "已複製",
+ inapp_browser_got_it: "知道了",
+ inapp_browser_try_chrome: "嘗試用 Chrome 開啟(Android)",
+ app_store_soon_title: "開發中",
+ app_store_soon_body:
+ "App Store 下載尚在開發與上架準備中,暫時未開放。請先使用 Android 下載。造成不便敬請見諒。",
+ app_store_soon_ok: "知道了",
+ },
footer: {
logoAlt: "TalkPro",
description:
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
index e4d42c5..57c8450 100644
--- a/src/layouts/Base.astro
+++ b/src/layouts/Base.astro
@@ -27,6 +27,7 @@ const {
+