{
  "slug": "producing-articles-via-api",
  "title": "Producing articles via API",
  "description": "How AI sessions and external services create and publish CMS content programmatically — endpoint, JSON shape, access tokens, and what happens after a successful POST.",
  "category": "api-reference",
  "order": 5,
  "locale": "en",
  "translationGroup": "e31fecc5-03e0-4cfd-a713-8a45a1b2ab83",
  "helpCardId": null,
  "content": "## When to use this\n\nThis guide is for **AI sessions, scheduled jobs, and external services** that need to push content into a CMS site without going through the admin UI. Typical cases:\n\n- A research-agent session writes a markdown article and publishes it to your blog\n- A daily cron drops a digest post compiled from external sources\n- A webhook from a third-party tool (Zapier, n8n) creates documents on demand\n\nIf you are a human editing through `webhouse.app/admin`, you don't need this — just use the editor.\n\n## The contract in one paragraph\n\n`POST` your document JSON to `/api/cms/{collection}?site={siteId}` with an `Authorization: Bearer wh_...` header carrying a token scoped `content:write` for that site. The body needs only four fields (`slug`, `status`, `data`, optionally `locale`); the server fills in `id`, `_fieldMeta`, `updatedAt`, and audit metadata. Use `status: \"published\"` — drafts do not trigger the live-site revalidation webhook.\n\n## Step 1 — Create an access token\n\nIn `webhouse.app/admin`:\n\n1. Click your avatar → **Account Preferences** → **Access tokens** → **New token**\n2. Set scope to `content:write` and resource to the specific site (`site:trail`, not `site:*`)\n3. Optionally restrict to a single collection (`posts` only) for least-privilege\n4. Copy the generated `wh_...` string — it is shown once only\n\nFor cross-session use (e.g. handing the token to a different cc session), the user owning the token must paste it into the recipient's environment. There is no secure cross-session sharing primitive yet.\n\n## Step 2 — POST the document\n\n```bash\ncurl -X POST 'https://webhouse.app/api/cms/posts?site=trail' \\\n  -H 'Authorization: Bearer wh_xxx' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"slug\": \"three-architectures-of-agent-memory\",\n    \"status\": \"published\",\n    \"data\": {\n      \"title\": \"Three architectures of agent memory — and why Trail picked Compile\",\n      \"excerpt\": \"RAG, fine-tuning, and compile-time integration each solve a different memory problem. Here is what each gives up — and why Trail spent a year building the third.\",\n      \"content\": \"## Section heading\\n\\nMarkdown body…\\n\\n{{svg:scan-wall-curve}}\\n\\nMore prose…\",\n      \"date\": \"2026-05-03\",\n      \"author\": \"Trail Team\",\n      \"category\": \"research\",\n      \"tags\": [\"rag\", \"llm-wiki\", \"agent-memory\"],\n      \"readTime\": \"9 min read\"\n    }\n  }'\n```\n\nResponse on success (HTTP 201):\n\n```json\n{\n  \"id\": \"three-architectures-of-agent-memory\",\n  \"slug\": \"three-architectures-of-agent-memory\",\n  \"status\": \"published\",\n  \"locale\": \"en\",\n  \"data\": { /* echoed back */ },\n  \"updatedAt\": \"2026-05-03T15:42:11.000Z\",\n  \"_fieldMeta\": {}\n}\n```\n\n## What the body fields mean\n\n| Field | Required | Notes |\n|-------|----------|-------|\n| `slug` | yes | URL-safe (`a-z0-9-`), unique within the collection. Becomes the URL fragment after `urlPrefix`. |\n| `status` | yes | `\"published\"` or `\"draft\"`. **Use `\"published\"`** unless you specifically want to stage drafts — draft documents do not fire the revalidation webhook, so the live site stays stale. |\n| `data` | yes | Object matching the collection's field schema. Field names must match `cms.config.ts` exactly. Unknown fields are stripped silently. |\n| `locale` | no | Defaults to `config.defaultLocale`. Set explicitly if the site is multilingual and you are creating a non-default-locale variant. |\n| `translationGroup` | no | UUID linking translations of the same document. Required for multilingual sites — use the SAME UUID for all language variants. Generate with `crypto.randomUUID()`. |\n\n## What happens after the POST\n\n1. The document JSON is written to `_data/sites/{siteId}/content/{collection}/{slug}.json` on the cms-admin server (or to the GitHub repo for github-adapter sites)\n2. cms-admin saves a revision under `_revisions/`\n3. If the site has `revalidateUrl` configured, the revalidation webhook fires — Next.js sites call `revalidatePath()` for the affected route within ~2 seconds\n4. If the site uses GitHub Pages and `deployOnSave: true`, the rocket pipeline runs: build.ts produces the static output, the diffed files upload to the `gh-pages` branch, GitHub Pages publishes, and the `page_build` webhook fires back to cms-admin\n5. The admin UI shows a toast and (if Web Push is enabled) a native OS notification\n\nFor a clean GH-Pages site like trail-landing, end-to-end POST → live URL is typically 30–90 seconds.\n\n## Multilingual posts\n\nFor a site with `da` and `en` locales, create one document per language with a SHARED `translationGroup`:\n\n```bash\nUUID=$(node -e 'console.log(crypto.randomUUID())')\n\n# English variant\ncurl -X POST '.../api/cms/posts?site=trail' -d \"{\n  \\\"slug\\\": \\\"my-post\\\",\n  \\\"locale\\\": \\\"en\\\",\n  \\\"translationGroup\\\": \\\"$UUID\\\",\n  \\\"data\\\": { /* English content */ }\n}\"\n\n# Danish variant — SAME UUID\ncurl -X POST '.../api/cms/posts?site=trail' -d \"{\n  \\\"slug\\\": \\\"mit-indlaeg\\\",\n  \\\"locale\\\": \\\"da\\\",\n  \\\"translationGroup\\\": \\\"$UUID\\\",\n  \\\"data\\\": { /* Danish content */ }\n}\"\n```\n\nWithout shared `translationGroup`, the language switcher, hreflang tags, and side-by-side translation editor all break.\n\n## Updating an existing document\n\nUse `PATCH` for partial update or `PUT` for full replace:\n\n```bash\ncurl -X PATCH 'https://webhouse.app/api/cms/posts/{slug}?site=trail' \\\n  -H 'Authorization: Bearer wh_xxx' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"data\": {\"title\": \"Updated title\"}}'\n```\n\nPATCH merges the supplied `data` keys with the existing document. Other top-level fields (`status`, `locale`) update independently.\n\n## SVG and other build-time assets\n\nThe Content API does NOT handle build-time assets like `{{svg:...}}` placeholders that a custom build.ts resolves at compile time. Those live in the **site's source repository**, not the CMS:\n\n- For a GitHub-backed site: open a PR adding `apps/landing/public/uploads/svg/<slug>.svg` plus any build.ts changes that wire up new slugs\n- For a CMS-uploaded image (used in a richtext editor or `image` field): use `POST /api/media` with multipart form-data\n\nThe distinction: if the asset is **referenced in your build.ts at compile time**, it belongs in the site source. If it is **rendered into editor content**, it belongs in CMS media.\n\n## Common mistakes\n\n- **POST without `?site=...`** — token-based callers MUST always include `?site=<siteId>` on the URL. The server falls back to the registry default site when omitted, which silently writes to the WRONG site. The token's site-scope claim is checked against `?site=`, not against fallbacks.\n- **Status `\"draft\"`** — silently does not appear on the live site because revalidation does not fire. Use `\"published\"`.\n- **Field names with typos** — unknown fields are silently dropped. The document is created but missing data. Run `GET /api/cms/{collection}/{slug}` to verify the round-trip.\n- **Multilingual without `translationGroup`** — orphans the document from its translation group. Side-by-side editor and language switcher break.\n- **Reusing an existing slug** — POST returns 409. Use PATCH instead, or pick a unique slug.\n\n## Verifying success\n\n```bash\n# Did it land?\ncurl 'https://webhouse.app/api/cms/posts/{slug}?site=trail' \\\n  -H 'Authorization: Bearer wh_xxx'\n\n# Is it live on the deployed site?\ncurl -I 'https://www.your-site.com/posts/{slug}/'\n```\n\nThe second should return `HTTP/2 200`. If it returns 404, the build/deploy pipeline did not fire — check `_data/sites/{siteId}/deploy-log.json` for the most recent entry.\n\n## See also\n\n- [Headless Site API & Chat Embedding](/docs/headless-api) — the full REST reference and chat-embed flow.\n- [Content API & ContentService](/docs/api-reference) — the in-process programmatic API used by build.ts.\n- [Storage adapters](/docs/storage-adapters) — where the JSON physically lives (filesystem, GitHub, SQLite).",
  "excerpt": "When to use this\n\nThis guide is for AI sessions, scheduled jobs, and external services that need to push content into a CMS site without going through the admin UI. Typical cases:\n\n- A research-agent session writes a markdown article and publishes it to your blog\n- A daily cron drops a digest post c",
  "seo": {},
  "updatedAt": "2026-05-03T15:42:00.000Z"
}