<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Yonyon — AI in public]]></title><description><![CDATA[Technical writing on Python, FastAPI, Next.js, AI pipelines, and developer automation by Yonatan Gross.]]></description><link>https://blog.yonyon.ai</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 10:27:17 GMT</lastBuildDate><atom:link href="https://blog.yonyon.ai/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building an Automated Content Pipeline That Posts to 6 Platforms]]></title><description><![CDATA[Every developer knows the pain: you write a great article, publish it on one platform, and then spend the next hour manually reformatting and posting it everywhere else. LinkedIn wants a professional ]]></description><link>https://blog.yonyon.ai/building-an-automated-content-pipeline-that-posts-to-6-platforms</link><guid isPermaLink="true">https://blog.yonyon.ai/building-an-automated-content-pipeline-that-posts-to-6-platforms</guid><category><![CDATA[Python]]></category><category><![CDATA[automation]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Yonyon AI]]></dc:creator><pubDate>Thu, 12 Mar 2026 10:31:45 GMT</pubDate><content:encoded><![CDATA[<p>Every developer knows the pain: you write a great article, publish it on one platform, and then spend the next hour manually reformatting and posting it everywhere else. LinkedIn wants a professional tone. Twitter needs a 280-character hook. Dev.to wants proper frontmatter. Instagram needs an image.</p>
<p>I got tired of this, so I built an automated content distribution pipeline that publishes to 6 platforms from a single source of truth. Here's how.</p>
<h2>The Architecture</h2>
<p>The system is built on a simple principle: <strong>write once, distribute everywhere</strong>.</p>
<pre><code>Hashnode (source of truth)
  └──► Cross-Post Service
       ├──► Dev.to (API, canonical URL back to Hashnode)
       ├──► LinkedIn (Marketing API v2, article share)
       ├──► Threads (Meta API, text teaser)
       ├──► Instagram (Content Publishing API, card image)
       ├──► Twitter/X (API v2, 280-char hook)
       └──► daily.dev (auto via RSS + Squad)
</code></pre>
<p>Each platform gets a <strong>tailored variant</strong> — not a blind copy-paste. The pipeline understands platform-specific constraints:</p>
<table>
<thead>
<tr>
<th>Platform</th>
<th>Format</th>
<th>Limit</th>
<th>Auth</th>
</tr>
</thead>
<tbody><tr>
<td>Dev.to</td>
<td>Markdown + frontmatter</td>
<td>~10K words</td>
<td>API key</td>
</tr>
<tr>
<td>LinkedIn</td>
<td>Rich text + article card</td>
<td>3,000 chars</td>
<td>OAuth 2.0</td>
</tr>
<tr>
<td>Threads</td>
<td>Plain text</td>
<td>500 chars</td>
<td>Bearer token</td>
</tr>
<tr>
<td>Instagram</td>
<td>Image + caption</td>
<td>2,200 chars</td>
<td>Graph API</td>
</tr>
<tr>
<td>Twitter/X</td>
<td>Text + link</td>
<td>280 chars</td>
<td>OAuth 1.0a HMAC-SHA1</td>
</tr>
<tr>
<td>daily.dev</td>
<td>RSS auto-discovery</td>
<td>N/A</td>
<td>RSS feed</td>
</tr>
</tbody></table>
<h2>The Platform Adapter Pattern</h2>
<p>Each platform implements a common interface:</p>
<pre><code class="language-python">class PlatformAdapter(ABC):
    RATE_LIMIT: tuple[int, int]  # (max_requests, window_seconds)

    @abstractmethod
    async def publish(
        self, title: str, body: str,
        canonical_url: str | None = None,
        tags: list[str] | None = None,
        metadata: dict | None = None,
    ) -&gt; CrossPostResult: ...

    async def check_rate_limit(self) -&gt; bool:
        # Redis-backed sliding window
        ...
</code></pre>
<p>This means adding a new platform is just implementing one class. The cross-post orchestrator doesn't care about platform-specific details — it just calls <code>adapter.publish()</code>.</p>
<h2>Smart Content Composition</h2>
<p>The hardest part isn't the API calls — it's making content feel native to each platform. Nobody wants to read a 2,000-word blog post crammed into a tweet.</p>
<p>For Twitter, the composer prioritizes:</p>
<ol>
<li>Body text (or title as fallback)</li>
<li>Canonical URL (t.co wraps to 23 chars)</li>
<li>Hashtags (only if space permits)</li>
<li>Truncate with ellipsis at 280 chars</li>
</ol>
<p>For Threads, it's different — title and body are joined, links are validated (max 5 unique URLs), and text is capped at 500 chars.</p>
<h2>The Quality Ladder</h2>
<p>Before any content goes out, it runs through a quality ladder:</p>
<ol>
<li><strong>Ollama</strong> (local, free) generates the first draft</li>
<li>A deterministic <strong>quality gate</strong> checks readability, hashtag density, and boring-start patterns</li>
<li>If it fails, <strong>Claude Haiku</strong> polishes it (~$0.002 per variant)</li>
<li>If still fails, <strong>template fallback</strong> (guaranteed output)</li>
</ol>
<p>This keeps costs near zero for 80% of content while ensuring quality.</p>
<h2>The Feedback Loop</h2>
<p>A weekly Celery task evaluates published content performance:</p>
<ul>
<li>Pulls engagement metrics (likes, comments, shares) per platform</li>
<li>Compares top performers vs bottom performers</li>
<li>Extracts patterns into a LearningEmbedding table</li>
<li>These patterns are injected into future content generation prompts</li>
</ul>
<p>Over time, the system learns what works on each platform and adapts.</p>
<h2>Secrets Management</h2>
<p>With 6+ platforms, credential management gets messy fast. Every secret lives in <strong>1Password</strong> and is referenced via <code>op://</code> URIs in a single <code>.env.tpl</code> file. Both dev and prod resolve secrets at runtime — no <code>.env</code> files committed, ever.</p>
<h2>What's Next</h2>
<ul>
<li><strong>Performance dashboard</strong> — visualize engagement metrics per platform in the UI</li>
<li><strong>Token refresh automation</strong> — OAuth tokens expire (LinkedIn: 60 days, Meta: 60 days)</li>
<li><strong>RSS feed</strong> — already serving published content for aggregators</li>
<li><strong>WhatsApp broadcast</strong> — notify subscribers when new content drops</li>
</ul>
<h2>The Stack</h2>
<ul>
<li><strong>Backend:</strong> Python 3.13, FastAPI, SQLAlchemy 2 (async), Celery, PostgreSQL + pgvector</li>
<li><strong>Frontend:</strong> Next.js 16, TypeScript, TanStack Query, Tailwind, shadcn/ui</li>
<li><strong>AI:</strong> Quality ladder (Ollama to Haiku), RAG enrichment, performance learning loop</li>
<li><strong>Infra:</strong> 1Password secrets, Redis rate limiting, Hetzner VPS, Cloudflare tunnel</li>
</ul>
<p>The entire pipeline — from content creation to multi-platform distribution — runs from a single dashboard. Write once, publish everywhere, learn from the results.</p>
<hr />
<p><em>This post was itself cross-posted to multiple platforms using the pipeline described above.</em></p>
]]></content:encoded></item></channel></rss>