youtube-title-tag-optimizer
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
More from nikhilbhansali/youtube-data-skills
youtube-thumbnails
Download top 10 thumbnails for videos, shorts, and live streams from any YouTube channel. Creates an Obsidian-compatible index with embedded thumbnails. Use when the user wants to download thumbnails, analyze thumbnail designs, or create a visual overview of a YouTube channel's content. Accepts @handle, channel URL, or channel ID.
2youtube-topic-researcher
Research any YouTube topic or niche using YouTube Data API v3. Analyze top-performing videos, find content gaps, identify outlier videos, assess niche saturation, and generate data-driven video ideas. Use when users want to (1) Research a topic before making videos, (2) Find content gaps in a niche, (3) Validate whether a niche is worth entering, (4) Discover what's working for a keyword, (5) Find underserved subtopics, (6) Get video ideas backed by data. Requires user's YouTube Data API v3 key.
2youtube-trending-scanner
Scan what's trending right now in any YouTube niche using YouTube Data API v3. Find velocity outliers, rising channels, breakout videos, and emerging topics. Use when users want to (1) See what's trending in their niche right now, (2) Find breakout videos getting disproportionate views, (3) Discover rising channels with unusual traction, (4) Catch trends before they peak, (5) Find outdated content to remake, (6) Identify first-mover opportunities. Requires user's YouTube Data API v3 key.
2youtube-comment-miner
Mine YouTube comments for content ideas, audience questions, pain points, and monetization signals using YouTube Data API v3. Analyze comments from specific videos, top videos of a channel, or search results for a topic. Use when users want to (1) Find what their audience is asking for, (2) Mine content ideas from comments, (3) Discover audience pain points, (4) Find FAQ patterns in comments, (5) Detect monetization signals, (6) Understand audience language and sentiment. Requires user's YouTube Data API v3 key.
2youtube-competitor-analyzer
Find and analyze YouTube competitor channels using YouTube Data API v3. Discover competitors through keyword search, category matching, content similarity, and related channel discovery. Compare metrics, content strategies, and market positioning. Use when users want to (1) Find competitors for their YouTube channel, (2) Analyze competitor performance metrics, (3) Compare their channel against competitors, (4) Identify content gaps and opportunities, (5) Benchmark against similar creators, (6) Generate competitive analysis reports. Requires user's YouTube Data API v3 key.
2youtube-own-channel-analyzer
Comprehensive YouTube channel analysis using YouTube Data API v3. Analyze your own channel's performance metrics, content strategy, upload patterns, engagement rates, video performance, and growth trends. Use when users want to (1) Analyze their YouTube channel performance, (2) Get insights on video engagement and metrics, (3) Understand upload patterns and optimal posting times, (4) Identify top-performing content types, (5) Generate channel health reports, (6) Track subscriber and view growth patterns. Requires user's YouTube Data API v3 key.
2