Skip to main content

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:

  1. Install a browser cookie export extension (e.g., "Get cookies.txt LOCALLY")
  2. Log into your Substack, navigate to any post
  3. Export cookies in Netscape/Mozilla format
  4. 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

  1. Fetches the post via Substack's API
  2. Downloads all images to a temp directory
  3. Converts HTML to clean Markdown (strips Substack-specific elements)
  4. Adds configured header/footer messages
  5. Suggests tags automatically via keyword matching (no API keys needed)
  6. 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)
  7. Cleans up temp files
  8. 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 pytest and 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:

  1. Create stackcross/publishers/yourplatform.py with validate_*() and publish() functions
  2. Add the platform to the CLI (cli.py): import, validation, publishing block
  3. Add config section and init wizard prompts (config.py)
  4. Add tag rules to tagger.py if the platform has specific tag conventions
  5. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stackcross-0.1.0.tar.gz (23.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stackcross-0.1.0-py3-none-any.whl (23.0 kB view details)

Uploaded Python 3

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

Hashes for stackcross-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7ef0522c28288a98c9b403977520509568deb49c7a1bff10f66bcf4bc7b4e617
MD5 c91db81bd695a9ff622844def3bdf2be
BLAKE2b-256 29d58e5c23c15cdadfac1edf3932643faeaebca4a826f2a4852075213ad16c22

See more details on using hashes here.

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

Hashes for stackcross-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a2a55d7471eb23a3f0e6ec501925d6ced9ef26e26141fc5979819b721b17bd75
MD5 42b5d7c9c0396d056a2ae54a68da75d2
BLAKE2b-256 c3b9b9f7ebee7e032808f2fc27b4ce64941976eeeb925544a3c843b5c2ee97f7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page