youtube-title-tag-optimizer

Installation
SKILL.md

YouTube Title & Tag Optimizer

Optimize your video title, tags, and description before publishing by analyzing what works for top-ranking videos.

Usage

/youtube-title-tag-optimizer "air fryer recipes"
/youtube-title-tag-optimizer "Python tutorial" --my-title "Learn Python in 10 Minutes"
/youtube-title-tag-optimizer --keyword "home workout" --my-title "Best Home Workout for Beginners 2024"

Instructions

When the user invokes this skill:

Step 1: Parse Arguments

Extract from the user's input:

  • Keyword (required): The topic/keyword to optimize for
  • --my-title "..." (optional): User's working title to score and improve

Step 2: Get API Key

Check the user's Claude memory for a YouTube Data API v3 key. If not found, ask:

"I need a YouTube Data API v3 key to analyze ranking videos. You can get one from the Google Cloud Console. Please paste your key."

Step 3: Write the Script

Write the following Python script to /tmp/_yt_title_optimizer_XXXX.py (where XXXX is a random suffix, e.g. $(openssl rand -hex 4)):

#!/usr/bin/env python3
"""
YouTube Title & Tag Optimizer

Analyzes top-ranking videos for a keyword to extract winning title patterns,
effective tags, and generate optimization recommendations.

Usage:
    YT_API_KEY=KEY python3 /tmp/_yt_title_optimizer_XXXX.py "keyword"
    YT_API_KEY=KEY python3 /tmp/_yt_title_optimizer_XXXX.py "keyword" --my-title "My Title"
"""

import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from collections import Counter

try:
    from googleapiclient.discovery import build
    from googleapiclient.errors import HttpError
except ImportError:
    print("ERROR: google-api-python-client not installed. Run: pip3 install google-api-python-client")
    sys.exit(1)


def parse_duration(iso_duration: str) -> int:
    m = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", iso_duration or "")
    if not m:
        return 0
    return int(m.group(1) or 0) * 3600 + int(m.group(2) or 0) * 60 + int(m.group(3) or 0)


def format_number(n: int) -> str:
    if n >= 1_000_000: return f"{n/1_000_000:.1f}M"
    if n >= 1_000: return f"{n/1_000:.1f}K"
    return str(n)


def analyze_title(title: str) -> dict:
    """Analyze a single title for patterns."""
    analysis = {
        "length_chars": len(title),
        "length_words": len(title.split()),
        "has_number": bool(re.search(r'\d', title)),
        "has_question": title.rstrip().endswith("?"),
        "has_how_to": bool(re.search(r'how\s+to', title, re.I)),
        "has_brackets": bool(re.search(r'[\[\(]', title)),
        "has_parentheses": bool(re.search(r'\(', title)),
        "has_caps_word": bool(re.search(r'\b[A-Z]{2,}\b', title)),
        "has_emoji": bool(re.search(r'[^\w\s,.\-!?\'\"()\[\]:;/\\@#$%^&*+=~`|<>]', title)),
        "has_year": bool(re.search(r'20[12]\d', title)),
        "has_list_format": bool(re.search(r'^\d+\s', title)),
        "has_vs": bool(re.search(r'\bvs\.?\b', title, re.I)),
        "has_colon": ":" in title,
        "has_pipe": "|" in title,
        "has_dash_separator": " - " in title or " — " in title,
        "has_exclamation": "!" in title,
        "starts_with_how": title.lower().startswith("how"),
        "starts_with_why": title.lower().startswith("why"),
        "starts_with_what": title.lower().startswith("what"),
        "starts_with_number": bool(re.match(r'^\d', title)),
    }

    # Detect power words
    power_words = {
        "urgency": ["now", "today", "immediately", "urgent", "hurry", "fast", "quick", "instantly", "stop"],
        "curiosity": ["secret", "hidden", "surprising", "shocking", "unexpected", "weird", "strange", "mystery", "truth", "reveal"],
        "value": ["free", "best", "top", "ultimate", "complete", "guide", "hack", "tips", "tricks", "easy", "simple"],
        "emotional": ["amazing", "incredible", "insane", "mind-blowing", "life-changing", "game-changer", "beautiful", "perfect", "love", "hate"],
        "fear": ["mistake", "wrong", "avoid", "never", "worst", "dangerous", "warning", "risk", "fail", "scam", "don't"],
    }

    title_lower = title.lower()
    found_power_words = {}
    for category, words in power_words.items():
        matches = [w for w in words if w in title_lower]
        if matches:
            found_power_words[category] = matches
    analysis["power_words"] = found_power_words

    # Detect title structure/formula
    structures = []
    if re.match(r'^\d+\s', title):
        structures.append("listicle")
    if re.search(r'how\s+to', title, re.I):
        structures.append("how-to")
    if title.rstrip().endswith("?"):
        structures.append("question")
    if re.search(r'\bvs\.?\b', title, re.I):
        structures.append("comparison")
    if re.search(r'\breview\b', title, re.I):
        structures.append("review")
    if re.search(r'in\s+\d+\s+(minute|min|second|sec|hour|day|step)', title, re.I):
        structures.append("time-bound")
    if re.search(r'for\s+(beginners|newbies|noobs|starters)', title, re.I):
        structures.append("beginner-targeted")
    if re.search(r'(complete|ultimate|full|definitive)\s+guide', title, re.I):
        structures.append("comprehensive-guide")
    if re.search(r'\b(do|don\'t|never|stop|avoid)\b', title, re.I):
        structures.append("imperative")
    analysis["structures"] = structures

    return analysis


def main():
    parser = argparse.ArgumentParser(description="Optimize YouTube titles and tags")
    parser.add_argument("keyword", help="Keyword to optimize for")
    parser.add_argument("--my-title", default=None, help="Your working title to score")
    parser.add_argument("--output-dir", default=None, help="Output directory")
    args = parser.parse_args()

    api_key = os.environ.get("YT_API_KEY")
    if not api_key:
        print("ERROR: YT_API_KEY environment variable not set.")
        sys.exit(1)

    youtube = build("youtube", "v3", developerKey=api_key)
    quota_used = 0

    print(f"Analyzing keyword: {args.keyword}")
    if args.my_title:
        print(f"Your title: {args.my_title}\n")

    # --- Search by relevance ---
    print("Fetching top-ranking videos (by relevance)...")
    try:
        relevance_resp = youtube.search().list(
            part="snippet", q=args.keyword, type="video",
            order="relevance", maxResults=50,
        ).execute()
        relevance_ids = [item["id"]["videoId"] for item in relevance_resp.get("items", [])]
        quota_used += 100
    except HttpError as e:
        print(f"Search error: {e}")
        sys.exit(1)

    # --- Search by view count ---
    print("Fetching most-viewed videos...")
    try:
        viewcount_resp = youtube.search().list(
            part="snippet", q=args.keyword, type="video",
            order="viewCount", maxResults=50,
        ).execute()
        viewcount_ids = [item["id"]["videoId"] for item in viewcount_resp.get("items", [])]
        quota_used += 100
    except HttpError as e:
        viewcount_ids = []
        print(f"View count search error: {e}")

    # Combine and deduplicate
    all_ids = list(dict.fromkeys(relevance_ids + viewcount_ids))
    print(f"Total unique videos: {len(all_ids)}")

    # --- Fetch video details ---
    print("Fetching video details...")
    videos = []
    for i in range(0, len(all_ids), 50):
        batch = all_ids[i:i+50]
        try:
            resp = youtube.videos().list(
                part="snippet,statistics,contentDetails",
                id=",".join(batch),
            ).execute()
            videos.extend(resp.get("items", []))
            quota_used += 1
        except HttpError as e:
            print(f"Error: {e}")
    print(f"  Got details for {len(videos)} videos")

    # --- Process videos ---
    processed = []
    all_tags = Counter()
    title_analyses = []

    for v in videos:
        views = int(v.get("statistics", {}).get("viewCount", 0))
        likes = int(v.get("statistics", {}).get("likeCount", 0))
        comments = int(v.get("statistics", {}).get("commentCount", 0))
        duration = parse_duration(v.get("contentDetails", {}).get("duration", ""))
        title = v["snippet"]["title"]
        tags = v.get("snippet", {}).get("tags", [])

        for tag in tags:
            all_tags[tag.lower()] += 1

        t_analysis = analyze_title(title)
        title_analyses.append(t_analysis)

        processed.append({
            "video_id": v["id"],
            "title": title,
            "title_analysis": t_analysis,
            "channel": v["snippet"]["channelTitle"],
            "views": views,
            "likes": likes,
            "comments": comments,
            "engagement_rate": round((likes + comments) / max(views, 1) * 100, 2),
            "duration_sec": duration,
            "tags": tags,
            "tag_count": len(tags),
            "description_first_line": v["snippet"].get("description", "").split("\n")[0][:200],
            "published_at": v["snippet"]["publishedAt"],
        })

    processed.sort(key=lambda x: x["views"], reverse=True)

    # --- Aggregate title analysis ---
    total = len(title_analyses) or 1
    aggregate_title = {
        "avg_length_chars": round(sum(t["length_chars"] for t in title_analyses) / total, 1),
        "avg_length_words": round(sum(t["length_words"] for t in title_analyses) / total, 1),
        "pct_has_number": round(sum(1 for t in title_analyses if t["has_number"]) / total * 100, 1),
        "pct_question": round(sum(1 for t in title_analyses if t["has_question"]) / total * 100, 1),
        "pct_how_to": round(sum(1 for t in title_analyses if t["has_how_to"]) / total * 100, 1),
        "pct_brackets": round(sum(1 for t in title_analyses if t["has_brackets"]) / total * 100, 1),
        "pct_caps_word": round(sum(1 for t in title_analyses if t["has_caps_word"]) / total * 100, 1),
        "pct_emoji": round(sum(1 for t in title_analyses if t["has_emoji"]) / total * 100, 1),
        "pct_year": round(sum(1 for t in title_analyses if t["has_year"]) / total * 100, 1),
        "pct_list_format": round(sum(1 for t in title_analyses if t["has_list_format"]) / total * 100, 1),
        "pct_colon": round(sum(1 for t in title_analyses if t["has_colon"]) / total * 100, 1),
        "pct_pipe": round(sum(1 for t in title_analyses if t["has_pipe"]) / total * 100, 1),
        "pct_exclamation": round(sum(1 for t in title_analyses if t["has_exclamation"]) / total * 100, 1),
    }

    structure_counter = Counter()
    for t in title_analyses:
        for s in t["structures"]:
            structure_counter[s] += 1
    aggregate_title["structures"] = {k: round(v/total*100, 1) for k, v in structure_counter.most_common()}

    power_category_counter = Counter()
    for t in title_analyses:
        for cat in t["power_words"]:
            power_category_counter[cat] += 1
    aggregate_title["power_word_categories"] = {k: round(v/total*100, 1) for k, v in power_category_counter.most_common()}

    word_counter = Counter()
    stop_words = {"the", "a", "an", "is", "it", "in", "on", "at", "to", "for", "of", "and", "or", "but", "with", "you", "your", "my", "this", "that", "i", "me", "we", "how", "what", "why", "do", "does", "can", "will", "be", "are", "was", "not", "no", "so", "if"}
    for v in processed:
        words = re.findall(r'[a-zA-Z]{3,}', v["title"].lower())
        for w in words:
            if w not in stop_words:
                word_counter[w] += 1
    aggregate_title["common_title_words"] = word_counter.most_common(30)

    # --- Score user's title if provided ---
    my_title_score = None
    if args.my_title:
        my_analysis = analyze_title(args.my_title)
        score = 0
        feedback = []

        if 40 <= my_analysis["length_chars"] <= 70:
            score += 15
            feedback.append("Title length is in the optimal range (40-70 chars)")
        elif my_analysis["length_chars"] < 30:
            feedback.append("Title is too short -- aim for 40-70 characters")
        elif my_analysis["length_chars"] > 80:
            feedback.append("Title may be too long -- could get truncated in search results")
        else:
            score += 10
            feedback.append("Title length is acceptable but could be optimized")

        keyword_lower = args.keyword.lower()
        title_lower = args.my_title.lower()
        if keyword_lower in title_lower:
            score += 20
            if title_lower.startswith(keyword_lower) or title_lower[:20].find(keyword_lower) >= 0:
                score += 5
                feedback.append("Keyword appears early in title -- great for SEO")
            else:
                feedback.append("Keyword is present in title")
        else:
            kw_words = keyword_lower.split()
            matches = sum(1 for w in kw_words if w in title_lower)
            if matches > 0:
                score += 10
                feedback.append(f"Partial keyword match ({matches}/{len(kw_words)} words)")
            else:
                feedback.append("WARNING: Keyword not found in title -- critical for SEO")

        if my_analysis["has_number"]:
            score += 10
            feedback.append("Contains a number -- increases click-through rate")

        if my_analysis["power_words"]:
            score += 10
            cats = list(my_analysis["power_words"].keys())
            feedback.append(f"Uses power words ({', '.join(cats)})")
        else:
            feedback.append("Consider adding power words for emotional impact")

        if my_analysis["structures"]:
            score += 10
            feedback.append(f"Uses proven structure: {', '.join(my_analysis['structures'])}")
        else:
            feedback.append("Consider using a proven structure (how-to, listicle, question)")

        if my_analysis["has_brackets"]:
            score += 5
            feedback.append("Uses brackets -- can boost CTR by 33%")

        if my_analysis["has_caps_word"]:
            score += 5
            feedback.append("Strategic use of CAPS for emphasis")

        top_10_titles = [v["title"] for v in processed[:10]]
        common_patterns_in_top = set()
        for t in top_10_titles:
            ta = analyze_title(t)
            for s in ta["structures"]:
                common_patterns_in_top.add(s)

        matching_patterns = set(my_analysis["structures"]) & common_patterns_in_top
        if matching_patterns:
            score += 10
            feedback.append(f"Matches top-performer patterns: {', '.join(matching_patterns)}")

        my_title_score = {
            "title": args.my_title,
            "score": min(score, 100),
            "analysis": my_analysis,
            "feedback": feedback,
        }

    # --- Build output ---
    output = {
        "keyword": args.keyword,
        "analyzed_at": datetime.now(timezone.utc).isoformat(),
        "total_videos_analyzed": len(processed),
        "aggregate_title_analysis": aggregate_title,
        "top_tags": all_tags.most_common(50),
        "tag_stats": {
            "unique_tags": len(all_tags),
            "avg_tags_per_video": round(sum(v["tag_count"] for v in processed) / len(processed), 1) if processed else 0,
        },
        "top_videos": [
            {
                "title": v["title"],
                "video_id": v["video_id"],
                "views": v["views"],
                "engagement_rate": v["engagement_rate"],
                "channel": v["channel"],
                "tag_count": v["tag_count"],
                "title_analysis": v["title_analysis"],
                "description_first_line": v["description_first_line"],
            }
            for v in processed[:20]
        ],
        "all_videos": processed,
        "my_title_score": my_title_score,
        "quota_used": {
            "total_estimated": quota_used,
        },
    }

    # --- Save ---
    safe_kw = re.sub(r'[^a-zA-Z0-9_-]', '_', args.keyword)[:100]
    date_str = datetime.now().strftime("%Y%m%d")
    output_dir = args.output_dir or f"yt_optimize_{safe_kw}_{date_str}"
    os.makedirs(output_dir, exist_ok=True)

    output_file = os.path.join(output_dir, "title_tag_data.json")
    try:
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(output, f, indent=2, ensure_ascii=False)
    except (IOError, OSError) as e:
        print(f"ERROR: Could not write output file: {e}")
        sys.exit(1)

    print(f"\nData saved to: {output_file}")
    print(f"Videos analyzed: {len(processed)}")
    print(f"Unique tags found: {len(all_tags)}")
    if my_title_score:
        print(f"Your title score: {my_title_score['score']}/100")
    print(f"Estimated quota used: ~{quota_used} units")


if __name__ == "__main__":
    main()

Step 4: Install Dependencies

pip3 install google-api-python-client

Step 5: Run the Script

YT_API_KEY=API_KEY python3 /tmp/_yt_title_optimizer_XXXX.py "KEYWORD" [--my-title "User Title"]

Step 6: Clean Up

rm -f /tmp/_yt_title_optimizer_XXXX.py

Step 7: Read the Data

Read the generated title_tag_data.json file.

Step 8: Generate the Optimization Report

Write a report to the output directory as title_tag_report.md:

# Title & Tag Optimization: [Keyword]
*Analyzed [date] | [N] videos*

## Your Title Score (if provided)
**Score: [X]/100**
| Criterion | Status |
|-----------|--------|
[Feedback items as table rows]

### Improved Title Suggestions
Generate 5-10 optimized title variations based on:
- Top-performing patterns from the data
- Power words that work in this niche
- Optimal length (40-70 chars)
- Keyword placement (front-loaded)

## Keyword Analysis
| Metric | Value |
|--------|-------|
| Videos Analyzed | |
| Avg Title Length | chars / words |
| Avg Tags per Video | |

## Top-Performing Titles
| # | Title | Views | Engagement | Key Patterns |
|---|-------|-------|------------|--------------|
[Top 10 titles with analysis]

## Title Pattern Analysis
### What Works for "[Keyword]"

**Title Structures:**
| Structure | Usage % | Avg Views |
|-----------|---------|-----------|
[listicle, how-to, question, comparison, etc.]

**Power Words:**
| Category | Usage % | Top Words |
|----------|---------|-----------|

**Formatting Elements:**
| Element | Usage % | Impact |
|---------|---------|--------|
[numbers, brackets, caps, emoji, year, etc.]

### Winning Title Formulas
Based on top performers, these formulas work best for this keyword:
1. [Formula 1 with example]
2. [Formula 2 with example]
3. [Formula 3 with example]

## Optimized Tag Set
Ordered by priority:

### Primary Tags (use these first)
[Top 10 most-used tags]

### Secondary Tags
[Next 10 tags]

### Long-tail Tags
[Suggested long-tail variations]

### Recommended Tag Set (copy-paste ready)
[Comma-separated complete tag set optimized for the keyword]

## Description SEO Template
Based on top performers' first lines:

[Template with placeholders]


### Top Description First Lines
| Video | First Line |
|-------|------------|

## Hashtag Recommendations
Top hashtags to use based on video data.

## Quick-Reference Checklist
- [ ] Title is 40-70 characters
- [ ] Keyword appears in first 5 words
- [ ] Contains a number or power word
- [ ] Uses proven structure (how-to/listicle/question)
- [ ] Tags include primary + secondary + long-tail
- [ ] Description first line contains keyword

## Quota Usage
| Operation | Units |
|-----------|-------|

Step 9: Report Completion

Tell the user:

  • Output folder path
  • Title score (if they provided a title)
  • Top 3 recommended title variations
  • Tag count extracted
  • Quota used
Related skills

More from nikhilbhansali/youtube-data-skills

Installs
2
First Seen
Mar 30, 2026