Created
January 8, 2026 19:52
-
-
Save mirzemehdi/c9e11a320277a841399421c8220cce61 to your computer and use it in GitHub Desktop.
Automatically generates App Store and Google Play metadata (title, subtitle, keywords, description) for multiple locales using OpenAI. Accepts an app idea or PRD file and optionally target keywords, following real ASO best practices. Outputs files for iOS and Android, with parallel generation and locale support.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ============================================================ | |
| # ASO Metadata Generator (Named Arguments) | |
| # ============================================================ | |
| # ---------------------------- | |
| # CONFIG | |
| # ---------------------------- | |
| OPENAI_API_KEY="" # set your OpenAI key | |
| MODEL="gpt-4" | |
| OUTPUT_DIRECTORY="./store_metadata" | |
| DEFAULT_LOCALES=("en-US") | |
| DEFAULT_STORE="both" | |
| # ---------------------------- | |
| # VARIABLES | |
| # ---------------------------- | |
| STORE="$DEFAULT_STORE" | |
| KEYWORDS="" | |
| IDEA_TEXT="" | |
| IDEA_FILE="" | |
| LOCALES_INPUT="" | |
| # ---------------------------- | |
| # HELP | |
| # ---------------------------- | |
| usage() { | |
| cat <<EOF | |
| Usage: | |
| ./generate_aso_metadata.sh --keywords "<keywords>" (--idea "<text>" | --idea-file <file>) [--locales "<l1,l2>"] | |
| Required: | |
| --idea "<text>" OR --idea-file <file> | |
| Options: | |
| --keywords Comma-separated ASO target keywords that you want to be ranked for | |
| --idea Short app idea or description | |
| --store ios|android|both (default: both) | |
| --idea-file Path to PRD / idea document (markdown or text) | |
| --locales Comma-separated locales (default: ${DEFAULT_LOCALES[*]}) | |
| -h, --help Show this help | |
| Examples: | |
| ./generate_aso_metadata.sh --idea "AI manga translator" | |
| ./generate_aso_metadata.sh --idea-file prd.md --store ios | |
| ./generate_aso_metadata.sh --idea "OCR scanner" --keywords "document scanner,ocr app" --locales "en-US,es-ES" | |
| EOF | |
| exit 1 | |
| } | |
| # ---------------------------- | |
| # ARGUMENT PARSING | |
| # ---------------------------- | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --keywords) | |
| KEYWORDS="$2" | |
| shift 2 | |
| ;; | |
| --idea) | |
| IDEA_TEXT="$2" | |
| shift 2 | |
| ;; | |
| --idea-file) | |
| IDEA_FILE="$2" | |
| shift 2 | |
| ;; | |
| --locales) | |
| LOCALES_INPUT="$2" | |
| shift 2 | |
| ;; | |
| --store) | |
| STORE="$2" | |
| shift 2 | |
| ;; | |
| -h|--help) | |
| usage | |
| ;; | |
| *) | |
| echo "β Unknown argument: $1" | |
| usage | |
| ;; | |
| esac | |
| done | |
| # ---------------------------- | |
| # VALIDATION | |
| # ---------------------------- | |
| if [[ -z "$IDEA_TEXT" && -z "$IDEA_FILE" ]]; then | |
| echo "β One of --idea or --idea-file is required" | |
| usage | |
| fi | |
| if [[ -n "$IDEA_TEXT" && -n "$IDEA_FILE" ]]; then | |
| echo "β Use either --idea or --idea-file, not both" | |
| usage | |
| fi | |
| if [[ "$STORE" != "ios" && "$STORE" != "android" && "$STORE" != "both" ]]; then | |
| echo "β Invalid --store value" | |
| usage | |
| fi | |
| # ---------------------------- | |
| # LOAD IDEA CONTENT | |
| # ---------------------------- | |
| if [[ -n "$IDEA_FILE" ]]; then | |
| if [[ ! -f "$IDEA_FILE" ]]; then | |
| echo "β Idea file not found: $IDEA_FILE" | |
| exit 1 | |
| fi | |
| echo "π Loading app idea from $IDEA_FILE" >&2 | |
| APP_IDEA="$(cat "$IDEA_FILE")" | |
| else | |
| APP_IDEA="$IDEA_TEXT" | |
| fi | |
| # ---------------------------- | |
| # LOCALES | |
| # ---------------------------- | |
| if [[ -z "$LOCALES_INPUT" ]]; then | |
| LOCALES_INPUT=$(IFS=','; echo "${DEFAULT_LOCALES[*]}") | |
| fi | |
| IFS=',' read -ra LOCALES <<< "$LOCALES_INPUT" | |
| # ---------------------------- | |
| # PROMPT BUILDER | |
| # ---------------------------- | |
| build_ios_prompt() { | |
| local locale="$1" | |
| cat <<EOF | |
| You are a professional iOS App Store ASO expert. | |
| Your goal is to maximize keyword ranking according to Apple's App Store rules. | |
| IMPORTANT ASO FACTS (follow strictly): | |
| - App Name has the HIGHEST ranking weight | |
| - Words earlier (left-most) rank stronger | |
| - Subtitle has the SECOND highest ranking weight | |
| - Keyword field has LOWER weight | |
| - Description has NO ranking impact (conversion only) | |
| APP CONTEXT (this is the source of truth): | |
| $APP_IDEA | |
| TARGET KEYWORDS (OPTIONAL): | |
| ${KEYWORDS:-"(not provided β derive from app context)"} | |
| LOCALE: | |
| $locale | |
| TASK: | |
| Generate optimized iOS App Store metadata following these rules: | |
| 1. APP NAME (Title) | |
| - Put the SINGLE most important keyword FIRST | |
| - Front-load it (left-most position) | |
| - Can include a short brand or descriptor after | |
| - No fluff, no marketing phrases | |
| - Maximize ranking first, branding second | |
| 2. SUBTITLE | |
| - Use 1β2 SECONDARY keywords | |
| - Front-load important words | |
| - Do NOT repeat words from the title | |
| - Keep it concise and descriptive | |
| 3. KEYWORDS FIELD | |
| - Comma-separated | |
| - Max 100 characters total | |
| - No spaces | |
| - NO words used in title or subtitle | |
| - Include synonyms, plural/singular, long-tail variations | |
| 4. DESCRIPTION | |
| - Written ONLY for conversion | |
| - Explain features and benefits clearly | |
| - No keyword stuffing | |
| OUTPUT FORMAT: | |
| - Return ONLY valid JSON | |
| - No explanations | |
| - No markdown | |
| - Keys exactly: name, subtitle, keywords, description | |
| EOF | |
| } | |
| build_android_prompt() { | |
| local locale="$1" | |
| cat <<EOF | |
| You are a professional Google Play ASO expert. | |
| IMPORTANT GOOGLE PLAY RULES: | |
| - Title has the HIGHEST ranking weight | |
| - Short description has VERY strong ranking impact | |
| - Long description IS indexed | |
| - No keyword field | |
| - Natural keyword usage and density matter | |
| APP CONTEXT: | |
| $APP_IDEA | |
| TARGET KEYWORDS (optional): | |
| ${KEYWORDS:-"(not provided β derive from app context)"} | |
| INSTRUCTIONS: | |
| - If keywords are not provided, derive them from app context | |
| - Choose a PRIMARY keyword and several SECONDARY keywords | |
| - Use keywords naturally, not spammy | |
| LOCALE: | |
| $locale | |
| FIELDS: | |
| 1. TITLE | |
| - Include PRIMARY keyword | |
| - Place it as early as possible | |
| - Maximize ranking but remain readable | |
| 2. SHORT DESCRIPTION | |
| - 80 characters max | |
| - Include PRIMARY + SECONDARY keywords | |
| - Strong value proposition | |
| - Natural language | |
| 3. FULL DESCRIPTION | |
| - 3β5 mentions of PRIMARY keyword | |
| - Natural mentions of secondary keywords | |
| - Explain features, benefits, and use cases | |
| - Use short paragraphs or bullet points | |
| - Avoid keyword stuffing | |
| OUTPUT: | |
| Return ONLY valid JSON: | |
| title, short_description, full_description | |
| EOF | |
| } | |
| # ---------------------------- | |
| # OPENAI CALL | |
| # ---------------------------- | |
| call_openai() { | |
| local prompt="$1" | |
| local payload | |
| payload=$(jq -n \ | |
| --arg model "$MODEL" \ | |
| --arg content "$prompt" \ | |
| '{ | |
| model: $model, | |
| messages: [{ role: "user", content: $content }] | |
| }' | |
| ) | |
| response=$(curl -s https://api.openai.com/v1/chat/completions \ | |
| -H "Authorization: Bearer $OPENAI_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$payload" | |
| ) | |
| if echo "$response" | jq -e '.error' >/dev/null; then | |
| echo "β OpenAI error:" >&2 | |
| echo "$response" >&2 | |
| exit 1 | |
| fi | |
| echo "$response" | jq -r '.choices[0].message.content' | |
| } | |
| # ---------------------------- | |
| # GENERATE (PARALLEL) | |
| # ---------------------------- | |
| PIDS=() | |
| for locale in "${LOCALES[@]}"; do | |
| ( | |
| echo "π $locale β $STORE" >&2 | |
| if [[ "$STORE" == "ios" || "$STORE" == "both" ]]; then | |
| ios_prompt="$(build_ios_prompt "$locale")" | |
| ios_json="$(call_openai "$ios_prompt")" | |
| out="$OUTPUT_DIRECTORY/appstore/$locale" | |
| mkdir -p "$out" | |
| echo "$ios_json" | jq -r '.name' > "$out/name.txt" | |
| echo "$ios_json" | jq -r '.subtitle' > "$out/subtitle.txt" | |
| echo "$ios_json" | jq -r '.keywords' > "$out/keywords.txt" | |
| echo "$ios_json" | jq -r '.description' > "$out/description.txt" | |
| fi | |
| if [[ "$STORE" == "android" || "$STORE" == "both" ]]; then | |
| android_prompt="$(build_android_prompt "$locale")" | |
| android_json="$(call_openai "$android_prompt")" | |
| out="$OUTPUT_DIRECTORY/playstore/$locale" | |
| mkdir -p "$out" | |
| echo "$android_json" | jq -r '.title' > "$out/title.txt" | |
| echo "$android_json" | jq -r '.short_description' > "$out/short_description.txt" | |
| echo "$android_json" | jq -r '.full_description' > "$out/full_description.txt" | |
| fi | |
| echo "β $locale done" >&2 | |
| ) & | |
| PIDS+=($!) | |
| done | |
| for pid in "${PIDS[@]}"; do | |
| wait "$pid" | |
| done | |
| echo "π ASO metadata generated successfully in $OUTPUT_DIRECTORY !" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ASO Metadata Generator (iOS + Google Play)
Automatically generates App Store and Google Play metadata using OpenAI GPT, following ASO best practices.
Features
Usage
Arguments
Output Strucutre
File names are same as what Fastlane uses
store_metadata/
βββ ios/
β βββ /
β βββ name.txt
β βββ subtitle.txt
β βββ keywords.txt
β βββ description.txt
βββ android/
βββ /
βββ title.txt
βββ short_description.txt
βββ full_description.txt