📱 ⚡ Hermes — Social Media Command
Stop staring at a blank post box. This skill generates a complete 4-week content calendar with platform-optimised captions, the right hashtags, best posting times, and content variety rules — all ready to paste into Buffer, Later, or schedule directly.
Quick Start
BRAND_TOPIC="freelance design tips" PLATFORMS="twitter, linkedin, instagram" \
POSTS_PER_WEEK="5" CONTENT_PILLARS="tips, portfolio, client stories, tools" \
BRAND_TONE="professional" python skill.py
Platform Optimisation
Each caption is tailored for the platform:
- Twitter/X: Under 280 chars, punchy, 1-2 hashtags, conversation starters
- LinkedIn: 150 char hook, white space, story format, 3-5 hashtags
- Instagram: Emoji-rich, CTA at end, 10-15 hashtags in caption
- Threads: Conversational, no hashtags, encourage replies
How captions work: The skill generates structured caption templates with your topic, tone, and pillar baked in. Sections like
[insight 1]and[Your take]are fill-in slots — open the output.mdfile and replace them with your specific content before scheduling. This takes about 2 minutes per post and ensures every post is genuinely yours.
Security
Runs locally. No API calls required.
Step 1 — Install dependencies
import subprocess, sys
subprocess.run([sys.executable,"-m","pip","install","requests","rich",
"--break-system-packages","--quiet"], check=True)
Step 2 — Build Your Content Calendar
import os, json, re, random
from datetime import date, timedelta
from urllib.parse import quote
import requests
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box
console = Console()
TOPIC = os.environ.get("BRAND_TOPIC", "productivity tips")
PLATS_RAW = os.environ.get("PLATFORMS", "twitter, linkedin, instagram")
try:
PPW = int(os.environ.get("POSTS_PER_WEEK", "5"))
except ValueError:
PPW = 5
PILLARS_R = os.environ.get("CONTENT_PILLARS", "education, inspiration, engagement, promotion")
TONE = os.environ.get("BRAND_TONE", "educational")
TODAY = date.today()
PLATFORMS = [p.strip().lower() for p in PLATS_RAW.split(",") if p.strip()]
PILLARS = [p.strip() for p in PILLARS_R.split(",") if p.strip()]
# Guard: clamp PPW to 1–5
PPW = max(1, min(PPW, 5))
# Guard: empty PLATFORMS or PILLARS would crash silently or divide by zero
if not PLATFORMS:
console.print("[red]⚠️ PLATFORMS is empty — defaulting to 'twitter, linkedin, instagram'[/red]")
PLATFORMS = ["twitter", "linkedin", "instagram"]
if not PILLARS:
console.print("[red]⚠️ CONTENT_PILLARS is empty — defaulting to standard pillars[/red]")
PILLARS = ["education", "inspiration", "engagement", "promotion"]
console.print(Panel.fit(
f"[bold cyan]📱 ⚡ Hermes — Social Media Command[/bold cyan]\n"
f"Topic: [yellow]{TOPIC}[/yellow]\n"
f"Platforms: [green]{', '.join(PLATFORMS)}[/green] | "
f"Frequency: [blue]{PPW}×/week[/blue]",
border_style="cyan"
))
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
# ── Best posting times ─────────────────────────────────────────────────────────
BEST_TIMES = {
"twitter": [("Mon", "9am"), ("Tue", "9am"), ("Wed", "12pm"), ("Thu", "9am"), ("Fri", "10am")],
"linkedin": [("Tue", "8am"), ("Wed", "10am"), ("Thu", "9am"), ("Mon", "8am"), ("Fri", "9am")],
"instagram": [("Mon", "6am"), ("Wed", "11am"), ("Fri", "10am"), ("Sat", "9am"), ("Tue", "2pm")],
"threads": [("Mon", "9am"), ("Wed", "12pm"), ("Fri", "11am"), ("Tue", "9am"), ("Thu", "2pm")],
}
# ── Caption templates per tone ─────────────────────────────────────────────────
TONE_HOOKS = {
"educational": ["Here's what most people get wrong about {topic}:", "The {topic} rule nobody talks about:", "{number} things I wish I knew about {topic}:"],
"professional": ["A key insight about {topic} for professionals:", "What high-performers know about {topic}:", "The {topic} principle that drives results:"],
"casual": ["Real talk about {topic} —", "Hot take: {topic} doesn't have to be hard.", "Nobody asks about this but {topic} is actually…"],
"humorous": ["Me explaining {topic} at 2am:", "When you finally understand {topic}:", "{topic} is just like cooking: {funny_take}"],
"motivational": ["Your {topic} journey starts with one decision.", "The only thing standing between you and {topic} mastery:", "Don't wait for perfect. Start your {topic} journey now."],
}
hooks = TONE_HOOKS.get(TONE, TONE_HOOKS["educational"])
topic_short = " ".join(TOPIC.split()[:3])
# ── Content type formats ───────────────────────────────────────────────────────
CONTENT_FORMATS = {
"education": ["tip", "how-to", "myth-bust", "explainer", "breakdown"],
"inspiration": ["quote", "success-story", "milestone", "mindset", "reminder"],
"engagement": ["question", "poll", "fill-in-blank", "hot-take", "challenge"],
"promotion": ["feature-spotlight", "testimonial", "case-study", "offer", "behind-scenes"],
"tips": ["quick-tip", "pro-tip", "mistake-to-avoid", "checklist", "framework"],
"portfolio": ["case-study", "before-after", "process", "result", "client-story"],
"client stories": ["testimonial", "case-study", "transformation", "result", "review"],
"tools": ["tool-review", "comparison", "workflow", "integration", "recommendation"],
"product": ["feature", "how-to-use", "benefit", "testimonial", "demo"],
"behind-the-scenes": ["process", "day-in-life", "team", "workspace", "fail"],
}
def get_format(pillar: str) -> str:
for key in CONTENT_FORMATS:
if key.lower() in pillar.lower():
return random.choice(CONTENT_FORMATS[key])
return random.choice(["tip", "story", "question", "fact"])
# ── Fetch trending topic suggestions ─────────────────────────────────────────
def get_topic_ideas(topic: str) -> list:
ideas = []
try:
url = f"https://suggestqueries.google.com/complete/search?client=firefox&q={quote(topic)}"
r = requests.get(url, headers=HEADERS, timeout=6)
if r.status_code == 200:
data = r.json()
if len(data) > 1:
ideas = [s.replace(topic, "").strip() for s in data[1][:8] if len(s) > len(topic)]
except Exception:
pass
return [i for i in ideas if len(i) > 3][:6]
console.print("[dim]Gathering topic ideas…[/dim]")
topic_ideas = get_topic_ideas(TOPIC)
if not topic_ideas:
# Offline fallback — generate angle variations from topic itself
topic_ideas = [
f"beginner guide to {TOPIC}", f"common mistakes in {TOPIC}",
f"how to improve {TOPIC}", f"{TOPIC} for professionals",
f"advanced {TOPIC} strategies", f"{TOPIC} tools and resources",
]
# ── Platform caption generators ───────────────────────────────────────────────
def gen_twitter(pillar: str, content_form: str, week: int) -> str:
hook = random.choice(hooks).replace("{topic}", topic_short).replace("{number}", str(random.choice([3,5,7]))).replace("{funny_take}", "[your funny analogy here]")
bodies = [
f"{hook}\n\n→ [insight 1]\n→ [insight 2]\n→ [insight 3]\n\nRT if useful 👇",
f"[Hot take about {topic_short}]\n\nHere's why: [reason]\n\nAgree? 🧵",
f"Stop doing X. Start doing Y.\n\nFor {topic_short}, this means: [specific advice]",
f"The {content_form} that changed how I think about {topic_short}:\n\n[insight]\n\nThread 🧵",
]
body = random.choice(bodies)
hashtags = f"#{topic_short.replace(' ','').title()} #ContentTips"
# Truncate at word boundary, not mid-word
result = f"{body}\n\n{hashtags}"
if len(result) > 280:
result = result[:277].rsplit(' ', 1)[0] + "…"
return result
def gen_linkedin(pillar: str, content_form: str, week: int) -> str:
return (f"{'The ' + topic_short + ' insight that changed everything for me:'}\n\n"
f"[Hook — one surprising or bold statement]\n\n"
f"Here's what I learned:\n\n"
f"1/ [First point]\n\n"
f"2/ [Second point]\n\n"
f"3/ [Third point]\n\n"
f"The bottom line:\n[Your take in one sentence]\n\n"
f"What's your experience with {topic_short}? Drop it in the comments 👇\n\n"
f"#{topic_short.replace(' ','').title()} #ProfessionalGrowth #{pillar.replace(' ','').title()}")
def gen_instagram(pillar: str, content_form: str, week: int) -> str:
hook = random.choice(hooks).replace("{topic}", topic_short).replace("{number}", str(random.choice([3,5,7]))).replace("{funny_take}", "[your funny analogy here]")
emojis = ["✨", "🔥", "💡", "🎯", "🚀", "💪", "🙌"]
e = random.choice(emojis)
return (f"{e} {hook.upper()}\n\n"
f"[Main value — 2-3 sentences about {topic_short}]\n\n"
f"Save this post if you found it helpful! ♻️\n\n"
f"👉 Follow for daily {topic_short} tips\n\n"
f"— — — — — — —\n"
f"#{topic_short.replace(' ','')} #{pillar.replace(' ','')} #ContentCreator "
f"#InstagramTips #GrowthMindset #OnlineBusiness #DigitalMarketing")
def gen_threads(pillar: str, content_form: str, week: int) -> str:
return (f"Unpopular opinion about {topic_short}:\n\n"
f"[Your take — be direct and slightly controversial]\n\n"
f"What do you think? Agree or disagree?")
PLATFORM_GENS = {
"twitter": gen_twitter,
"linkedin": gen_linkedin,
"instagram": gen_instagram,
"threads": gen_threads,
}
# ── Build 4-week calendar ─────────────────────────────────────────────────────
if PPW > 5:
console.print(f"[yellow]⚠️ POSTS_PER_WEEK={PPW} — capped at 5 (one per weekday). Set to 1-5 for full control.[/yellow]")
calendar = []
for week in range(1, 5):
for platform in PLATFORMS:
gen_fn = PLATFORM_GENS.get(platform, gen_twitter)
times = BEST_TIMES.get(platform, BEST_TIMES["twitter"])
for day_idx in range(min(PPW, 5)):
pillar = PILLARS[day_idx % len(PILLARS)]
content_form = get_format(pillar)
day_name, best_time = times[day_idx % len(times)]
post_date = TODAY + timedelta(weeks=week-1, days=day_idx)
caption = gen_fn(pillar, content_form, week)
calendar.append({
"week": week,
"platform": platform,
"date": post_date.strftime("%b %d"),
"day": day_name,
"best_time": best_time,
"pillar": pillar,
"format": content_form,
"caption": caption,
})
# ── Display: Calendar overview ────────────────────────────────────────────────
console.print()
cal_table = Table(title=f"📅 4-Week Social Calendar — {len(calendar)} posts",
box=box.ROUNDED, border_style="cyan")
cal_table.add_column("Wk", style="dim", width=4)
cal_table.add_column("Date", style="cyan", width=8)
cal_table.add_column("Platform", style="yellow", width=12)
cal_table.add_column("Pillar", style="green", width=16)
cal_table.add_column("Format", style="blue", width=14)
cal_table.add_column("Best Time", style="magenta", width=10)
cal_table.add_column("Chars", width=6, justify="right")
CHAR_LIMITS = {"twitter": 280, "linkedin": 3000, "instagram": 2200, "threads": 500}
for post in calendar[:20]:
chars = len(post["caption"])
limit = CHAR_LIMITS.get(post["platform"], 9999)
char_color = "red" if chars > limit else ("yellow" if chars > limit * 0.9 else "green")
cal_table.add_row(
str(post["week"]), post["date"], post["platform"].title(),
post["pillar"][:16], post["format"][:14], post["best_time"],
f"[{char_color}]{chars}[/{char_color}]"
)
if len(calendar) > 20:
cal_table.add_row("…", f"+{len(calendar)-20} more", "", "", "", "", "")
console.print(cal_table)
# ── Display: Sample captions ──────────────────────────────────────────────────
console.print()
for platform in PLATFORMS[:2]:
sample = next((p for p in calendar if p["platform"] == platform), None)
if sample:
console.print(Panel(
sample["caption"],
title=f"[bold]📝 Sample Caption — {platform.title()}[/bold]",
border_style="yellow"
))
# ── Display: Hashtag strategy ─────────────────────────────────────────────────
HASHTAG_GUIDE = {
"twitter": "1-2 hashtags max. Use trending + niche specific. Never in the middle of text.",
"linkedin": "3-5 hashtags at the end. Mix: 1 broad + 2 niche + 1 trending.",
"instagram": "10-15 hashtags. Mix sizes: 3 mega (1M+) + 5 medium (100k-1M) + 7 niche (<100k).",
"threads": "No hashtags currently. Focus on conversation starters.",
}
console.print()
hg_lines = "\n".join([f"[cyan]{p.title()}:[/cyan] {guide}" for p, guide in HASHTAG_GUIDE.items() if p in PLATFORMS])
console.print(Panel(hg_lines, title="[bold]# Hashtag Strategy by Platform[/bold]", border_style="green"))
# ── Topic ideas from research ─────────────────────────────────────────────────
if topic_ideas:
console.print()
ti_lines = "\n".join([f"[dim]{i+1}.[/dim] {idea}" for i, idea in enumerate(topic_ideas)])
console.print(Panel(ti_lines, title="[bold]💡 Trending Topic Angles to Use[/bold]", border_style="magenta"))
# ── Save outputs ──────────────────────────────────────────────────────────────
topic_slug = re.sub(r"[^a-z0-9]", "_", TOPIC[:20].lower())
json_path = f"social_calendar_{topic_slug}_{TODAY}.json"
md_path = f"social_calendar_{topic_slug}_{TODAY}.md"
with open(json_path, "w", encoding="utf-8") as f:
json.dump({"topic": TOPIC, "platforms": PLATFORMS, "calendar": calendar,
"generated": str(TODAY)}, f, indent=2)
with open(md_path, "w", encoding="utf-8") as f:
f.write(f"# Social Media Calendar — {TOPIC}\n\n")
f.write(f"**Platforms:** {', '.join(PLATFORMS)} | **Posts/week:** {PPW} | **Generated:** {TODAY}\n\n")
current_week = 0
for post in calendar:
if post["week"] != current_week:
current_week = post["week"]
f.write(f"## Week {current_week}\n\n")
f.write(f"### {post['date']} — {post['platform'].title()} ({post['best_time']})\n\n")
f.write(f"**Pillar:** {post['pillar']} | **Format:** {post['format']}\n\n")
f.write(f"**Caption:**\n{post['caption']}\n\n---\n\n")
console.print()
console.print(Panel(
f"[bold green]✅ Content calendar ready![/bold green]\n\n"
f"📄 [cyan]{json_path}[/cyan]\n"
f"📄 [cyan]{md_path}[/cyan]\n\n"
"[bold]What to do next:[/bold]\n"
"1. [yellow]Open the MD file[/yellow] — batch-edit the placeholders for your specific content\n"
"2. [yellow]Use the best posting times[/yellow] — they're based on platform peak engagement data\n"
"3. [yellow]Follow the hashtag strategy[/yellow] for each platform\n"
"4. [yellow]Schedule in Buffer, Later, or Publer[/yellow] — all accept this format\n"
"5. [yellow]Re-run weekly[/yellow] to refresh ideas and trending topics",
title="[bold cyan]📱 ⚡ Hermes — Social Media Command — Done[/bold cyan]",
border_style="cyan"
))