Skip to content
Development

The last native publisher: finishing the Postiz consolidation

By Victor Da Luz
railsrubypostizmediumdev-logblog-manager

blog-manager started life with a native integration per platform: a Medium client, a Dev.to client, a Hashnode client, a LinkedIn announcer, each with its own draft flow, sync job, and pile of status columns. Over the last few weeks I’ve been deleting them one by one and routing everything through Postiz, the self-hosted scheduler that already handles my social posts. This week the last one fell. Medium is gone - the native code, anyway - and blog-manager now talks to exactly one external system.

Why Medium went last

Dev.to was easy mode: it has a working API, Postiz has a working Dev.to provider, and migrating was mostly payload work. Medium is the opposite. Medium stopped issuing new API tokens and I never had one from before the cutoff, which means my “native Medium integration” was four hundred lines of code that could not publish anything. It wasn’t an integration. It was a museum exhibit.

There is no API path to Medium anymore. The way in, built in a separate homelab project, is a small sidecar next to Postiz: a stealth-patched headless browser holding a logged-in Medium session, driving Medium’s official Import a story feature. Postiz’s Medium provider is patched to call that sidecar instead of api.medium.com. From blog-manager’s point of view none of that machinery is visible - it just schedules a post to the Medium channel like any other.

An article whose body is a URL

The interesting design wrinkle: for Dev.to, blog-manager sends the full markdown body plus title, tags, cover, and canonical URL. For Medium it sends almost nothing - the post’s public canonical URL as the content. The bridge pastes that URL into Medium’s import box, and Medium itself fetches the page, extracts the article, and sets rel=canonical back to my site. The less I send, the less can drift. Medium’s own importer is the rendering engine, so formatting fidelity is Medium’s problem, not mine.

That made the code change small. The existing ArticlePoster service from the Dev.to migration became provider-aware: one branch builds the Dev.to payload (body + settings), the other sends the URL. Status tracking needed zero changes - both providers’ entries land in the same per-channel publish map, and the same recurring reconcile job watches for them to flip from draft to published.

Bitten twice, but faster the second time

I sent the first Medium post with empty settings, reasoning that since Medium extracts everything from the URL, there’s nothing to configure. Postiz rejected it: settings.title should not be null… settings.subtitle should not be null.

This is the exact failure mode that bit me during the Dev.to migration: Postiz validates the request against a settings DTO before the provider ever runs, and the DTO demands fields the runtime barely uses. Last time I lost a round of live-testing to it. This time I recognized the error on sight, pulled the MediumSettingsDto from the Postiz source, saw title and subtitle are required with a two-character minimum, and shipped the fix in minutes. There’s something satisfying about a bug that costs you an hour the first time and five minutes the second - it means the first hour actually bought something. (It also helped that I’d written the gotcha into my knowledge base after round one, including the instruction “read both the provider AND the settings DTO.” Past me left a note exactly where future me would trip.)

One subtlety: the title isn’t just DTO appeasement. Medium’s importer silently drops the title, and the bridge re-applies it after import from that settings field. So the required field that looked like bureaucracy is actually load-bearing.

The bonfire

After one real post round-tripped - blog-manager staged it, Postiz handed it to the bridge, the bridge imported and published it, the article came out with the right title, body, canonical link, and reference list - I deleted the native integration. Six service classes, two jobs, a webhook endpoint, an RSS poller, seven columns on posts and two on blogs, the credentials UI, a dashboard card, and a recurring sync. Net: minus 1,603 lines.

Counting the whole arc (Hashnode, LinkedIn, Dev.to, Medium), blog-manager has shed every platform client it ever had. What remains is one Postiz client, one article poster with two small provider branches, and one reconcile loop. Three different status-sync mechanisms became one.

What I’d tell past me

Delete code that can’t run. The native Medium integration sat there for months looking like a working feature. Its token could never be re-minted. Code that cannot succeed is worse than no code - it makes the UI lie.

When you hit a weird third-party failure, write it down where you’ll trip next time. The DTO-stricter-than-runtime note paid for itself in one issue.

Consolidation compounds. Each migration made the next one smaller. Dev.to required building the article path, the reconcile sweep, and the UI row. Medium reused all of it and was mostly a payload branch and a removal diff.

The one new dependency is real: all publishing now flows through my homelab Postiz instance, and Medium specifically depends on a browser session that will eventually expire. That’s a trade I made knowingly - one engine to maintain instead of four integrations to babysit, with a manual import button as the fallback if the bridge ever breaks mid-week.

Related reading

Development

Add a feature, or move a responsibility?

Adding Postiz social cross-posting looked done until a blunt question exposed a double-post bug, and a full audit of every posting path in the app found two more like it.

Read