My Blog Now Posts to LinkedIn Automatically — But Not Without Me
How I extended a Laravel blog to generate and publish LinkedIn content with Claude, and why the most important architectural decision was keeping a human in the loop.
I have a blog and a familiar problem: writing the article is only half the work. The other half — adapting it for LinkedIn, actually posting it, not procrastinating on it — is the half that never gets done. So I automated that half. This post covers the architectural decisions I made, including two or three I wish I'd read about before starting.
Spoiler: the LinkedIn post that probably brought you here was written by Claude and published by this pipeline. I just approved it.
The flow in 30 seconds
My blog runs on Laravel 10 and publishes posts through queues. The extension does this:
- When an article is published, an Eloquent observer detects it and dispatches a job.
- The job sends the title, body, and URL to Claude's API with a fixed prompt, and saves the resulting draft as pending review. I get an email.
- In an admin panel I review the draft: I can edit it, approve it, reject it, or request a regeneration with feedback.
- Only if I approve does a second job publish to LinkedIn via its official API.
None of this is exotic. The interesting part is what I decided not to build.
Decision 1: the human is not optional
The fully automatic version was easier to build: generate and publish, no panel, no intermediate states. I ruled it out for a simple reason: the cost of an error is not symmetric. A mediocre draft that never gets published costs nothing. A post published with a hallucinated fact, a tone that isn't mine, or a technical error sitting on my professional profile costs credibility — which is exactly what the system is supposed to build.
I'd used this pattern before. At a previous job I integrated an LLM into a payment refund workflow: the model proposed the resolution, a person approved it, the system executed. It automated 70% of the effort without giving up the decision. The lesson I took away and applied here: automate the proposal, never the approval — at least when the cost of an error comes out of your reputation.
In practice, "human in the loop" is just another state in the state machine: generating → pending_review → approved → published, with exits to rejected and failed. The whole system is a deterministic workflow with a human pause in the middle.
Decision 2: don't touch the code that already works
The blog was publishing fine. The rule I set for myself: extend without modifying (yes, the Open/Closed principle applied to my own legacy code). The single coupling point is an Eloquent observer that reacts when a post transitions to published:
public function updated(Post $post): void
{
if ($post->wasChanged('published_at')) {
$this->syncIfPublished($post);
}
}
Zero lines changed in the original flow. If I remove the integration tomorrow, I delete some files and one observer registration line. That cheap reversibility is what lets you experiment without fear.
Decision 3: one API call, not an "agent"
The temptation of the moment: put an agent on everything. There's none here. The generation is one HTTP request to Anthropic's API — fixed system prompt, the article as input, text as output. No tools, no iterations, no model decisions.
Why do I insist on the distinction? Because agentic complexity is justified when the task requires variable steps or access to external systems. Writing a post from an article doesn't. A single call is cheaper, faster, trivial to test (a mocked ContentGenerator interface), and above all predictable. Choosing the boring tool is also an architectural decision.
The prompt does have careful work in it: it asks for a native post that delivers standalone value, not a teaser with a link. LinkedIn's algorithm penalizes reach for posts whose only content is an external URL; it rewards content where the value lives in the post itself. The article URL goes at the end, for anyone who wants to go deeper.
Decision 4: accepting the friction of the LinkedIn API
Two things worth knowing before integrating with LinkedIn:
There are no refresh tokens for self-serve apps. The access token lasts 60 days and programmatic renewal is reserved for approved partners. My solution: a scheduled command that checks expiration daily and sends me an email warning a week in advance; renewal is one click in the panel. Elegant? No. Sufficient for someone posting twice a week? Completely. Over-engineering avoided.
The self-serve API is the classic one. To publish as a member, the path is still POST /v2/ugcPosts with the w_member_social scope ("Share on LinkedIn" product). The newer versioned Posts API is partner territory. Knowing this upfront saves an afternoon reading documentation that doesn't apply.
Defensive details that were worth it: the publishing job is idempotent (it verifies the status is still approved before publishing, so a queue retry won't duplicate posts), failures are logged with their error message visible in the panel, and the token is stored encrypted in the database.
What this does NOT solve
Honesty first: the pipeline removes logistical friction, not the underlying work. Claude writes surprisingly decent drafts, but I still put the final voice in during review — and the source article still has to be written. Automation removes excuses, not creative effort.
I also deliberately left out optimal posting schedules, engagement analytics, and multi-network support. Each one is a branch of complexity I don't need today. When Instagram enters my plan, the interface-based design (ContentGenerator, per-platform client) already has the socket ready.
Closing
If this article reached you through LinkedIn, you've already seen the system working: that post came out of this pipeline, with my approval as the only manual step. The whole architecture fits in one sentence: domain events + queues + an LLM as writer + a human as editor.
If you're building something similar — or integrating LLMs into any flow where errors have a cost — I'd like to know how you're solving it. The conversation is open in the LinkedIn post, or in the comments below.
Stack: PHP 8.1, Laravel 10 (queues, observers), Anthropic API (Claude), LinkedIn API (OAuth 2.0 + ugcPosts).