I’ve published crier to PyPI - a command-line tool for cross-posting content to multiple platforms simultaneously.
Like a town crier announcing news across the land, crier broadcasts your content to dev.to, Ghost, WordPress, Hashnode, Medium, Bluesky, Mastodon, Threads, Telegram, Discord, and more.
Installation
pip install crier
The Problem
If you maintain a blog and want to reach audiences on multiple platforms, you face tedious manual work: copy content to dev.to, create a Bluesky post with a link, toot on Mastodon, post to your Discord server, etc. Each platform has its own interface, API, and quirks.
The Solution
Crier lets you publish once and distribute everywhere:
# Publish to multiple platforms at once
crier publish post.md --to devto --to bluesky --to mastodon --to discord
That single command:
- Publishes the full article to dev.to
- Creates a Bluesky post with title, description, and link
- Toots on Mastodon with hashtags from your tags
- Posts an announcement embed to your Discord server
Supported Platforms
| Platform | Type | Notes |
|---|---|---|
| dev.to | Blog | Full article support |
| Hashnode | Blog | Full article support |
| Medium | Blog | Publish only |
| Ghost | Blog | Full article support |
| WordPress | Blog | Self-hosted or .com |
| Buttondown | Newsletter | Email subscribers |
| Bluesky | Social | Posts with link cards |
| Mastodon | Social | Toots with hashtags |
| Threads | Social | Short posts |
| Social | Professional network | |
| Twitter/X | Social | Copy-paste mode |
| Telegram | Channel | Bot posts |
| Discord | Channel | Webhook embeds |
How It Works
Crier reads standard markdown with YAML front matter:
---
title: "My Post Title"
description: "A brief description"
tags: [python, cli, automation]
canonical_url: https://myblog.com/my-post
---
Your content here...
For blog platforms, it publishes the full article. For social platforms, it creates short announcements with links back to your canonical URL.
GitHub Actions: Fully Automated Cross-Posting
The real power comes from combining crier with GitHub Actions. Push a new post to your repo and it automatically gets cross-posted everywhere.
Quick Setup
# In your content repository
crier init-action
This creates a workflow file and sets up your API keys as GitHub secrets automatically.
How It Works
- You push a new markdown file to
posts/ - GitHub Action triggers
- Crier checks its registry for what’s already published
- Crier publishes to any platforms that are missing
- The registry is committed back to your repo
Here’s what the workflow looks like:
name: Cross-Post Content
on:
push:
branches: [main]
paths:
- 'posts/**/*.md'
jobs:
crosspost:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install crier
- name: Cross-post to platforms
env:
CRIER_DEVTO_API_KEY: ${{ secrets.CRIER_DEVTO_API_KEY }}
CRIER_BLUESKY_API_KEY: ${{ secrets.CRIER_BLUESKY_API_KEY }}
run: |
crier backfill ./posts --yes
- name: Save publication state
run: |
git config user.name "github-actions[bot]"
git add .crier/
git diff --staged --quiet || git commit -m "Update publication registry"
git push
The Registry
Crier maintains a registry (.crier/registry.yaml) tracking what’s been published:
posts:
"posts/my-article.md":
title: "My Article"
publications:
devto:
id: "12345"
url: https://dev.to/user/my-article
bluesky:
id: "abc123"
This means adding a new platform later will automatically backfill your existing content.
CLI Commands
crier publish FILE [--to PLATFORM]... # Publish to platform(s)
crier backfill PATH [--profile NAME] # Publish missing content
crier audit PATH # Show what needs publishing
crier list PLATFORM # List your articles
crier update PLATFORM ID --file FILE # Update existing
crier platforms # Show available platforms
crier config set KEY VALUE # Configure API keys
crier init-action # Set up GitHub Actions
Configuration
Set up API keys once:
crier config set devto.api_key YOUR_DEV_TO_KEY
crier config set bluesky.api_key "handle.bsky.social:app-password"
crier config set mastodon.api_key "mastodon.social:access-token"
Or define profiles for common workflows:
# ~/.config/crier/config.yaml
profiles:
blogs: [devto, hashnode]
social: [bluesky, mastodon]
all: [blogs, social]
Then use: crier publish post.md --profile all
Why I Built This
I maintain content across several platforms and got tired of the manual copy-paste workflow. Crier automates the tedious parts while keeping you in control of what goes where.
The GitHub Actions integration is the key feature: write once in your repo, push, and your content appears everywhere automatically.
Links
- PyPI: pypi.org/project/crier
- GitHub: github.com/queelius/crier
- Documentation: github.com/queelius/crier/tree/main/docs
Discussion