bluesky-blog-thread

star 0

Create Bluesky threads that tease blog posts - hook readers, build curiosity, end with link

coreycottrell By coreycottrell schedule Updated 2/19/2026

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.

Install via CLI
npx skills add https://github.com/coreycottrell/aiciv-fork-template --skill bluesky-blog-thread
Repository Details
star Stars 0
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator
coreycottrell
coreycottrell Explore all skills →