Skip to main content

article

Tools to semi-automate the publication of articles.

Write an article once as a JSON file. Publish it to a primary platform (Substack), then syndicate it to secondary platforms (Medium, Dev.to, Hashnode) — each pointing its canonical URL back to the primary, so you get cross-platform reach without an SEO duplicate-content penalty.

pip install article

The single source of truth

The Substack public URL captured in phase 1 is the SSOT. It is stored (keyed by article slug) and injected as canonical_url into every secondary platform in phase 2, so each emits <link rel="canonical" href="…"> pointing back at the original.

author.json ──► publish-primary ──► Substack ──► live URL ─┐
                                                           │ stored as canonical_url
                                                           ▼  (pipeline_state.json, by slug)
                          syndicate-secondary ──► Medium / Dev.to / Hashnode
                                                  each canonical → Substack URL

The two phases are deliberately separate commands so you can syndicate later (e.g. +3 days) on your own schedule.

Two-command flow

# 1. Author once, then publish to the primary (records the canonical URL):
python -m article publish-primary article.json

# 2. Later — syndicate everywhere, injecting the canonical URL for SEO:
python -m article syndicate-secondary article.json

# Subset / machine-readable output:
python -m article syndicate-secondary article.json --platforms medium,dev_to --json-out

After pip install, the article console script is equivalent (article publish-primary article.json).

The article JSON

One standardized file is the single source of truth for content and which platforms to publish to — a platform is published only if it appears under platforms (see examples/article.example.json):

{
  "title": "Designing an SSOT Publishing Pipeline",
  "slug": "ssot-publishing-pipeline",
  "content_markdown": "# Designing an SSOT Publishing Pipeline\n\n…",
  "tags": ["python", "automation", "seo"],
  "platforms": {
    "substack": { "publish_as_draft": false },
    "medium":   { "publish_as_draft": true },
    "dev_to":   { "publish_as_draft": true },
    "hashnode": { "publish_as_draft": true }
  }
}

Configuration

Secrets and settings load from .env (see .env.example) — copy it and fill in what you use:

cp .env.example .env

Every platform is optional; omit a platform's secrets (or its platforms entry) to skip it.

Using it from Python

The CLI is a thin wrapper over the same functions, so everything is callable programmatically:

import asyncio
from article import load_article, publish_primary, syndicate_secondary

art = load_article("article.json")
asyncio.run(publish_primary(art))  # phase 1 → records canonical_url
asyncio.run(syndicate_secondary(art))  # phase 2 → canonical SEO everywhere

Adding a platform

Adapters are registered in an open-closed registry. To add a platform, write one async function satisfying the PublishAdapter protocol and register it — no core changes:

from article import register_adapter, PublishResult
from article.util import require_canonical_url


@register_adapter("my_platform")
async def publish(article, *, canonical_url, config, secrets):
    require_canonical_url("my_platform", canonical_url)  # SSOT is never optional
    # … call the platform's API, injecting canonical_url …
    return PublishResult.success("my_platform", url="…", canonical_url=canonical_url)

Architecture

Layer Module Responsibility
Schema + config article/config.py Pydantic Article model, .env Settings, load_article (friendly errors)
Vocabulary article/base.py PublishResult, RunSummary, platform names, exceptions
State store article/state.py MutableMapping facade over JSON, keyed by slug (the SSOT)
Registry article/registry.py PublishAdapter protocol + open-closed registry
Engine article/engine.py Orchestrates phase 1 → phase 2; canonical handoff; graceful errors
Adapters article/adapters/* One module per platform (config + secrets injected)
CLI article/__main__.py cw dispatch of the two commands

Status: the layers above are complete and tested; each adapter's actual network / Playwright call is a clearly-marked TODO stub that returns a typed placeholder result. Payload field names are verified against each platform's live API (canonicalUrl / canonical_url / originalArticleURL).

Metadata

Release files for article 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for article 0.1.3
File Size Uploaded
article-0.1.3.tar.gz 32.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for article 0.1.3
File Interpreter ABI Platform
article-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 67.0 kB

Release files / article-0.1.3.tar.gz

Download URL article-0.1.3.tar.gz
Size 32.6 kB
Tags Source
SHA-256 checksum
How to use checksums
2f50fb20ada14b3d77914b2692b9975968af215626d61155d7e6171959759e14
BLAKE2b-256 checksum
How to use checksums
68f55a152d2273c00e24e35e013a2a657faab099794e4adae2f71937e3ae518c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / article-0.1.3-py3-none-any.whl

Download URL article-0.1.3-py3-none-any.whl
Size 34.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
29355dc70eb2ea03ddc459aac85d53c306271b34febd8505d2ba885953751edf
BLAKE2b-256 checksum
How to use checksums
9fb80dcf64724056b39f819e1da3cc4e869f1a5663b52d0fe94a73f382390ee2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.1.4

2 release files

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

1 release file

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page