storebound/skills/web-presence/SKILL.md

7.1 KiB

name description
web-presence 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 <toolchain>/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 [<domain>] [--json]: read-only, no credentials. Fetches the live files from https://<domain> 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 <a versionCode you uploaded>: 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: <why, how to fix>; 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:<domain> (and webcredentials:<domain> 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:<domain>?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, <data android:scheme="https" android:host="<domain>" /> 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 <package>, re-verify with adb shell pm verify-app-links --re-verify <package>).
  7. Smart App Banner: smart-app-banner.html holds the <meta name="apple-itunes-app" content="app-id=..."> tag. Put it in the <head> of each page that should show it (the landing page already has it). Add app-argument=<url> 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<appId>), a Google Play link (https://play.google.com/store/apps/details?id=<package>) 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).