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 argh 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).

Download files

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

Source Distribution

article-0.1.2.tar.gz (29.7 kB view details)

Uploaded Source

Built Distribution

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

article-0.1.2-py3-none-any.whl (33.5 kB view details)

Uploaded Python 3

File details

Details for the file article-0.1.2.tar.gz.

File metadata

  • Download URL: article-0.1.2.tar.gz
  • Upload date:
  • Size: 29.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","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}

File hashes

Hashes for article-0.1.2.tar.gz
Algorithm Hash digest
SHA256 597e49af451d005728fda033279514f12c91c65546e169466bb4cf01619155c0
MD5 9cf0a652bbe772168543a2763e7869cb
BLAKE2b-256 5268198f51dde761073bf291e8718e33e89bdbe0eb68089e4bc9106344a5fcb0

See more details on using hashes here.

File details

Details for the file article-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: article-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 33.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","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}

File hashes

Hashes for article-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 03765c1c3ce826d5b42a68ad2ea6e6f07e1065a977f1c0605604022528eb8fce
MD5 be60489df76dd591fc48178292f987da
BLAKE2b-256 9f3aa9cd589495e679f944b3ed6f3314a15824f7b17848f8bba5dbbe0aa39fd6

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.1

1 file

0.0.1

2 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