4 min read · 976 words
Tips & Tricks / Blog Management / Python · API
Approx. 2,400 characters
When writing blog posts with an LLM, the most common pattern is asking ChatGPT or Claude, "Write a guide on this topic," and then manually copying the resulting HTML to paste it into the Blogger admin UI. This takes about 5 minutes, and you have to manually set the labels, meta descriptions, and categories every single time. To solve this, we built a bypass endpoint that sends LLM chat outputs directly to Blogger as a DRAFT with a single-line curl command.
Why We Built It
Directly calling LLM APIs (e.g., Anthropic / Google) accumulates token costs, costing around $0.10 to $0.50 per post. For 100 posts, that's $10 to $50. This is a burden for solo-operated blogs like ours.
On the other hand, subscriptions like ChatGPT Plus, Claude Pro, or Gemini Advanced cost a flat $20 per month. Even if you write 100 posts a month, the additional cost is zero. However, the workflow of copying and pasting from the chat window to Blogger is so slow that you eventually end up reverting to API calls.
The Solution: Send the HTML received from the LLM chat to a single publishing endpoint in our webapp. This automatically passes through the hook chain—handling labels, meta descriptions, automatic chart injection, and SEO meta validation—and saves it as a Blogger DRAFT. All the human has to do is get the response from the chat and run a single curl command.
With a flat $20 monthly subscription, you can publish 100 posts while passing them all through the webapp's publishing hook chain (sanitize / quality gate / 5-channel indexing).
How It Works
There is a separate endpoint inside the webapp: POST /api/writer/external-publish.
Request Shape
{
"title": "Post Title",
"content_html": "<p>Body HTML...</p><h2>Section</h2>...",
"label": "Tips & Tricks",
"post_type": "guide",
"primary_keyword": "keyword",
"meta_description": "100-160 character meta description"
}
Simply paste the HTML received from the LLM chat into content_html and fill in the other 5 fields.
Server Processing
- Authentication — Restricted to localhost (127.0.0.1). No external exposure.
- Label Normalization — Automatically maps variations like
["활용 팁", "사용팁", "tips"]to one of the site's 6 canonical labels. If it's not a recognized label, it returns a{"error":"unknown_label","allowed":["..."]}response. - HTML Validation — Strips
/tags and automatically converts rawAutomatic Prompt Template Generation
Calling
GET /api/writer/external-publish/prompt-template?topic=foo&label=활용+팁returns a pre-configured LLM prompt tailored to the site's tone and structure. You can simply copy and paste that directly into the ChatGPT/Claude chat window to get your response.Real-world Results
- LLM API Costs: $0.10-0.50 per post × 100 posts/month → Flat $20/month (ChatGPT Plus)
- Publishing Time per Post (LLM Response → Blogger DRAFT): 5 minutes → 30 seconds
- Hook Chain Pass Rate: 47 out of 47 posts generated by external LLMs (averaging a perfect 15/15 on
publish_quality_gate) - Mislabeled Post Incidents: Average of 3 cases/month before adoption → 0 cases (thanks to canonical normalization)
- Most Common Usage Pattern: ChatGPT chat → Copy response → Single-line curl in terminal
Processing 100 posts a month with a single ChatGPT Plus subscription brings your annual LLM cost to $240 (subscription) vs. $1,200–$6,000 (API calls). This is a massive difference for a solo operator.
Validation Methods
We performed three types of validation.
Hook Chain Pass Rate
Logged all
publish_quality_gateresults after publishing 47 posts generated by external LLMs. 47/47 scored ≥ 90, with an average score of 95. Thepublish_sanitizerautomatically cleaned up sloppy HTML in the LLM output (such as leftover markdown syntax or emptytags) to meet the score requirements.Label Normalization Regression Testing
Created unit tests with a golden set of 30 test cases (input label variations). 30/30 mapped correctly, such as
"tips"→"활용 팁","사용팁"→"활용 팁", and"compare"→"비교". Unknown labels returned a 400 response.Security Spot Check
Calling the endpoint from an external IP returns a 403. Enforced localhost-only restriction. 5/5 passed.
How to Build It
The core is a single FastAPI endpoint function.
from fastapi import FastAPI, HTTPException, Request from pydantic import BaseModel app = FastAPI() LABEL_MAP = { "활용 팁": "활용 팁", "tips": "활용 팁", "사용팁": "활용 팁", "비교": "비교", "compare": "비교", "가이드": "가이드", "howto": "가이드", # ... site canonical label mapping } class ExternalPublishBody(BaseModel): title: str content_html: str label: str post_type: str = "guide" primary_keyword: str = "" meta_description: str = "" @app.post("/api/writer/external-publish") async def external_publish(body: ExternalPublishBody, request: Request): # localhost only client_ip = request.client.host if request.client else "" if client_ip not in ("127.0.0.1", "localhost", "::1"): raise HTTPException(403, "localhost-only") # label normalize canonical = LABEL_MAP.get(body.label.strip().lower()) if not canonical: raise HTTPException(400, f"unknown_label allowed={list(LABEL_MAP.values())}") # forward to publish_post hook chain from webapp.routers.blogger import publish_post, PublishPostBody pp = PublishPostBody( title=body.title, content=body.content_html, labels=[canonical], is_draft=True, meta_description=body.meta_description, primary_keyword=body.primary_keyword, ) return await publish_post(pp)Get the post from the ChatGPT/Claude chat and run the curl command:
curl -X POST http://127.0.0.1:8766/api/writer/external-publish \ -H "Content-Type: application/json" \ -d '{ "title": "Guide post on this topic", "content_html": "<p>Body HTML...</p>", "label": "Tips & Tricks", "meta_description": "100-160 character meta..." }'Response:
{"ok": true, "post_id": "1234567...", "blogger_url": "https://..."}Summary: A flat-rate LLM subscription + a single-line curl endpoint = publishing 100 posts a month while passing them all through your hook chain. It takes just 30 seconds from chat to response to publication. You can max out your content generation within the budget constraints of a solo operator.
Category Coverage Notice
This article follows our label-specific editorial criteria. Details: