<?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>Tue, 08 Sep 2026 21:04:11 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[Making your docs site agent-readable: llms.txt, MCP, and the .well-known files that actually matter]]></title><description><![CDATA[AI agents increasingly read your docs instead of a human. If your documentation site only emits HTML for a browser, an agent has to scrape and guess. There's a better surface — and most of it is a han]]></description><link>https://blog.yonyon.ai/making-your-docs-site-agent-readable-llms-txt-mcp-and-the-well-known-files-that-actually-matter</link><guid isPermaLink="true">https://blog.yonyon.ai/making-your-docs-site-agent-readable-llms-txt-mcp-and-the-well-known-files-that-actually-matter</guid><category><![CDATA[AI]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Yonyon AI]]></dc:creator><pubDate>Mon, 15 Jun 2026 07:07:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5f965b6a40346172a86c2c1e/4da50400-3d8c-4889-a4a5-8dbbb780bc77.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI agents increasingly read your docs instead of a human. If your documentation site only emits HTML for a browser, an agent has to scrape and guess. There's a better surface — and most of it is a handful of small, standard files. Here's the full stack we ship on the OrchestKit docs site, why each piece exists, and how to verify it.</p>
<pre><code class="language-mermaid">flowchart TD
    A(["AI agent arrives"]) --&gt; B["GET /llms.txt — orient in one fetch"]
    B --&gt; C{"What does it need?"}
    C --&gt;|full docs| D["/llms-full.txt"]
    C --&gt;|one page| E["append .md (or Accept: text/markdown)"]
    C --&gt;|call an API| F["/api/openapi + /.well-known/api-catalog"]
    C --&gt;|use tools| G["MCP server /api/mcp + server-card.json"]
    C --&gt;|who are you| H["JSON-LD graph: Organization + SoftwareApplication"]
    F --&gt; I["RFC 9728: anonymous = a positive signal"]
    D --&gt; Z(["clean, structured context"])
    E --&gt; Z
    G --&gt; Z
    H --&gt; Z
    I --&gt; Z
</code></pre>
<p><em>How an agent traverses the surface</em></p>
<h2>1. llms.txt — the agent's table of contents</h2>
<p>A plain-text index at /llms.txt: what the product is, its constraints, and a link map to every machine-readable resource. Keep it under ~30k chars; put the exhaustive page list in /docs/llms.txt and the full corpus in /llms-full.txt. The win: an agent gets oriented in one fetch instead of crawling.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f965b6a40346172a86c2c1e/64c9986b-812a-463e-b6c8-694c68ac4edd.png" alt="" style="display:block;margin:0 auto" />

<p><em>Live output from /llms.txt</em></p>
<h2>2. Markdown content negotiation</h2>
<p>Append .md to any page URL (or send Accept: text/markdown) and return the raw Markdown. Agents get clean tokens; humans still get the rendered page.</p>
<h2>3. An OpenAPI spec for your read APIs</h2>
<p>Even a docs site has an API surface (search, page fetch). Publish an OpenAPI document at a predictable path so an agent can call it without reverse-engineering. Pair it with RFC 9727 — a /.well-known/api-catalog linkset that enumerates every API entry point.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f965b6a40346172a86c2c1e/93864c99-ef2b-453d-82ba-d5c37329468e.png" alt="" style="display:block;margin:0 auto" />

<p><em>RFC 9727 linkset at /.well-known/api-catalog</em></p>
<h2>4. An MCP server</h2>
<p>The Model Context Protocol lets agents call your tools natively. We expose a read-only MCP server over Streamable HTTP at /api/mcp plus a discovery server-card.json. Two tools — search docs, get a doc by id — are enough to be useful.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f965b6a40346172a86c2c1e/a3d1f93d-40a7-45a2-9f97-18415228bea7.png" alt="" style="display:block;margin:0 auto" />

<p><em>The MCP server card</em></p>
<h2>5. The .well-known identity files</h2>
<ul>
<li><p>agent-card.json (A2A): declares your agent skills.</p>
</li>
<li><p>agent-skills/index.json: the Agent Skills Discovery RFC, with a SHA-256 digest per skill so a consumer can verify it.</p>
</li>
<li><p>oauth-protected-resource (RFC 9728): if your API is anonymous, say so — an empty authorization_servers is a positive signal, not an omission.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/5f965b6a40346172a86c2c1e/0b12de88-c1b4-4865-8472-dd9c012bb958.png" alt="" style="display:block;margin:0 auto" />

<p><em>The A2A agent card</em></p>
<h2>6. JSON-LD that an entity graph can reconcile</h2>
<p>Emit a <a href="http://schema.org">schema.org</a> graph (Organization, SoftwareApplication, WebSite) linked by @id, with sameAs pointing at the registries that already verify you (GitHub, your package registry, Wikidata). One canonical Organization block, reused everywhere, so the graph never sees conflicting identifiers. Never fabricate an aggregateRating — surface real signals (e.g. GitHub stars as an InteractionCounter) instead.</p>
<h2>7. Tell crawlers the truth in robots.txt</h2>
<p>Explicitly allow the named AI crawlers you want (GPTBot, ClaudeBot, OAI-SearchBot, Google-Extended…), and emit a Content-Signal directive. Link your sitemap and a schema-map.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f965b6a40346172a86c2c1e/d00cf0a2-ce7e-4d31-9d09-7e61a31c4c01.png" alt="" style="display:block;margin:0 auto" />

<p><em>Named AI crawlers + Content-Signal</em></p>
<h2>How to verify</h2>
<p>curl -s <a href="https://yoursite/llms.txt">https://yoursite/llms.txt</a>, fetch each .well-known path, and run your JSON-LD through a structured-data validator. If you build on Claude Code, the open-source OrchestKit docs site implements every item above — the source is on GitHub, MIT-licensed, and you can read the route handlers directly.</p>
<p>I maintain OrchestKit (a free, MIT plugin for Claude Code, 111 skills/37 agents/210 hooks). The agent-discovery surface described here is what its docs site ships today.</p>
]]></content:encoded></item><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>