Cross-post your content to dev.to, Hashnode, Medium, and more
Project description
Crier
Cross-post your content to dev.to, Ghost, WordPress, Hashnode, Medium, Bluesky, Mastodon, Threads, Telegram, Discord, and more.
Like a town crier announcing your content to the world.
Getting Started
Quick Setup
pip install crier
cd your-blog
crier init
The init command walks you through:
- Creating the
.crier/registry directory - Detecting your content directories
- Configuring platforms with API keys
How It Works
- Your markdown posts with YAML front matter are the source of truth
.crier/registry.yamltracks what's published wherecrier auditshows what's missing or changedcrier publishoraudit --publishpublishes content
# See what needs publishing
crier audit
# Publish a file to a platform
crier publish post.md --to devto
# Publish to multiple platforms
crier publish post.md --to devto --to bluesky --to mastodon
# Bulk publish missing content (interactive)
crier audit --publish
With Claude Code
Crier is designed to work with Claude Code for AI-assisted publishing.
Install the skill with crier skill install, then just ask Claude naturally:
- "Cross-post my latest article to all platforms"
- "What articles haven't been published to Bluesky?"
- "Publish this post to Mastodon with a good announcement"
Claude automatically detects when to use the crier skill and follows the workflow: audit, select, publish (with rewrites for short-form platforms).
Installation
pip install crier
Supported Platforms
| Platform | API Key Format | Notes |
|---|---|---|
| dev.to | api_key |
Full article support |
| Hashnode | token or token:publication_id |
Full article support |
| Medium | integration_token |
Publish only (no edit/list) |
| Ghost | https://site.com:key_id:key_secret |
Full article support |
| WordPress | site.wordpress.com:token or https://site.com:user:app_pass |
Full article support |
| Buttondown | api_key |
Newsletter publishing |
| Bluesky | handle:app_password |
Short posts with link cards |
| Mastodon | instance:access_token |
Toots with hashtags |
| Threads | user_id:access_token |
Short posts (no edit support) |
| Telegram | bot_token:chat_id |
Channel/group posts |
| Discord | webhook_url |
Server announcements |
access_token |
Requires API access | |
| Twitter/X | any (copy-paste mode) |
Generates tweet for manual posting |
Platform Notes
Blog Platforms (dev.to, Hashnode, Medium, Ghost, WordPress):
- Full markdown article publishing
- Preserves front matter (title, description, tags, canonical_url)
- Best for long-form content
Newsletter Platforms (Buttondown):
- Publishes to email subscribers
- Full markdown support
- Great for content repurposing
Social Platforms (Bluesky, Mastodon, LinkedIn, Twitter, Threads):
- Creates short posts with link to canonical URL
- Uses title + description + hashtags from tags
- Best for announcing new content
Announcement Channels (Telegram, Discord):
- Posts to channels/servers
- Good for community announcements
- Discord uses webhook embeds
Manual Mode
For platforms with restrictive API access (Medium, LinkedIn, Twitter/X), you can use manual (copy-paste) mode:
# Explicit manual mode with --manual flag
crier publish post.md --to medium --manual
crier publish post.md --to linkedin --manual
# Skip auto-opening browser
crier publish post.md --to twitter --manual --no-browser
Auto-manual mode: If you configure a platform's API key to "manual", crier automatically uses manual mode:
# Configure platform for manual mode (no API key needed)
crier config set twitter.api_key manual
crier config set linkedin.api_key manual
# Now these automatically use manual mode without --manual flag
crier publish post.md --to twitter
crier publish post.md --to linkedin
Manual mode workflow:
- Formats content for the platform
- Copies it to your clipboard
- Opens the compose page in your browser
- Asks if you successfully posted
- Records to registry only if you confirm
This ensures the registry accurately reflects what's actually published.
Configuration
Crier uses two configuration files:
Global Config (~/.config/crier/config.yaml)
API keys and profiles (shared across all projects):
platforms:
devto:
api_key: your_key_here
bluesky:
api_key: "handle.bsky.social:app-password"
mastodon:
api_key: "mastodon.social:access-token"
twitter:
api_key: manual # Copy-paste mode
medium:
api_key: import # URL import mode
profiles:
blogs:
- devto
- hashnode
- medium
social:
- bluesky
- mastodon
everything:
- blogs # Profiles can reference other profiles
- social
Local Config (.crier/config.yaml)
Project-specific settings:
content_paths:
- content # Directories to scan for markdown files
site_base_url: https://yoursite.com
exclude_patterns:
- _index.md # Files to skip (Hugo section pages)
file_extensions:
- .md
- .mdx # Optional: for MDX content
default_profile: everything # Used when no --to or --profile specified
rewrite_author: claude-code # Default author for AI-generated rewrites
| Option | Purpose |
|---|---|
content_paths |
Directories to scan for content |
site_base_url |
For inferring canonical URLs |
exclude_patterns |
Filename patterns to skip |
file_extensions |
Extensions to scan (default: .md) |
default_profile |
Default platforms when none specified |
rewrite_author |
Default --rewrite-author value |
Environment Variables
Environment variables override config files:
export CRIER_DEVTO_API_KEY=your_key_here
export CRIER_BLUESKY_API_KEY="handle.bsky.social:app-password"
Markdown Format
Crier reads standard markdown with YAML front matter:
---
title: "My Amazing Post"
description: "A brief description"
tags: [python, programming]
canonical_url: https://myblog.com/my-post
published: true
---
Your content here...
Commands
crier init # Interactive setup wizard
crier publish FILE --to PLATFORM # Publish to platform(s)
crier publish FILE --to PLATFORM --manual # Manual copy-paste mode
crier audit # See what's missing/changed
crier audit --publish # Bulk publish interactively
crier audit --publish --yes # Bulk publish without prompting
crier status [FILE] # Show publication status
crier list PLATFORM # List your articles
crier config show # Show configuration
crier config set KEY VALUE # Set configuration
crier doctor # Verify API keys work
crier skill install # Install Claude Code skill
Automation
Batch Mode
Use --batch for fully automated, non-interactive publishing (CI/CD):
# Batch mode implies --yes --json, skips manual/import platforms
crier publish post.md --to devto --to bluesky --batch
crier audit --publish --batch --long-form
JSON Output
Use --json for machine-readable output:
crier publish post.md --to devto --json
crier audit --json
JSON output structure:
{
"command": "publish",
"file": "post.md",
"results": [{"platform": "devto", "success": true, "url": "..."}],
"summary": {"succeeded": 1, "failed": 0, "skipped": 0}
}
Auto-Rewrite
Use --auto-rewrite to generate short-form content using an LLM:
# Configure LLM first (see below), then:
crier publish post.md --to bluesky --auto-rewrite
Configure LLM in ~/.config/crier/config.yaml:
llm:
provider: openai # OpenAI-compatible API
base_url: http://localhost:11434/v1 # Ollama local
model: llama3
# api_key: sk-... # For cloud providers (OpenAI, Groq, etc.)
Or via environment variables:
CRIER_LLM_BASE_URL— API endpointCRIER_LLM_MODEL— Model nameCRIER_LLM_API_KEY— API key (optional for local providers like Ollama)
Bulk Operations
The audit command supports powerful filtering for targeted bulk operations:
# Post to API platforms only (skip manual/import)
crier audit --publish --yes --only-api
# Long-form only (skip bluesky, mastodon, twitter, threads)
crier audit --publish --yes --long-form
# Random sample of 5 articles
crier audit --publish --yes --sample 5
# Include changed content (default: missing only)
crier audit --publish --yes --include-changed
# Filter by path
crier audit content/post --publish --yes --only-api
# Filter by date (relative)
crier audit --since 1w --publish --yes # Last week
crier audit --since 1m --publish --yes # Last month
crier audit --since 7d --until 1d --publish --yes # 7 days ago to yesterday
# Filter by date (absolute)
crier audit --since 2025-12-01 --until 2025-12-31 --publish --yes
# Combine filters
crier audit content/post --since 1m --only-api --long-form --sample 10 --publish --yes
Filter Reference
| Filter | Description |
|---|---|
[PATH] |
Only scan specific directory |
--since |
Only content from this date (1d, 1w, 1m, 1y, or YYYY-MM-DD) |
--until |
Only content until this date |
--only-api |
Skip manual/import/paste platforms |
--long-form |
Skip short-form platforms (bluesky, mastodon, twitter, threads) |
--sample N |
Random sample of N items |
--include-changed |
Also update changed content (default: missing only) |
--batch |
Non-interactive mode (implies --yes --json, skips manual platforms) |
--json |
Output results as JSON |
Filters are applied in order: path → date → platform mode → content type → changed → sampling
Getting API Keys
dev.to
- Go to https://dev.to/settings/extensions
- Generate API key
Hashnode
- Go to https://hashnode.com/settings/developer
- Generate Personal Access Token
Medium
- Go to https://medium.com/me/settings/security
- Generate Integration Token
Bluesky
- Go to Settings → App Passwords
- Create an app password
- Use format:
yourhandle.bsky.social:xxxx-xxxx-xxxx-xxxx
Mastodon
- Go to Settings → Development → New Application
- Create app with
write:statusesscope - Use format:
instance.social:your-access-token
Twitter/X
Uses copy-paste mode - generates formatted tweet text for manual posting. No API setup required. Just set any placeholder value:
crier config set twitter.api_key manual
Ghost
- Go to Settings → Integrations → Add custom integration
- Copy the Admin API Key (format:
key_id:key_secret) - Use format:
https://yourblog.com:key_id:key_secret
WordPress
WordPress.com:
- Go to https://developer.wordpress.com/apps/
- Create an app and get OAuth token
- Use format:
yoursite.wordpress.com:access_token
Self-hosted WordPress:
- Go to Users → Profile → Application Passwords
- Create a new application password
- Use format:
https://yoursite.com:username:app_password
Buttondown
- Go to https://buttondown.email/settings/programming
- Copy your API key
- Use format:
api_key
Threads
- Create a Meta Developer account at https://developers.facebook.com/
- Create an app with Threads API access
- Get your user_id and access_token
- Use format:
user_id:access_token
Telegram
- Message @BotFather to create a bot and get the bot token
- Add your bot as admin to your channel
- Get your channel's chat_id (e.g.,
@yourchannelor numeric ID) - Use format:
bot_token:chat_id
Discord
- Go to Server Settings → Integrations → Webhooks
- Create a new webhook for your announcement channel
- Copy the webhook URL
- Use the full URL as the API key
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file crier-0.5.0.tar.gz.
File metadata
- Download URL: crier-0.5.0.tar.gz
- Upload date:
- Size: 55.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
543ff00ade9e8b357b15081596979df7b480c2a4b6ec971b6f887e05d17544ce
|
|
| MD5 |
59e756f5fdac6ad0e3aa16dbb7eddda8
|
|
| BLAKE2b-256 |
3a103a3036dfe2d9977ea30c924bc5e32947e19670c7b6ddac6880264cd7adfd
|
File details
Details for the file crier-0.5.0-py3-none-any.whl.
File metadata
- Download URL: crier-0.5.0-py3-none-any.whl
- Upload date:
- Size: 71.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e62a374a53db1cdff5edfc777eec4a34afafb53ff073fd5ebed6ce78459fc31
|
|
| MD5 |
146b24c07484ac3718314d4171453cfd
|
|
| BLAKE2b-256 |
625036ef40508e02a468e089c871099a0d4377b7bda7bcdbfbf0fc317c98d2dc
|