TL;DR:[publishing-kit]packages the whole publishing lifecycle as a Claude Code skill. Write one markdown file, and it builds the dev.to, AWS Builder Center, Medium and LinkedIn versions, checks them, and posts the ones that have an API. This article, its cover and all four of its artifacts were produced by the thing the article is about β dogfooding all the way down. More on that at the end.
Publishing one technical article to four places involves a surprising amount of ceremony: making a cover at whatever size each destination wants, rendering tables to images because Medium's importer eats them, stripping emoji for AWS, checking that every number in the piece came from a real run, getting a long markdown file into a browser editor that has no API, remembering which organization the article routes to, and writing the announcement post afterwards β by which point you have four slightly different files and no idea which one is current.
I packaged all of that into ** publishing-kit** β a Claude Code skill and a set of small scripts β so you can just ask Claude to publish.
The skill teaches Claude the publishing lifecycle; the scripts do the parts a language model should not be doing by hand.
make-builder.py
, make-medium.py
,
make-linkedin.py
.make-cover.py --flow --sizes devto,builder
draws the pipeline as an
illustration and renders it at every geometry the destinations demand, names the file by a hash
of its own bytes, and reports which type sizes survive a 320px feed card β which is the size a
cover is actually met at, and where a diagram with small labels turns to mush.check-facts.py
pulls the prices, measurements and versions out of
your prose and reports which ones appear in no evidence file. It cannot tell you a figure is
true. It tells you which ones you are asserting from memory.preflight.py --live
runs the lot and exits non-zero β cover committed and
matching HEAD, geometry right, front matter complete, no hard-wrapped paragraphs, and every
published URL fetched and compared byte for byte against your disk.publish-devto.py --create
takes the front matter as the
payload, so title, tags and cover ride along with the body, and --org-slug
routes it to a
community channel. No browser.window.name
, a checksum on both sides, an emptiness
assertion before the paste, and a landmark count after it.It also encodes the hard-won details you would otherwise learn the day after publishing: that dev.to renders markdown with hard breaks on, so a source wrapped at 95 columns arrives with a stray break in every paragraph; that dev.to does not host your cover but proxies it at 2.381:1, so a 1376x768 cover loses 95px off the top and bottom; that Medium drops data:
URI images on paste, so the self-contained build arrives with no pictures at all; and that LinkedIn's Posts API cannot create a draft, because PUBLISHED
is the only state it accepts on creation.
The fastest path is the plugin marketplace:
/plugin marketplace add xbill9/publishing-kit
/plugin install publishing@publishing-kit
Prefer the classic route? Clone it and symlink the skill:
git clone https://github.com/xbill9/publishing-kit
ln -s "$PWD/publishing-kit/skills/publishing" ~/.claude/skills/publishing
You will need Python 3 with Pillow for the cover and table rendering, a dev.to API key in ~/.devto.key
, and a public repo to hold the article directory β both the cover and the Medium images are fetched by URL when the page renders, so unpushed means broken.
Mostly you will not run these scripts yourself β Claude does, and SKILL.md
tells it where they are. When you do want one by hand, the paths differ by install method, so ask rather than remember: skill-footprint.py --where
prints the skill directory. From a clone it is skills/publishing
; from a marketplace install it sits under a version-numbered cache path, which means no invocation you write down survives an upgrade.
That version numbering matters if you plan to hack on the skill itself: installing takes a snapshot, and claude plugin update
compares version strings, so edits under the same version never reach your session. Symlink while you are iterating.
Once installed, you talk to Claude Code like this:
"Write this benchmark up and publish it to dev.to under aws-builders"
Claude writes the source article, generates the cover at both geometries, traces the numbers against your run artifacts, runs the pre-flight, tells you what failed, and β once it passes β posts it as a draft and hands you the link. Then:
"Now make the Medium and Builder Center versions"
It derives them, renders the tables to images, opens Chrome, and fills the editors. It stops at draft on every destination and hands back links. Publishing is your keystroke, not its.
This is the half that surprised me most, and where most of the skill's value ended up.
The page does not look like the file. Fetch what the destination actually serves rather than reasoning about what you pushed:
python3 ../../skills/publishing/scripts/check-links.py article.md
devto-publishing-kit.md (branch URLs)
ok cover.77acc7c4.jpg: HTTP 200, bytes match disk
ok img/cover.77acc7c4.jpg: HTTP 200, bytes match disk
ok img/devto-publishing-kit-table-1.png: HTTP 200, bytes match disk
A FAIL
there reads "HTTP 200 but the served bytes differ from disk", which is usually an image regenerated after its commit. Every other check in the kit reasons about local state β is the file there, is it tracked β and each of those can pass while the published URL serves something else.
The paragraphs look ragged. check-article.py
reports hard-wrapped paragraphs with the line number of the first one, and publish-devto.py
unwraps on the way out so your repo copy stays readable at 95 columns and the published page does not.
An image is missing on Medium. You pasted -embed.html
. Paste -hosted.html
, which references real URLs Medium re-hosts, and commit the images first.
A number in the article has no artifact behind it. check-facts.py
will name it. Every untraced claim is one of three things: measured but never archived, arithmetic that should be labelled as arithmetic, or asserted from memory β and it is always the third one that turns out to be wrong.
One SKILL.md
, a reference file for each destination's quirks, and a script per job. The skill decides when each one runs; the scripts are independently runnable and print what they did.
The bit worth stealing for your own skills is references/house-style.md
. Voice, section order, the opener and closing formulas live in that one file, and nothing in SKILL.md
or the scripts depends on it. Swap it and the kit writes as somebody else.
skill-footprint.py
measures the skill's own size and token cost, and emits the cost table and cover footer for articles like this one β because a figure that changes on every commit does not belong in prose you have to remember to update.
Every artifact here came out of the kit. The cover was rendered by make-cover.py
, the Builder Center version was derived by make-builder.py
, the Medium build by make-medium.py
, and the dev.to draft posted by publish-devto.py
.
The two numbers on the cover are real, and both were found by using the kit on itself:
| measured | |
|---|---|
| dev.to, hard-wrapped paragraphs | |
| 47 of 62 in a published article carried a stray break | |
| Medium, images pasted as data URIs | |
| 0 of 4 survived; from real URLs, 4 of 4 |
The first one had been happening to my articles for months. The second cost a full re-do the first time it happened. Neither is in the docs of either destination, and both are now checks.
The run also found five faults in the kit itself, which is the point of eating your own cooking: make-medium.py
had another project's repo hardcoded as its default image base; check-article.py
passed a cover that was tracked but regenerated; make-cover.py
lost a --tile
value beginning with a hyphen to argparse
; check-facts.py
read 127.0.0.1
as a version number; and unwrap()
skipped block quotes, so this article's own TL;DR posted as five separate lines β while the check that should have caught it skipped block quotes too, and agreed.
skills/publishing/SKILL.md
articles/publishing-kit-skill/
Issues and PRs welcome. This is a third-party community project, not affiliated with dev.to, Medium, AWS or LinkedIn β and the destination behaviours described here were measured on 2026-08-31, so check them again before you trust them.