--- name: web-presence description: Set up the web side of a mobile app or game - iOS universal links (apple-app-site-association, Associated Domains entitlement), Android App Links (Digital Asset Links assetlinks.json with the Play app signing SHA-256, intent filter autoVerify), the Safari Smart App Banner meta tag and a minimal landing page with App Store and Google Play links - then check the live files (HTTPS, no redirects, application/json, ids match). Use when the user wants links to open the app, sets up a domain or landing page for an app, sees universal links or app links not opening the app, gets "assetlinks" / "apple-app-site-association" errors, or asks for a Smart App Banner. --- # Web presence: universal links, app links, Smart App Banner, landing page CLI: `storebound` (or `bunx storebound` in a project that has it, or `bun /cli/index.ts`). - `storebound web init [--out web] [--landing] [--version-code N] [--dry] [--json]`: writes the files into a local folder (default `web/`). Local writes only; nothing is uploaded. - `storebound web check [] [--json]`: read-only, no credentials. Fetches the live files from `https://` without following redirects and lists every check as ok / problem / info with the fix. Exit code 1 when there is a problem. Config (`storebound.config.ts`): - `apple.teamId` (10 characters, developer.apple.com > Account > Membership details), `apple.bundleId`, `apple.appId` (App Store Connect > App Information > Apple ID, for the banner and the landing link). - `play.packageName`. - `web.domain` (`links.example.com`, no scheme or path), `web.paths` (default `["/*"]`), `web.exclude` (paths that stay in the browser), `web.androidSha256` (Play app signing SHA-256, `AB:CD:...`), `web.webcredentials` (password AutoFill), `web.landing`, `web.out`. - `app.name`, `app.privacyPolicyUrl`, `app.supportUrl`, `app.termsUrl` fill the landing page. ## Steps 1. Pick the domain with the owner. A subdomain only for links (`links.example.com`) is the simplest: then `web.paths: ["/*"]` is fine. On a domain that also has a normal website, list only the app paths (`["/invite/*", "/open/*"]`) so the rest of the site stays in the browser. 2. Fingerprint for Android. With Play App Signing the key on users' devices is Google's, not the local keystore: - Play Console > Test and release > App integrity > Play app signing: copy "SHA-256 certificate fingerprint" of the app signing key into `web.androidSha256`. Add the upload key too if you install locally signed builds and want links to verify there (an extra fingerprint is fine). - Or `storebound web init --version-code `: reads it read-only from the Play Developer API (generatedapks.list, `certificateSha256Hash`) and prints it; then put it in `web.androidSha256`. 3. `storebound web init --dry`, show the output, then `storebound web init` (add `--landing` for `index.html`). Missing inputs are listed as `skipped: `; unchanged files are left alone. 4. Owner step: deploy the folder contents to the domain root over HTTPS. Requirements the host must meet: - `/.well-known/apple-app-site-association` (no extension) and `/.well-known/assetlinks.json`, status 200, `Content-Type: application/json`, no redirect (no http -> https, apex -> www or trailing-slash redirect for these paths), public (no login, no IP or bot blocking, any user agent), valid certificate. - Many static hosts skip dot-folders or serve extension-less files as `application/octet-stream`: add a headers rule for the AASA path, and make sure a single-page-app fallback does not answer these paths with `index.html`. - The files go on every host the app handles (`example.com` and `www.example.com` are two hosts). 5. App side (the code; storebound does not edit it): - iOS: Signing & Capabilities > Associated Domains: `applinks:` (and `webcredentials:` with `web.webcredentials`). The App ID needs the Associated Domains capability. Handle the URL in `onOpenURL` / `scene(_:continue:)`. While developing against a server that is not public, use `applinks:?mode=developer` (developer mode on the device) to bypass Apple's CDN. - Android: in the activity, an intent filter with `android:autoVerify="true"`, action `VIEW`, categories `DEFAULT` and `BROWSABLE`, `` plus the same paths as `web.paths` (`android:pathPattern` / `pathPrefix`). `web.exclude` is also written as dynamic rules in assetlinks.json (Android 15+); older versions only use the intent filter, so keep excluded paths out of it. 6. `storebound web check` until there are no problems. Then test on devices: Apple's CDN fetches the AASA within about a day after install and rechecks about weekly; Android verifies at install (`adb shell pm get-app-links `, re-verify with `adb shell pm verify-app-links --re-verify `). 7. Smart App Banner: `smart-app-banner.html` holds the `` tag. Put it in the `` of each page that should show it (the landing page already has it). Add `app-argument=` by hand to pass a deep link. It shows in Safari on iPhone and iPad, not in the simulator or inside frames, and not again after the user closes it. ## Landing page `index.html` is a small, responsive page with no external requests: the app name, an App Store link (`https://apps.apple.com/app/id`), a Google Play link (`https://play.google.com/store/apps/details?id=`) and privacy / terms / support links. The store marks are the simple-icons files (`appstore.svg`, `googleplay.svg`, copied unchanged next to it) shown in the text colour, with the trademark note in the footer. They only name the store; for Apple's and Google's official badges, download them from Apple's marketing resources and Google Play's badge page and follow their guidelines. Edit the page freely after the first write; `web init` reports it as `updated` when it would replace your edits, so run `--dry` first. ## Owner decisions - The domain, which paths open the app, and whether a landing page replaces an existing site. - Hosting and DNS changes: storebound writes files, the owner deploys them. ## Out of scope - Deploying or hosting the files, DNS, certificates. - Editing the Xcode entitlements or AndroidManifest.xml. - Deferred deep links (install then open the right screen) and link attribution: that needs a link service. - App Clips (`appclips` in the AASA): not generated. ## Gotchas - A redirect on the AASA or assetlinks path breaks verification even when the browser shows the file fine. - `text/plain` or `application/octet-stream` on assetlinks.json fails Android verification; Apple wants `application/json` and an AASA of at most 128 KB. - Lower-case or colon-less fingerprints: assetlinks.json uses upper-case `AB:CD:...`; `web init` writes that form. - The local keystore fingerprint usually differs from the Play app signing one: links that work in debug builds can fail in the Play build. - iOS caches the AASA: after a fix, reinstall the app or wait for the CDN (developer mode skips it while testing).