Saltar al contenido

Esta página todavía no está disponible en español. Estás viendo la versión en inglés.

Development

Add a feature, or move a responsibility?

Por Victor Da Luz
railsrubypostizdev-logblog-manager

I added social cross-posting to my blog manager this week. It works now, but the version I first shipped had a bug I didn’t catch until someone asked me a blunt question: “I don’t remember asking for this. Why would you implement it?” The honest answer is that I’d confused two different jobs, and the difference is worth writing down.

The setup

My blog manager already cross-posts articles to Medium, Dev.to, and Hashnode, and it announced new posts to LinkedIn through LinkedIn’s own API. I run a self-hosted Postiz instance for scheduling social posts, and the plan was to route the social channels, Bluesky, Mastodon, X, LinkedIn, through it. Keep the blog platforms native, move the social stuff to Postiz. A clean split.

The build itself was straightforward. A small client over the Postiz public API:

def create_post(payload:)
  request(Net::HTTP::Post, "/posts", payload)
end

Auth turned out to be the raw API key in the Authorization header, no Bearer prefix. The post body wants a settings: {} per channel, and the server fills in the provider type for you, so one generic payload works for every channel. I wired up a service to build the payload, a job to run it, a settings field for the key, and a “Share to social” picker on each post. Tests green, rubocop clean. I wrote it up and handed it over.

The bug I shipped

Here’s the part I got wrong. The old LinkedIn announce path was still there, running on a daily cron. My new Postiz path could also post to LinkedIn. So a single post could go out to LinkedIn twice, once the old way, once the new way. I’d even noticed the overlap while building, and instead of resolving it I left a note calling it a “future decision” and moved on.

That’s the moment the question landed. The issue was to move social posting to Postiz. I had added Postiz next to what already existed. Those sound similar but they’re not the same verb. Adding is purely constructive, write the new thing, ship it. Moving has a second half that’s easy to skip: you have to go retire the old thing. I’d done the fun half and waved at the rest.

Auditing for the rest of it

Rather than just delete the one LinkedIn path I knew about, I went looking for everything that posts to an external service anywhere in the app, every job, every cron, every controller action, every model callback. I wanted the full map, because if I’d missed one overlap I’d probably missed others.

I had. Three things surfaced:

  • The native LinkedIn announce fired automatically from three different daily sync jobs, not one. Retiring it meant touching all three, plus the model columns, the UI, and a pile of tests.
  • My new channel picker listed every connected Postiz channel. I’d connected Dev.to to Postiz for testing, so the picker would happily let me post to Dev.to through Postiz while it was already being published natively. Another double-post, one I’d built myself.
  • A post scheduled through Postiz showed “scheduled” forever, because the piece that confirms it actually published lives in a separate issue I haven’t built yet.

The picker one stung a little. I’d added the exact duplication risk I was supposedly removing, just on a different platform.

The fix, and the lesson

So I did the move properly. The native LinkedIn path is gone, the service, the columns, the UI, the tests, all of it, and a migration drops the columns. LinkedIn is now only reachable through Postiz. The picker filters out the blog platforms so it can’t duplicate the native flow. And the responsibility for “post to social” lives in exactly one place.

The thing I keep relearning: when a task says “move X to Y,” the work is mostly in finding and removing the old X, not in building Y. Building Y is visible and satisfying and gives you something to show. Removing X is invisible, it’s the absence of a double-post that nobody will ever thank you for. But it’s the actual point of the task. I skipped it because it didn’t feel like progress, and “doesn’t feel like progress” is a terrible reason to leave a footgun in the codebase.

It also made me appreciate the value of mapping the whole system before declaring a move done. I couldn’t have listed those three overlaps from memory. I had to go read every posting path in the app, and only then could I see that “the LinkedIn cron” was actually three crons and a picker bug. A move isn’t finished when the new path works. It’s finished when the old paths are gone, and you only know they’re gone if you went and looked.

What’s next

The current version posts immediately with a title and a link. I want to schedule posts for later, pull in the description, and attach a thumbnail, that’s the next issue. And the “did it actually publish” confirmation still needs its webhook. But the foundation is honest now: one responsibility, one place, no quiet duplicates.

Lecturas relacionadas