Stop Copy-Pasting From ChatGPT: Wire Your Site to the LLM API So It Updates Itself
Michael Sacca•The chat window is a dead end, and almost every tutorial pretends it isn't. You prompt, Claude writes a blog post, you copy the HTML, you paste it into your file, you deploy. It works. It's also a manual job you just created for yourself, which defeats the whole reason you built the site with an LLM in the first place.
We've covered the middle path before — connecting Claude to your live site via MCP so drafts get created, reviewed, and published on a schedule. But MCP assumes you want to trigger things conversationally. There's a tier below that: no chat at all. Your site calls the LLM API directly, on a schedule, and publishes without you. This post is about that plumbing — the unglamorous part that actually makes a site self-maintaining.
What you're actually building
Strip away the hype and the architecture is four pieces:
- A schedule. Cron on a server, or a scheduled serverless function (Vercel Cron Jobs, Cloudflare Workers Cron Triggers, GitHub Actions on
schedule:). Pick whichever host your site already lives on. - An API call. A script that sends a prompt to OpenAI or Anthropic and gets structured output back.
- A write step. The script writes the result into your repo or your CMS — a new markdown file, a new HTML page, a content file your static site generator picks up.
- A deploy trigger. If your files live in git, a push triggers the build automatically. If you're on a static host with a git integration, that part is free.
That's it. No dashboard, no chat, no you.
The code that does the work
Here's a minimal version in Node, calling Anthropic's API and writing a blog post into an Astro or Eleventy-style content folder:
// scripts/generate-post.mjs
import Anthropic from "@anthropic-ai/sdk";
import { writeFile } from "node:fs/promises";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 4000,
messages: [{
role: "user",
content: `Write a 600-word blog post about ${process.env.TOPIC}.
Return ONLY valid JSON matching this shape:
{ "title": "...", "slug": "...", "body": "markdown string" }`
}]
});
const post = JSON.parse(response.content[0].text);
const filename = `src/content/posts/${post.slug}.md`;
const frontmatter = `---\ntitle: "${post.title}"\n---\n\n${post.body}`;
await writeFile(filename, frontmatter);
console.log(`Wrote ${filename}`);
The OpenAI version is structurally identical — client.chat.completions.create() with response_format: { type: "json_object" } if you want the JSON constraint enforced server-side rather than asked for politely. Both providers now support structured output one way or another, and you should use it: parsing markdown out of a chatty paragraph is a bug factory.
Then the schedule. On Vercel, a vercel.json entry runs a serverless function daily:
{
"crons": [{ "path": "/api/generate-post", "schedule": "0 6 * * 1" }]
}
On GitHub Actions, the same idea against your repo directly:
on:
schedule:
- cron: "0 6 * * 1"
The script writes a file, the script commits and pushes, your host rebuilds. Site updated. Nobody opened anything.
Failure mode one: hallucinated HTML
This is the one that will actually hurt you. An LLM asked to "write a blog post" will sometimes, helpfully, return a full HTML document — with its own <html>, <head>, maybe a <style> block it invented. Drop that into your content folder and your layout breaks, your fonts change, and you find out from a visitor.
Defenses, in order of how much they help:
- Ask for markdown, not HTML. Your site generator turns markdown into correct HTML inside your template. The LLM never touches your layout.
- Constrain output to JSON with a fixed schema, as in the script above. A JSON string can't smuggle in a
<style>tag that escapes your template — well, it can contain one, so sanitize on the way in. - Diff before deploy. This is the same discipline from the safe-edit loop for AI-built sites: if the generated file adds tags your other posts don't have, reject the run. A five-line check for
</html>or<scriptin the generated body will catch most disasters. - Preview deploys, always. The scheduled run should push to a branch or open a PR rather than commit straight to main, at least until you trust the output. Static hosts build previews for free.
Failure mode two: rate limits and cost
API calls cost money every run, and the free tiers of the chat products do not apply here — API usage is billed separately, by token. What a run costs varies with model choice, output length, and how much context you stuff into the prompt, and prices change often enough that you should check current pricing rather than trust any number in a blog post, including this one. What I can tell you: a short markdown post from a mid-tier model is pennies, the same request on a flagship model costs multiples of that, and a weekly schedule means cost is a rounding error either way — but a runaway loop that retries on every failure is not.
So build in the guardrails from day one:
- Rate limits are real. Both providers return
429responses when you hit them. Don't retry in a tight loop with no backoff — that's how you turn one failed run into a bill. Exponential backoff with a cap, or just fail the run and let cron try again next week. - Cap
max_tokens. An uncapped generation can wander into thousands of tokens of preamble before the actual content. Cap it, and cap your own retry count. - Log every run with tokens used. If cost ever matters, you want a ledger, not a mystery.
Failure mode three: the silent drift
The sneakiest problem isn't technical. Your scheduled posts drift in voice. Each run is independent — the model doesn't know what it wrote last week unless you tell it. The fix is cheap: include one or two of your best existing posts in the prompt as style reference, and keep a short "voice rules" string in your config that every run receives. Your posts stop reading like a new stranger wrote each one.
Start with one job
Don't automate your whole editorial calendar on day one. Pick the single most repetitive task — probably a weekly link roundup or a content refresh of one page — wire the script, run it to a preview branch, and watch it for a month. When the output is boringly consistent, add the second job.
That's the real payoff of going API-first instead of chat-first: the site stops being something you maintain and becomes something that reports to you. You review the diff. You merge. You go back to whatever you were doing.