| title | write |
|---|---|
| parent | Commands |
| nav_order | 3 |
| description | auto-geo write — generate publish-ready GEO resource pages from target queries. |
Generate publish-ready GEO resource pages from a set of target queries — the questions you want your domain to be cited for inside ChatGPT, Perplexity, Claude, Gemini, and Google AI Overviews.
auto-geo write \
--domain https://www.shadow.inc \
--query "what is GEO" \
--query "GEO vs SEO" \
--query "how to get cited by ChatGPT" \
--out ./resources \
--provider openai \
--model gpt-4oEach query produces one JSON file in <out>/<slug>.json, validated against the canonical resourcePublishSchema. Files are ready to publish through your own pipeline — CMS import, static-site build, or custom renderer.
auto-geo doctor audits whether a page is shaped for citation. auto-geo write produces a page that already passes the audit. It encodes the GEO SOP as a system prompt, uses the Vercel AI SDK's generateObject with a Zod schema to constrain output, re-validates with the same schema your publish endpoint enforces, and feeds Zod issues back to the model on failure for a bounded self-correction loop.
The CLI ships with auto-geo (npm i -g auto-geo). The AI SDK and provider packages are installed alongside as runtime dependencies, so a fresh install already has everything it needs.
auto-geo write --helpSet the API key for the provider you choose:
export OPENAI_API_KEY=sk-… # for --provider openai (default)
# or
export ANTHROPIC_API_KEY=sk-… # for --provider anthropicIf the relevant key is missing, the CLI exits with a clear error (exit code 2). --dry-run does not require an API key.
| Flag | Default | What it does |
|---|---|---|
--domain <url> |
required | Publisher domain. Used to scope internal relatedGuides links. |
--query <text> |
required, repeats | Target query. One page per --query. |
--queries-file <path> |
Newline-separated file of queries. Lines beginning # are comments. |
|
--out <dir> |
./out |
Output directory. Created if it doesn't exist. |
--provider <id> |
openai |
LLM provider (openai | anthropic). |
--model <name> |
provider default | gpt-4o-mini for openai, claude-sonnet-4-6 for anthropic. Cheap defaults so a small run stays cheap. |
--basepath <path> |
/resources |
URL path under your domain where resources are served. |
--author-name <text> |
Shadow Research | Used for the page's author.name. |
--author-jobtitle <t> |
… | Used for author.jobTitle. |
--author-bio <text> |
Shadow team bio | Used for author.bio. Pass @path/to/bio.txt to read from a file. |
--author-linkedin <u> |
Used for author.linkedinUrl. |
|
--max-retries N |
2 |
Self-correction retries when the LLM draft fails Zod validation. |
--concurrency N |
2 |
Parallel LLM calls. Capped at 2 by default to respect rate limits + cost predictability. |
--dry-run |
Compute the plan + cost estimate without calling the LLM. | |
--json |
Emit a machine-readable summary on stdout. | |
--no-color |
Disable ANSI colors even on a TTY. |
Exit code: 0 if every page validates and writes; 1 if any page fails after retries; 2 on bad arguments or missing env.
The slug is derived from the query: lowercased, kebab-case, with question-form stopwords (what, how, is, the, a, to, by, …) stripped. Examples:
| Query | Slug |
|---|---|
what is GEO |
geo |
how does GEO work? |
geo-work |
GEO vs SEO |
geo-vs-seo |
how to get cited by ChatGPT |
get-cited-chatgpt |
When multiple queries collapse to the same slug, the second and subsequent get -2, -3, … appended.
auto-geo write ships a single system prompt that encodes the schema's hard rejection criteria and the audit's soft heuristics. Read the canonical version at cli/llm.ts (look for SYSTEM_PROMPT). Highlights:
- TL;DR: 40–60 words, must answer the target query.
- Answer capsules: 40–60 words per H2, self-contained.
- FAQ answers: 40–60 words each, 3–10 FAQ items.
- Related guides: 4–8 entries; mix of internal (
/{basepath}/<slug>) and authoritative external (Princeton GEO paper, Schema.org, Search Engine Land). - Key takeaways: 4–6 entries, each 10–35 words.
- H1 title: the target query phrased as a question.
- No banned superlatives:
industry-leading,revolutionary,cutting-edge,world-class,next-generation, etc., unless attributed or inside quotes. - No raw HTML:
**bold**,*italic*,[text](url)only.
1. LLM call (generateObject) → candidate payload
2. resourcePublishSchema.safeParse → pass or issues[]
3. If pass: write to <out>/<slug>.json, done
4. If fail and attempts < maxRetries:
- Re-prompt with the previous draft + numbered Zod issues
- Go to 1
5. If fail and attempts exhausted:
- Emit a `failed` outcome with the issue list
The SDK's generateObject already enforces JSON-mode + tool-use against the Zod schema, so the model can't return malformed JSON. But the SDK adapter doesn't run our custom superRefine callbacks (word-count windows, banned-superlative regex, table-row consistency), so we re-validate defense-in-depth.
Most validation failures clear on the first retry: the model sees the exact numeric path that's wrong and corrects it.
Token usage is read from the AI SDK result (usage.inputTokens, usage.outputTokens) and multiplied by per-model rates hardcoded in cli/cost.ts. Cheap-default models are quoted accurately; premium models fall back to a conservative provider default — verify against your billing dashboard.
Total: 3 pages · ~$0.18 spent · 47s elapsed
--dry-run shows the same line with estimated instead of spent based on observed per-page token counts (~4.7k tokens).
import { runWrite, DEFAULT_AUTHOR } from "auto-geo/cli/write"; // not a published subpath; for internal use
import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const summary = await runWrite({
domain: "https://www.shadow.inc",
queries: ["what is GEO", "GEO vs SEO"],
outDir: "./resources",
provider: "openai",
modelName: "gpt-4o-mini",
model: openai("gpt-4o-mini"),
basePath: "/resources",
author: DEFAULT_AUTHOR,
publishedAt: "2026-06-02",
maxRetries: 2,
});The same runWrite powers the CLI; it's a single function call so you can wire it into your own pipeline (Inngest job, cron, webhook).
"OPENAI_API_KEY is not set" — Export the env var, or switch to --provider anthropic with ANTHROPIC_API_KEY set.
"Generated payload failed schema validation after N attempts" — The model couldn't produce a schema-conformant page. Raise --max-retries, switch to a smarter model (--model gpt-4o, --model claude-opus-4-5), or rephrase the query. Inspect the issue list in the failure output.
"--provider must be 'openai' or 'anthropic'" — Only these two providers are wired in. Add a feature request to extend; the AI SDK supports many more.
Cost surprises — Always run --dry-run first on a large query batch. Tighter cost control: use --model gpt-4o-mini or --model claude-haiku, both ~10x cheaper than the flagship models.
Self-link in relatedGuides — The schema doesn't reject self-links (the audit warns instead). The system prompt instructs the model to avoid them, but if you see one, edit the generated JSON before publishing.
Once you've published the JSON, point auto-geo doctor at the live URL to confirm the page renders with the expected citation signals:
auto-geo doctor https://www.shadow.inc/resources/geo