diff --git a/.github/workflows/app-discovery-qa.yml b/.github/workflows/app-discovery-qa.yml index de838efa..76671d1a 100644 --- a/.github/workflows/app-discovery-qa.yml +++ b/.github/workflows/app-discovery-qa.yml @@ -10,6 +10,8 @@ on: - scripts/build_catalog.py - site_counts.json - tests/catalog-growth.test.mjs + - scripts/publish_app_discovery.sh + - scripts/check_app_discovery_live.sh - .github/workflows/app-discovery-qa.yml push: branches: [main] @@ -21,6 +23,8 @@ on: - scripts/build_catalog.py - site_counts.json - tests/catalog-growth.test.mjs + - scripts/publish_app_discovery.sh + - scripts/check_app_discovery_live.sh - .github/workflows/app-discovery-qa.yml permissions: contents: read @@ -38,5 +42,7 @@ jobs: python-version: '3.12' - name: Confirm catalog generator remains idempotent run: python3 scripts/build_catalog.py --check + - name: Verify non-destructive release scripts have valid shell syntax + run: bash -n scripts/publish_app_discovery.sh scripts/check_app_discovery_live.sh - name: Verify search, referral links and guide listings run: node --test tests/catalog-growth.test.mjs diff --git a/docs/APP_DISCOVERY_GROWTH_RELEASE_2026-10-27.md b/docs/APP_DISCOVERY_GROWTH_RELEASE_2026-10-27.md new file mode 100644 index 00000000..b10f732b --- /dev/null +++ b/docs/APP_DISCOVERY_GROWTH_RELEASE_2026-10-27.md @@ -0,0 +1,53 @@ +# App acquisition release and experiment plan (2026-10-27) + +## Source of truth / deployment reality + +- App catalog: **92** non-hidden Android listings in `apps_data.json` as of site count 2026-10-12. Do not call all 117 created apps Play-published. Hidden entries must never be promoted automatically. +- Production `appdevelopsk.com` is served by nginx on the VPS (`/var/www/html`). GitHub repository `appdevelopsk/appdevelopsk.github.io` **main is not itself the live site**. GitHub Pages publishes from a separate `pages-blank` branch and is not a replacement for the VPS. +- The source PR #4 introduced in `/apps` a searchable, accessible catalog and two new canonical SEO guides. **No successful HTTPS production deployment was established by merging it.** + +## Deployment with the existing local `pickly-vps` SSH alias + +From a **fresh main** checkout of this site repository, after checking working tree status: + +```bash +git pull --ff-only +bash scripts/publish_app_discovery.sh --dry-run +# Review the four changed destination paths. Never pass --delete. +bash scripts/publish_app_discovery.sh --apply +bash scripts/check_app_discovery_live.sh +``` + +Only these files transfer: `apps.html`, `sitemap.xml`, `guides/quick-games/index.html`, `guides/daily-habits/index.html`. Every other VPS file, including privacy, support, policy, payment and ads assets, stays untouched. To override environment use `APP_SITE_DEPLOY_HOST` and `APP_SITE_DEPLOY_ROOT` with a known-safe absolute path. The script refuses unknown flags/invalid targets. + +**Publish gate:** a dry-run is not publication. Only mark live once `check_app_discovery_live.sh` reports HTTP 200 and matches `id="find-app"`, both guide canonical URLs, guide click event and sitemap entries. After manual publication consider Google Search Console URL inspection and sitemap submission if the `appdevelopsk.com` property is available. + +## Growth experiment — acquisition, not vanity pageviews + +| Funnel stage | Event / source | Decision | +|---|---|---| +| Website entry | GA4 landing page, organic / referral / direct, `appdevelopsk.com/apps` and `/guides/*` | New landing sessions and engaged sessions by source | +| Discovery | `catalog_search_used`, `catalog_filter_used` | What proportion of visitors use discovery controls? These events must not include raw search terms | +| Store intent | `catalog_store_click`, `guide_store_click` with app slug | Store clicks by guide and category vs catalog views | +| Play install | Google Play Console listing visitors, acquisitions and Install Referrer campaign (`apps_catalog`, `quick_games`, `daily_habits`) | Conversion separately for each campaign, geography and released package | +| First app value | App GA4 events (first post, first game result, first photo, etc.) | Do newly acquired users finish core action? | +| Repeat value | Mature D1/D7 cohort with sufficient n, crash-free | Keep acquisition spend/effort only if app delivers retention | + +Do not sum the 117 apps' 7-day active-user counts as unique people. Do not interpret clicks as downloads, installs as activated users, or app-source merges as store releases. If a campaign has fewer than ~100 store visitors or D7 cohort n<30, call it directional/inconclusive. Monitor the first 7 days to catch broken links; compare at least 28-day periods for conversion trends while accounting for rollout date and channel mix. + +## Safe search-indexing / content integrity + +- All guides recommend only 4 existing **non-hidden** catalog apps each; their descriptions are sourced from the public catalog. Guides have no made-up star ratings, screenshots, money claims or app feature promises. +- All cards remain server-rendered so crawlers and no-JavaScript users still see published links. Filters run only when JS works. +- URLs use Play Install Referrer with guide-specific campaigns. Keep referrer and GA4 attribution separate; the actual install remains subject to Google Play behavior. +- If an app is delisted or renamed, edit `apps_data.json`, `scripts/build_catalog.py` and the affected guide together, then run Node tests and `python3 scripts/build_catalog.py --check`. + +## Release checklist + +- [x] Source changes merged to main (PR #4) +- [x] Static website tests and build-catalog consistency passed on PR #4 +- [ ] VPS rsync dry-run reviewed by deploy operator +- [ ] VPS content uploaded, actual HTTPS endpoint verified +- [ ] Sitemap submitted/indexing checked in Search Console +- [ ] GA4 DebugView events observed without search query payload +- [ ] Google Play listing traffic / acquisition comparison recorded after meaningful sample diff --git a/scripts/check_app_discovery_live.sh b/scripts/check_app_discovery_live.sh new file mode 100644 index 00000000..bc94cb9d --- /dev/null +++ b/scripts/check_app_discovery_live.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Read-only production smoke. Run after the VPS rsync; no deploy or credentials. +set -euo pipefail +base="${APP_SITE_PUBLIC_BASE:-https://appdevelopsk.com}" +[[ "$base" == 'https://appdevelopsk.com' ]] || { echo 'Expected canonical https://appdevelopsk.com' >&2; exit 2; } +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +ua='Mozilla/5.0 (compatible; SKAppsGrowthCheck/1.0)' +fetch() { + local route="$1" filename="$2" + local code + code=$(curl -sSL --compressed -A "$ua" --max-time 20 --retry 2 --retry-delay 2 \ + -o "$tmp/$filename" -w '%{http_code}' "$base$route") + [[ "$code" == 200 ]] || { echo "FAIL HTTP $code $base$route" >&2; exit 1; } +} +fetch '/apps' 'apps.html' +grep -Fq 'id="find-app"' "$tmp/apps.html" || { echo 'FAIL catalog search is stale' >&2; exit 1; } +grep -Fq 'catalog_store_click' "$tmp/apps.html" || { echo 'FAIL store-click attribution is stale' >&2; exit 1; } +for slug in quick-games daily-habits; do + fetch "/guides/$slug/" "$slug.html" + grep -Fq "https://appdevelopsk.com/guides/$slug/" "$tmp/$slug.html" || { echo "FAIL guide $slug content/canonical mismatch" >&2; exit 1; } + grep -Fq 'guide_store_click' "$tmp/$slug.html" || { echo "FAIL $slug click measurement missing" >&2; exit 1; } +done +fetch '/sitemap.xml' 'sitemap.xml' +for slug in quick-games daily-habits; do + grep -Fq "https://appdevelopsk.com/guides/$slug/" "$tmp/sitemap.xml" || { echo "FAIL sitemap missing $slug" >&2; exit 1; } +done +echo 'PASS: VPS catalog, two guides, click-tracking markers and sitemap all current (HTTP 200)' diff --git a/scripts/publish_app_discovery.sh b/scripts/publish_app_discovery.sh new file mode 100644 index 00000000..b157a397 --- /dev/null +++ b/scripts/publish_app_discovery.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Publish *only* the app-discovery growth files to the VPS backing appdevelopsk.com. +# Default is a non-destructive dry-run. --apply is an intentional human action. +set -euo pipefail +cd "$(dirname "$0")/.." +mode="${1:---dry-run}" +case "$mode" in + --dry-run) flags=(--dry-run) ;; + --apply) flags=() ;; + *) echo 'Usage: scripts/publish_app_discovery.sh [--dry-run|--apply]' >&2; exit 2 ;; +esac +host="${APP_SITE_DEPLOY_HOST:-pickly-vps}" +root="${APP_SITE_DEPLOY_ROOT:-/var/www/html}" +# Never evaluate an arbitrary SSH target or remote shell path. +if [[ ! "$host" =~ ^[a-zA-Z0-9_.@-]+$ || ! "$root" =~ ^/var/www/[a-zA-Z0-9_./-]+$ ]]; then + echo 'Invalid APP_SITE_DEPLOY_HOST or APP_SITE_DEPLOY_ROOT' >&2; exit 2 +fi +files=(apps.html sitemap.xml guides/quick-games/index.html guides/daily-habits/index.html) +for file in "${files[@]}"; do + [[ -s "$file" ]] || { echo "Missing source file: $file" >&2; exit 2; } +done +echo "Source checkout: $(git rev-parse --short HEAD 2>/dev/null || echo unknown)" +echo "Target: $host:$root; mode: $mode" +# --relative recreates only the listed nested file paths. No --delete. +rsync -avR --itemize-changes "${flags[@]}" -- "${files[@]}" "$host:$root/" +if [[ "$mode" == '--apply' ]]; then + echo 'Files transferred. Check CDN/VPS output (the publish command does not prove it is live):' + echo ' bash scripts/check_app_discovery_live.sh' +fi diff --git a/sitemap.xml b/sitemap.xml index 043a1c23..bbc387e2 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -5,7 +5,7 @@ https://appdevelopsk.com/contact2026-08-18yearly0.4 https://appdevelopsk.com/privacy2026-08-18yearly0.4 https://appdevelopsk.com/terms2026-08-18yearly0.4 - https://appdevelopsk.com/apps2026-08-18weekly0.9 + https://appdevelopsk.com/apps2026-10-27weekly0.9 https://appdevelopsk.com/support/2026-08-18weekly0.6 https://appdevelopsk.com/privacy-policy/2026-08-18weekly0.3 https://appdevelopsk.com/support/30sec-challenge/2026-08-18monthly0.8