Generating SEO content via API without the filler

Left to default settings, a model drifts toward generic phrases and lists that say nothing. How to shape the request to get facts, not padding.

4 min · · Updated · контент · seo

Why the model drifts toward filler

If the request carries no facts and no constraints, the model fills the gaps with generalities — its usual way of satisfying "write 1000 words" without a source of specifics. The fix isn't a different model, it's what you put in the prompt: concrete numbers, product facts, examples, and an explicit ban on certain constructions.

A prompt with bans beats a prompt with wishes

Asking to "write engagingly and to the point" barely helps — the model has no idea what you consider filler. What works is a list of specific banned phrases and a requirement that every claim be backed by a number or an example:

bash
{
  "model": "claude-sonnet-5",
  "messages": [
    {
      "role": "system",
      "content": "Do not use phrases like 'in this section we will look at', 'it is important to note', 'in summary'. No 'first / second / third' lists without specifics inside each point. Every paragraph must contain a number, a tool name, or an example command."
    },
    {"role": "user", "content": "Write a 300-word section about setting up API response caching."}
  ]
}

Models without built-in real-time search can be connected to POST /v1/web-search — a separate gateway endpoint for cases where you need current data rather than whatever the model remembers from training:

bash
curl https://api.zukko.pro/v1/web-search \
  -H "Authorization: Bearer fetch_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "SEO title length recommendations 2026"}'

The result can be mixed into the context of the next generation request — then the text cites specific numbers instead of an averaged "the title shouldn't be too long."

Technical limits on SEO fields

Title and description have hard character limits, and that's the one place worth demanding exact compliance rather than "approximately." For a 120-160 character description, ask the model for five variants so you can pick one that fits the range, instead of trimming manually and losing meaning mid-sentence:

bash
resp = client.chat.completions.create(
    model="gpt-5.6-sol",
    messages=[
        {"role": "system", "content": "Return exactly 5 meta description variants, each 120-160 characters, no quotes."},
        {"role": "user", "content": "Topic: migrating from the OpenAI API to a compatible gateway."},
    ],
)

Localizing numbers and currency

When generating the RU and EN versions of the same article, it helps to explicitly tell the model which number and currency format to use — 1,000,000 vs 1 000 000, dollar sign before or after the amount. Without that instruction the model applies formats inconsistently from one paragraph to the next within the same piece, and it shows immediately on proofreading.

Extended thinking for structuring long pieces

For long articles (1500+ words) it helps to first ask the model for an outline with subheadings and a word-count weight per section, and only then write the text section by section against that outline. The Claude and GPT models in the catalogue run with extended thinking and a 1M-token context (GLM has 128K, which is usually enough for an article too), so the outline and the draft can live in the same conversation without losing coherence between sections.

Caching boilerplate parts

If your content generator regularly produces the same type of block — a standard disclaimer, a "how to reach support" block — turn on caching with the X-Zukko-Cache: true header (temperature: 0 also enables the cache, but only on models that accept that parameter; Claude models — claude-sonnet-5, claude-opus-5-5 — may reject a temperature other than 1 with a 400, so don't pass it to them). A repeat of a fully identical request (the whole body has to match — an "almost the same" prompt won't hit the cache) comes back instantly and free, flagged with X-Zukko-Cache: hit in the response — no reason to pay for generating the same paragraph a hundredth time.

Checking for repeated openings across articles

If content is generated in batches on similar topics, there's a real risk the model opens different articles with the same phrase — it's statistically the most likely one for that kind of prompt. A simple fix is to keep a list of already-used opening sentences and pass it into the prompt as a "don't repeat" list:

bash
{
  "role": "system",
  "content": "Don't open the article the way these already-published intros do: ['Connecting to a compatible API usually takes a couple of minutes...', 'Left to default settings, a model drifts toward generic phrases...']. Open differently."
}

In practice, ten to fifteen recent openings in the list are enough to noticeably cut down repeats without bloating the prompt.

Generating the excerpt and tags in a separate call

It makes sense to generate the excerpt and tags for a blog card in a separate, cheaper call after the main text, rather than one big prompt covering everything at once — that makes it easier to cap the excerpt length (one or two sentences) and reuse the same call for different language versions of the article by passing in the finished text as context instead of re-explaining the topic.

Quality control against a second pass

A good way to check that a prompt actually removed the filler is to run the finished text back through the model with the task "find sentences without concrete facts and remove them." That's a separate call, possibly to a different model (claude-opus-5-5, say — check on your own texts whether it reads more carefully), and in practice it catches leftover generic phrases that slipped through the first pass.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Generating SEO content through the API without filler · Zukko