Three weeks after launching devpub, I got a notification I wasn't expecting: a pull request from someone I'd never talked to.
Harish / @harishteens had forked the repo, read the issues, picked one that I'd been putting off for weeks, and built a complete solution. Tests included. Design decisions documented. Edge cases handled.
The feature? devpub upload. The one command that should have existed from day one but couldn't, because the Forem API literally doesn't support it.
No more opening dev.to/new in the browser just to drag an image and copy the URL.
The problem How devpub upload works The authentication problem What Harish built Try it What's next
When I launched devpub in v0.1, the goal was to replace the Dev.to web editor entirely. Write locally, push with one command, track analytics. Done.
But there was a gap. Every time I wrote an article with diagrams or a cover image, I had to: Open dev.to/new in the browser Click the image upload button Select the file Wait for upload Copy the URL Paste it into my local markdown Close the tab
The reason I hadn't built this: the Forem API V1 has no image upload endpoint. It doesn't exist. You can set coverimage in article frontmatter, but only to a URL that already exists somewhere. The API cannot create that URL.
I filed issue #11 with a note saying "this requires reverse-engineering the web editor's upload mechanism" and moved on to other features.
The Dev.to web editor uploads images to POST /imageuploads. It's a multipart form submission, same as any file upload. But it's not authenticated with your API key. It uses your browser session.
The --markdown flag is the one I use most. Write your article with , then run devpub upload figures/.png --markdown and paste the output directly over your local references.
Here's why this feature sat undone for three weeks. The /imageuploads endpoint requires: A DevtoForemSession cookie (your login session) A CSRF token (anti-forgery protection) An Origin header matching https://dev.to
So devpub needs two extra credentials beyond your API key. They live in .devpub/.env (which devpub init gitignores by default):
If you run devpub upload without setting these, it doesn't just fail with a cryptic error. It shows you exactly where to find them:
One deliberate design choice: these are login credentials, not a scoped token. The .env file is gitignored, and when they expire (you'll get a 401 or 403), you just re-copy them. devpub tells you that upfront rather than leaving you to guess why uploads suddenly stopped working.
| File | Role | |------|------| | src/devpub/api/uploads.py | ImageUploader class with all HTTP logic | | src/devpub/cli/images.py | Click command + Rich table output | | tests/testuploads.py | 29 tests, all HTTP-mocked with respx |
Three things stood out in his implementation: Fail before the network. File existence, extension validation, and size check (25 MB limit) all happen before any HTTP request fires. A typo in a filename costs zero network round-trips. Flexible response parsing. Dev.to's upload response isn't documented, so the extracturl method handles every response shape that's been observed: links.url, image.url, images[0], a raw string. If none match, it raises with the full response body included. A wrong URL silently landing in your article would be worse than a loud error. Cookie flexibility. You can paste the full cookie header (a=1; b=2; DevtoForemSession=xyz) or just the session value. Both work. Because copying one value out of DevTools is easy to get wrong.
The PR description was thorough. Design rationale for keeping ImageUploader separate from DevtoClient (different auth models shouldn't share a class). Explicit call-out that tests pin the request shape, not the live response. A suggestion to smoke-test against a real session before release.
