Technology Sep 03, 2026 · 6 min read

My Dev.to CLI Got Its First Community PR. Image Uploads From Terminal.

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 d...

DE
DEV Community
by Sarvar Nadaf
My Dev.to CLI Got Its First Community PR. Image Uploads From Terminal.

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.

pip install devpub==0.3.0
devpub upload cover.png

That one command now gives you this:

  Uploaded: cover.png

              Uploaded Images
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ File         ┃ URL                                       ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ cover.png    │ https://dev-to-uploads.s3.amazonaws.com/… │
└──────────────┴──────────────────────────────────────────┘

No more opening dev.to/new in the browser just to drag an image and copy the URL.

Table of Contents

  • The problem
  • How devpub upload works
  • The authentication problem
  • What Harish built
  • Try it
  • What's next

The problem: no image endpoint in the API

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:

  1. Open dev.to/new in the browser
  2. Click the image upload button
  3. Select the file
  4. Wait for upload
  5. Copy the URL
  6. Paste it into my local markdown
  7. Close the tab

Seven steps for something that should be devpub upload figure.png.

The reason I hadn't built this: the Forem API V1 has no image upload endpoint. It doesn't exist. You can set cover_image 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.

Harish didn't move on. He figured it out.

How devpub upload works

The Dev.to web editor uploads images to POST /image_uploads. 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.

# Upload a single image
devpub upload cover.png

# Upload multiple images
devpub upload *.png

# Get ready-to-paste Markdown
devpub upload architecture.png --markdown
# Output: ![architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/...)

The --markdown flag is the one I use most. Write your article with ![](./figures/diagram.png), then run devpub upload figures/*.png --markdown and paste the output directly over your local references.

The authentication problem

Here's why this feature sat undone for three weeks. The /image_uploads endpoint requires:

  1. A _Devto_Forem_Session cookie (your login session)
  2. A CSRF token (anti-forgery protection)
  3. An Origin header matching https://dev.to

Your API key? Useless. This endpoint doesn't accept it.

So devpub needs two extra credentials beyond your API key. They live in .devpub/.env (which devpub init gitignores by default):

DEVPUB_SESSION_COOKIE=_Devto_Forem_Session=abc123...
DEVPUB_CSRF_TOKEN=a1b2c3d4...

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:

╭─ Session Credentials Required ─────────────────────────────╮
│                                                             │
│  devpub upload needs your browser session (not API key).    │
│                                                             │
│  1. Open https://dev.to/new in Chrome                       │
│  2. Press F12 → Application → Cookies → dev.to             │
│  3. Copy _Devto_Forem_Session value                         │
│  4. View page source → find <meta name="csrf-token">       │
│  5. Add both to .devpub/.env                                │
│                                                             │
╰─────────────────────────────────────────────────────────────╯

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.

What Harish built

Harish's PR wasn't a quick hack. It was a proper module with clear boundaries:

File Role
src/devpub/api/uploads.py ImageUploader class with all HTTP logic
src/devpub/cli/images.py Click command + Rich table output
tests/test_uploads.py 29 tests, all HTTP-mocked with respx

Three things stood out in his implementation:

1. 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.

2. Flexible response parsing. Dev.to's upload response isn't documented, so the _extract_url 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.

3. Cookie flexibility. You can paste the full cookie header (a=1; b=2; _Devto_Forem_Session=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.

This is what good open source contributions look like. Not just code that works, but code that explains why it works that way.

Other changes in v0.3.0

Beyond the upload feature:

  • Fixed hardcoded User-Agent: Was stuck at devpub/0.1.0. Now reads the actual version from importlib.metadata.
  • Fixed empty API key edge case: DevtoClient(api_key='') used to fall through to environment variables silently. Now it raises.
  • Repo cleanup: Removed personal draft articles from tracking, updated .gitignore for research docs and recordings.

Try it yourself

pip install devpub==0.3.0

For uploads, you need the session credentials (one-time setup):

# Add to .devpub/.env
DEVPUB_SESSION_COOKIE=your_session_cookie_here
DEVPUB_CSRF_TOKEN=your_csrf_token_here

Then:

devpub upload cover.png              # Upload and get URL
devpub upload *.png --markdown       # Ready-to-paste Markdown tags
devpub upload diagram.png --dry-run  # Validate without uploading

Full source: github.com/simplynadaf/devpub

What's next

Two follow-ups that Harish explicitly scoped out of his PR (smart -- ship the primitive first, then build on it):

  1. Auto-rewrite during push: devpub push could detect local image paths like ![](./figures/arch.png), upload them automatically, and rewrite the URLs in-place before publishing.

  2. Cover image shortcut: devpub push -f article.md --cover photo.png to upload the image and set cover_image in frontmatter in one step.

Both become trivial now that the upload primitive exists.

Shoutout

Big thanks to Harish @harishteens for the first external contribution to devpub. The PR was clean, well-tested, and properly documented. If you're looking for an open-source project to contribute to, devpub has open issues ranging from beginner-friendly to architecturally interesting.

What's your image workflow for Dev.to articles? Drag-and-drop in the browser? Hosted on GitHub? Imgur? I'd like to know if devpub upload fills a gap people feel, or if everyone's already solved this differently.

Follow me for more on AWS architecture, DevOps, and AI Infrastructure:
Portfolio | LinkedIn | Dev.to | YouTube | Email | AWS Builder Center | X

DE
Source

This article was originally published by DEV Community and written by Sarvar Nadaf.

Read original article on DEV Community
Back to Discover

Reading List