Cross-post Substack articles to dev.to and WordPress with one command
Project description
StackCross
Cross-post Substack articles to dev.to and WordPress with one command.
Installation
pip install stackcross
Requires Python 3.10+.
Quick Start
# Interactive setup — prompts for credentials and validates them
stackcross init
# Cross-post an article (defaults to draft on dev.to)
stackcross post https://yourpub.substack.com/p/your-post
# Post to dev.to and WordPress as public
stackcross post https://yourpub.substack.com/p/your-post --to devto,wordpress --status public
# Post only to WordPress
stackcross post https://yourpub.substack.com/p/your-post --to wordpress
# Dry run — preview without publishing
stackcross post https://yourpub.substack.com/p/your-post --dry-run
Setup
Platform Credentials
dev.to: Go to dev.to Settings > Extensions and generate an API key. If you belong to organizations, stackcross init will let you choose which one to publish under.
WordPress (self-hosted): Go to WP Admin > Users > Profile > Application Passwords, give it a name, and generate a password. Requires WordPress 5.6+ with the REST API enabled.
Configuration
Run stackcross init to create ~/.stackcross/config.toml interactively.
You can also set credentials via environment variables:
export DEVTO_API_KEY=your_key
export WORDPRESS_URL=https://yourblog.com
export WORDPRESS_USERNAME=your_username
export WORDPRESS_APP_PASSWORD=your_app_password
To update individual settings without re-running init:
stackcross config set crosspost.header "Originally published on My Blog."
stackcross config get crosspost.header
stackcross config list
Cross-post Header & Footer
Add messages to the top and bottom of every cross-posted article. Supports Markdown links — they render natively on dev.to and are converted to HTML for WordPress.
stackcross config set crosspost.header "Originally published on [My Blog](https://myblog.substack.com)."
stackcross config set crosspost.footer "Subscribe at [myblog.substack.com](https://myblog.substack.com). It's free."
Use --no-header, --no-footer, or both to skip them for a specific post.
Canonical URLs
Canonical URLs point search engines back to the original Substack post, preventing duplicate content penalties. StackCross sets the canonical URL automatically on both platforms.
dev.to: Canonical URLs are set natively via the dev.to API — no configuration needed. Every cross-posted article automatically includes a canonical link back to the original Substack post.
WordPress: Canonical URLs are set via your SEO plugin. During stackcross init, StackCross auto-detects your SEO plugin (Rank Math, Yoast, or AIOSEO). If auto-detection fails, you'll be prompted to select one manually.
You can also set the WordPress canonical meta key directly:
# Rank Math
stackcross config set wordpress.canonical_meta_key rank_math_canonical_url
# Yoast SEO
stackcross config set wordpress.canonical_meta_key _yoast_wpseo_canonical
# All in One SEO
stackcross config set wordpress.canonical_meta_key _aioseo_canonical_url
Use --no-canonical to skip setting the canonical URL on any platform.
Paywalled Substack Posts
To cross-post paywalled content you own:
- Install a browser cookie export extension (e.g., "Get cookies.txt LOCALLY")
- Log into your Substack, navigate to any post
- Export cookies in Netscape/Mozilla format
- Set the path in your config:
[substack]
cookies_path = "/path/to/cookies.txt"
Commands
stackcross init
Interactive setup wizard. Prompts for credentials and validates each one against its API. For dev.to, lists your organizations so you can choose where to publish. For WordPress, auto-detects your SEO plugin for canonical URL support.
stackcross config
Get or set individual config values.
| Subcommand | Description |
|---|---|
config set <key> <value> |
Set a config value (e.g. crosspost.header) |
config get <key> |
Get a config value |
config list |
Show all config values (sensitive values masked) |
stackcross post <substack_url> [OPTIONS]
Main command. Fetches a Substack post and cross-posts it.
| Option | Description |
|---|---|
--to |
Comma-separated targets: devto, wordpress (default: devto) |
--status |
draft or public (default: from config) |
--tags |
Comma-separated tags — skips auto-suggestion |
--title |
Override the post title |
--dry-run |
Preview output without publishing |
--keep-images |
Keep downloaded images after publishing |
--no-canonical |
Skip setting canonical URL |
--no-auto-tags |
Disable automatic tag suggestion |
--no-header |
Skip cross-post header message |
--no-footer |
Skip cross-post footer message |
stackcross validate
Checks all configured credentials. Exit code 0 if all valid, 1 if any fail.
stackcross version
Prints version number.
How It Works
- Fetches the post via Substack's API
- Downloads all images to a temp directory
- Converts HTML to clean Markdown (strips Substack-specific elements)
- Adds configured header/footer messages
- Suggests tags automatically via keyword matching (no API keys needed)
- Publishes to each target platform
- dev.to: Publishes Markdown with original image URLs; sets canonical URL natively via API; supports organization publishing
- WordPress: Publishes HTML with cover image and featured image upload; sets canonical URL via SEO plugin (Rank Math, Yoast, AIOSEO)
- Cleans up temp files
- Prints the published URLs
The canonical URL always points back to the original Substack post (unless --no-canonical is used).
Contributing
Contributions are welcome! StackCross is open source under the MIT license.
Development Setup
git clone https://github.com/marceloacosta/stackcross.git
cd stackcross
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Running Tests
pytest
Guidelines
- Open an issue first for non-trivial changes to discuss the approach
- Keep pull requests focused — one feature or fix per PR
- Add tests for new functionality
- Run
pytestand make sure all tests pass before submitting - Follow the existing code style (no linter enforced, just keep it consistent)
Adding a New Publishing Target
To add support for a new platform:
- Create
stackcross/publishers/yourplatform.pywithvalidate_*()andpublish()functions - Add the platform to the CLI (
cli.py): import, validation, publishing block - Add config section and init wizard prompts (
config.py) - Add tag rules to
tagger.pyif the platform has specific tag conventions - Update this README
Reporting Bugs
Please include:
- The command you ran
- The full error output
- Your Python version (
python --version) - Your OS
License
MIT - see LICENSE for details.
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 stackcross-0.1.0.tar.gz.
File metadata
- Download URL: stackcross-0.1.0.tar.gz
- Upload date:
- Size: 23.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ef0522c28288a98c9b403977520509568deb49c7a1bff10f66bcf4bc7b4e617
|
|
| MD5 |
c91db81bd695a9ff622844def3bdf2be
|
|
| BLAKE2b-256 |
29d58e5c23c15cdadfac1edf3932643faeaebca4a826f2a4852075213ad16c22
|
File details
Details for the file stackcross-0.1.0-py3-none-any.whl.
File metadata
- Download URL: stackcross-0.1.0-py3-none-any.whl
- Upload date:
- Size: 23.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2a55d7471eb23a3f0e6ec501925d6ced9ef26e26141fc5979819b721b17bd75
|
|
| MD5 |
42b5d7c9c0396d056a2ae54a68da75d2
|
|
| BLAKE2b-256 |
c3b9b9f7ebee7e032808f2fc27b4ce64941976eeeb925544a3c843b5c2ee97f7
|