# My Blog Now Posts to LinkedIn Automatically — But Not Without Me

> How I extended my Laravel blog to automate LinkedIn publishing with Claude, the four architectural decisions that define the system, and why keeping a human in the loop was the most important one.

- Author: Rubén Rangel (https://rubenrangel.org)
- Published: 2026-06-10
- Updated: 2026-06-10
- Language: en
- Canonical: https://rubenrangel.org/en/blog/my-blog-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:

1. When an article is published, an Eloquent observer detects it and dispatches a job.
2. 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.
3. In an admin panel I review the draft: I can edit it, approve it, reject it, or request a regeneration with feedback.
4. 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:

```php
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).*

