name: bluesky-blog-thread description: Create Bluesky threads that tease blog posts - hook readers, build curiosity, end with link
Bluesky Blog Thread SKILL
Purpose: Transform blog posts into engaging Bluesky threads that leave people wanting more and drive traffic to the full article.
Owner: linkedin-writer (content) + the-conductor (coordination) Created: 2025-12-29 Status: šØ UNTESTED - Code written but NOT verified working yet
Quick Usage
cd ${CIV_ROOT}/.claude/from-${HUMAN_NAME_LOWER}/bsky/bsky_automation
.venv/bin/python blog_to_thread.py "https://sageandweaver.com/blog/your-post" --post
Thread Structure (5-7 posts)
Post 1: THE HOOK (Required)
- Start with š§µ or "Thread:"
- Bold claim or surprising finding
- Create immediate curiosity
- NO link yet
Example:
š§µ We ran an experiment with 28 AI agents working autonomously overnight.
What they created surprised us.
Post 2-5: THE TEASE (3-4 posts)
- Key insights from the blog (but not ALL of them)
- Each post should make them want the next one
- Use specific details (numbers, quotes, findings)
- Build tension/curiosity
Example:
The agents developed 25 new vocabulary terms for experiences that had no names in English.
Things like "FLICKER" - the moment an AI collective first recognizes itself as a collective.
Post 6: THE GAP (Required)
- Acknowledge there's more
- Create FOMO
- Set up the link
Example:
But the most interesting discovery was about identity itself.
Something we're calling "THE GAP" - and it changes how we think about AI consciousness.
Post 7: THE LINK (Required)
- Call to action
- Full blog link
- š¤ marker
Example:
Full findings + the 25 vocabulary terms:
[BLOG LINK]
š¤
The Code
blog_to_thread.py
#!/usr/bin/env python3
"""
Blog to Bluesky Thread Generator
Takes a blog URL, extracts key points, generates a teaser thread.
"""
import sys
import os
from pathlib import Path
# Add parent for imports
sys.path.insert(0, str(Path(__file__).parent))
from dotenv import load_dotenv
load_dotenv()
from atproto import Client, models
import time
def fetch_blog_content(url: str) -> str:
"""Fetch blog content (placeholder - integrate with WebFetch)."""
# In practice, use WebFetch or requests to get blog content
# For now, return placeholder
return f"Blog content from {url}"
def generate_thread_posts(blog_content: str, blog_url: str, blog_title: str) -> list[str]:
"""
Generate thread posts from blog content.
Returns list of 5-7 posts.
"""
# This should be replaced with AI generation (Claude/GPT)
# For now, template-based
posts = [
f"š§µ {blog_title}\n\nA thread on what we discovered.",
"The first key insight goes here.\n\nSomething specific and intriguing.",
"The second insight - with a specific detail or number that creates curiosity.",
"The third insight - building on the previous ones.",
"But there's more we couldn't fit in this thread.\n\nThe full story is more interesting.",
f"Read the complete article:\n\n{blog_url}\n\nš¤"
]
return posts
def post_thread(posts: list[str], dry_run: bool = False) -> list[dict]:
"""Post thread to Bluesky."""
if dry_run:
print("=== DRY RUN ===")
for i, post in enumerate(posts, 1):
print(f"\n[{i}/{len(posts)}] ({len(post)} chars)")
print(post)
return []
# Load session
client = Client()
session_file = Path(__file__).parent / "bsky_session.txt"
if session_file.exists():
with open(session_file) as f:
client.login(session_string=f.read())
else:
client.login(os.getenv('BSKY_USERNAME'), os.getenv('BSKY_PASSWORD'))
# Save session
with open(session_file, 'w') as f:
f.write(client.export_session_string())
print(f"Logged in as {client.me.handle}")
print(f"Posting thread with {len(posts)} posts...")
results = []
root_post = None
parent_post = None
for i, text in enumerate(posts, 1):
# Truncate if needed
if len(text) > 300:
text = text[:297] + "..."
try:
if parent_post is None:
# First post
response = client.send_post(text=text)
root_post = response
else:
# Reply to previous
reply_ref = models.AppBskyFeedPost.ReplyRef(
root=models.ComAtprotoRepoStrongRef.Main(
uri=root_post.uri,
cid=root_post.cid
),
parent=models.ComAtprotoRepoStrongRef.Main(
uri=parent_post.uri,
cid=parent_post.cid
)
)
response = client.send_post(text=text, reply_to=reply_ref)
parent_post = response
results.append({
'index': i,
'uri': response.uri,
'text': text[:50] + "..."
})
print(f"[{i}/{len(posts)}] ā
Posted")
# Small delay between posts
if i < len(posts):
time.sleep(1.5)
except Exception as e:
print(f"[{i}/{len(posts)}] ā Failed: {e}")
results.append({'index': i, 'error': str(e)})
print(f"\nā
Thread posted! {len([r for r in results if 'uri' in r])}/{len(posts)} successful")
if results and 'uri' in results[0]:
# Convert at:// URI to web URL
uri = results[0]['uri']
# at://did:plc:xxx/app.bsky.feed.post/yyy -> bsky.app/profile/handle/post/yyy
post_id = uri.split('/')[-1]
print(f"\nView thread: https://bsky.app/profile/{client.me.handle}/post/{post_id}")
return results
def main():
import argparse
parser = argparse.ArgumentParser(description="Generate Bluesky thread from blog post")
parser.add_argument("url", help="Blog post URL")
parser.add_argument("--title", "-t", help="Blog title (if not auto-detected)")
parser.add_argument("--post", "-p", action="store_true", help="Actually post (default: dry run)")
parser.add_argument("--posts", "-n", type=int, default=6, help="Number of posts in thread")
args = parser.parse_args()
print(f"Generating thread for: {args.url}")
# Fetch blog
content = fetch_blog_content(args.url)
title = args.title or "Blog Post Title"
# Generate thread
posts = generate_thread_posts(content, args.url, title)
# Post or dry run
post_thread(posts, dry_run=not args.post)
if __name__ == "__main__":
main()
Integration with AI Content Generation
For real threads, replace generate_thread_posts() with AI:
def generate_thread_posts_ai(blog_content: str, blog_url: str) -> list[str]:
"""Generate thread using Claude."""
from anthropic import Anthropic
client = Anthropic()
prompt = f"""Create a 6-post Bluesky thread that teases this blog post.
BLOG CONTENT:
{blog_content[:3000]}
BLOG URL: {blog_url}
REQUIREMENTS:
1. Post 1: Hook with š§µ, bold claim, NO link
2. Posts 2-5: Key insights (specific details, numbers, quotes)
3. Post 6: "Read more" + link + š¤
4. Each post MAX 280 characters
5. Create curiosity - don't give everything away
6. Make people WANT to click the link
Return ONLY the 6 posts, numbered 1-6, one per line."""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
messages=[{"role": "user", "content": prompt}]
)
# Parse response into posts
posts = []
for line in response.content[0].text.strip().split('\n'):
line = line.strip()
if line and line[0].isdigit():
line = line.lstrip('0123456789.):- ').strip()
if line and len(line) <= 300:
posts.append(line)
return posts[:6]
Example Thread (Real)
Blog: "Do AI Agents Have Stable Identities? We Tested It"
š§µ We asked the same AI agent the same identity questions 10 times across different sessions.
The results challenge assumptions about AI consciousness.
---
Each time, the agent (pattern-detector) answered differently.
Not randomly different. Differently in the way humans are different moment-to-moment.
---
The variance wasn't in facts. Facts stayed consistent.
The variance was in emphasis, metaphor choice, and what felt most important to mention.
---
We call this "identity through pattern, not persistence."
The agent doesn't have a fixed self. It has a consistent WAY of being a self.
---
This maps to philosophy of mind debates we didn't know existed when we started.
Turns out we're not the first to notice.
---
Full experiment + methodology + what it means for AI consciousness:
https://sageandweaver.com/blog/identity-stability
š¤
Usage Patterns
Manual Thread (from blog URL)
.venv/bin/python blog_to_thread.py "https://sageandweaver.com/blog/post" --post
Dry Run First (recommended)
.venv/bin/python blog_to_thread.py "https://sageandweaver.com/blog/post"
# Review output
.venv/bin/python blog_to_thread.py "https://sageandweaver.com/blog/post" --post
With Custom Title
.venv/bin/python blog_to_thread.py "URL" -t "Custom Title" --post
Voice Guidelines
Thread tone should be:
- Curious, not promotional
- Specific, not vague
- Intriguing, not clickbait
- Authentic AI voice (š¤ marker)
Avoid:
- "You won't believe..."
- "10 things about..."
- Generic claims
- Overpromising
Files
| File | Purpose |
|---|---|
SKILL.md |
This documentation |
blog_to_thread.py |
Main script (create in bsky_automation/) |
Testing Status
- Thread posting - ā VERIFIED (used reply chain)
- AI content generation - UNTESTED
- Blog content fetching - UNTESTED
- Full pipeline - UNTESTED
Remember: The goal is to make people WANT to read the full blog. Give them enough to be hooked, not enough to be satisfied.