> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neuraldraft.io/llms.txt
> Use this file to discover all available pages before exploring further.

# CMS

> The editable layer for AI-built sites. Translation keys, image keys, brand context, register-a-component.

The CMS pillar is what turns a static AI-generated site into a real, editable
property. Every text node and image you mark up with our conventions becomes a
field in the project's admin — no schemas, no migrations, no glue code.

## What this gives you

* **Translation keys** — dot-namespaced keys (`hero.headline`, `pricing.tiers.0.title`)
  with per-language values. One bulk endpoint feeds your build process.
* **Image keys** — `data-image-key="hero.background"` on an `<img>`. The
  customer can swap the image in the admin; your build resolves the URL.
* **Brand context** — voice, colors, fonts, audience exposed as a single
  resource. Read once at build time, or hot-fetch per request.
* **Register-component** — turn AI-generated HTML into an admin-editable
  section in one call, with `data-translate` keys auto-discovered.

## Quick example

Read a single key, then upsert one. The SDK exposes both as one-liners.

<CodeGroup>
  ```ts SDK theme={null}
  import { NeuralDraftClient } from "@neuraldraft/sdk";

  const nd = new NeuralDraftClient({ apiKey: process.env.NEURALDRAFT_API_KEY! });

  // Read one key (response includes `all_locales` with every translation)
  const hero = await nd.content.get("hero.headline", { lang: "en" });
  console.log(hero.value);

  // Upsert (creates if missing). Charges 1 credit (`content_update`).
  await nd.content.set("hero.headline", "Welcome to Acme", "en");
  ```

  ```bash cURL theme={null}
  # Bulk-read for build time (up to 200 keys per call)
  curl "https://api.neuraldraft.io/v1/content/bulk?keys=hero.headline,hero.subhead,cta.label&lang=en" \
    -H "Authorization: Bearer $NEURALDRAFT_API_KEY"

  # Upsert one key
  curl -X PUT https://api.neuraldraft.io/v1/content/hero.headline \
    -H "Authorization: Bearer $NEURALDRAFT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"lang":"en","value":"Welcome to Acme"}'
  ```
</CodeGroup>

Bulk read response shape:

```json theme={null}
{
  "data": {
    "hero.headline": "Welcome",
    "hero.subhead": "We build software that scales.",
    "cta.label": "Get started"
  },
  "meta": { "lang": "en", "page": 1, "page_size": 200, "total": 3 }
}
```

Seed many keys at once with `bulkCreate` (idempotent — existing keys are
skipped, not failed):

```ts theme={null}
await nd.content.bulkCreate(
  {
    "hero.headline": "Welcome",
    "hero.subhead": "We build software that scales.",
    "cta.label": "Get started",
  },
  "en",
);
// => { created: ["hero.headline", "hero.subhead", "cta.label"], skipped_existing: [] }
```

<Note>
  **CMS writes cost 1 credit each.** `PUT /v1/content/{key}` (`content_update`),
  `POST`/`PATCH /v1/pages` (`page_update`), and registering an image via URL
  swap on `PUT /v1/images/{key}` or multipart upload on `POST /v1/images`
  (`image_register`) each consume 1 credit on success. AI generation
  (image, translation, blog, website) is metered separately at its own rate.
  Reads (`GET`) remain free. See [pricing](/pricing-and-credits).
</Note>

## Common workflows

### 1. Build-time fetch for SSG

Static-site generators (Next.js, Astro, Nuxt, Hugo) call `/v1/content/bulk`
once per locale at build time. Cache the response — re-fetch only when the
`content.changed` webhook fires.

### 2. Click-to-edit overlay on a live site

Drop a tiny JS snippet into your HTML. It looks for `data-translate`
attributes and turns them into inline-editable fields when the user is signed
into the admin in the same browser.

```html theme={null}
<script src="https://widgets.neuraldraft.io/v1/edit-overlay.js" defer></script>
```

The overlay calls `/v1/content/{key}` on save. Same backend, no rebuild.

### 3. AI registers a generated section

When the AI in your editor generates a hero, it calls
`register_component(html, intent)` via MCP. The component appears in your
admin instantly:

```json theme={null}
POST /v1/components/register
{
  "html": "<section data-component='hero'><h1 data-translate='hero.headline'>Welcome</h1></section>",
  "intent": "marketing hero"
}
```

```json theme={null}
{
  "component_id": "cmp_2NgcaXxFqLPo",
  "editor_url": "https://app.neuraldraft.io/components/cmp_2NgcaXxFqLPo",
  "keys_created": ["hero.headline"]
}
```

The admin auto-discovers `hero.headline` as an editable text field. No schema
work.

### 4. Translate a key into N languages

```ts SDK theme={null}
const job = await nd.content.translate("hero.headline", ["fr", "de", "es"]);
const finished = await nd.jobs.poll(job.id);
// finished.status === "completed"
```

```bash cURL theme={null}
curl -X POST https://api.neuraldraft.io/v1/content/hero.headline/translate \
  -H "Authorization: Bearer $NEURALDRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_langs":["fr","de","es"]}'
```

Returns a `Job` reference. Poll [`/v1/jobs/{id}`](/api-reference#tag/Jobs) (or
`nd.jobs.poll(id)`) until terminal, or subscribe to the `content.changed`
webhook to get notified once each language lands. Charges **7 credits per
target language** (`translate_language`).

### 5. Brand context for AI prompts

Read brand once, pass it into your AI calls. The MCP server does this for you;
if you're calling the API directly:

```ts SDK theme={null}
const brand = await nd.brand.get();
// Patch a field — `null` clears it.
await nd.brand.update({ voice: "friendly_professional" });
```

```bash cURL theme={null}
curl https://api.neuraldraft.io/v1/brand \
  -H "Authorization: Bearer $NEURALDRAFT_API_KEY"
```

```json theme={null}
{
  "voice": "friendly_professional",
  "audience": "indie founders building with AI tools",
  "content_tone": "warm",
  "colors": {
    "primary": { "hex": "#7C3AED", "name": "Purple" },
    "secondary": { "hex": "#0F172A", "name": "Ink" },
    "accent": { "hex": "#A78BFA", "name": "Lavender" }
  },
  "fonts": { "heading": "Inter", "body": "Inter" },
  "default_language": "en",
  "target_languages": ["en", "fr", "de"],
  "requires_branding_badge": false
}
```

## Conventions

The two attributes the AI uses everywhere:

```html theme={null}
<h1 data-translate="hero.headline">Welcome</h1>
<p data-translate="hero.subhead">We build software that scales.</p>

<img data-image-key="hero.background" src="..." alt="...">
```

Keys are case-sensitive, dot-namespaced, and lowercase by convention. Index
keys for arrays follow `section.field.0.subfield`.

## Per-page SEO meta

Search engines and social cards need per-route metadata — `<title>`,
`<meta name=description>`, and the `og:*` tags. Neural Draft stores this
on the page itself so each route can have its own values without polluting
the translation-key namespace.

There are two surfaces depending on what kind of content you're rendering:

### CMS-managed pages — `GET /v1/pages/{slug}`

For everything that isn't a blog post (homepage, about, services, legal,
landing pages…), fetch the `Page` resource at build time and use the meta
fields directly. Every field is nullable; an empty `meta_title` means the
page `title` is used as the document title.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.neuraldraft.io/v1/pages/about \
    -H "Authorization: Bearer $NEURALDRAFT_API_KEY"
  ```

  ```ts SDK theme={null}
  import { NeuralDraftClient } from "@neuraldraft/sdk";

  const nd = new NeuralDraftClient({ apiKey: process.env.NEURALDRAFT_API_KEY! });

  const page = await nd.pages.get("about");
  // =>
  // {
  //   id: 42,
  //   slug: "about",
  //   title: "About us",
  //   type: "landing",
  //   is_homepage: false,
  //   is_active: true,
  //   meta_title: "About — Acme",
  //   meta_description: "We build software that scales.",
  //   og_image: "https://cdn.example.com/og/about.jpg",
  //   canonical_url: null,
  //   ...
  // }

  // Create a page with SEO meta in one call (1 credit)
  await nd.pages.create({
    slug: "about",
    title: "About us",
    type: "landing",
    meta_title: "About — Acme",
    meta_description: "Built by makers who hate boilerplate.",
    og_image: "https://cdn.example.com/og/about.jpg",
  });

  // List pages (filter by type / is_active)
  const { data: pages } = await nd.pages.list({ page_size: 50, type: "landing" });
  ```
</CodeGroup>

Use `nd.pages.update(id, { meta_title: null })` (or pass `null` via cURL)
to clear a single meta field. Untouched fields are preserved — the API
merges the patch into the existing record.

### Blog posts — `GET /v1/blog-posts/{slug}`

Posts have their own per-translation meta. The locale-aware response picks
the matching translation for `meta_title` / `meta_description`. Use
`featured_image` for `og:image` (it's the canonical image associated with
the post).

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.neuraldraft.io/v1/blog-posts/5-minute-breathwork-for-anxious-mornings?lang=en" \
    -H "Authorization: Bearer $NEURALDRAFT_API_KEY"
  ```

  ```ts Node (SDK) theme={null}
  const post = await nd.blogPosts.get("5-minute-breathwork-for-anxious-mornings", { lang: "en" });
  // post.meta_title, post.meta_description, post.featured_image
  ```
</CodeGroup>

To patch SEO meta on a post (e.g. tighten the description after publish):

```ts theme={null}
await nd.blogPosts.update(post.id, {
  meta_title: "5-minute breathwork — Acme blog",
  meta_description: "A 5-minute morning routine to take the edge off anxiety.",
});
```

### Where to put what in your `<head>`

```html theme={null}
<title>{page.meta_title ?? page.title}</title>
<meta name="description" content={page.meta_description ?? ""} />
<meta property="og:title" content={page.og_title ?? page.meta_title ?? page.title} />
<meta property="og:description" content={page.og_description ?? page.meta_description ?? ""} />
<meta property="og:image" content={page.og_image ?? defaultOgImage} />
{page.canonical_url && <link rel="canonical" href={page.canonical_url} />}
{page.exclude_from_search && <meta name="robots" content="noindex" />}
```

For a blog post the same template applies, swapping `page.og_image` with
`post.featured_image`.

## Images

Three flavours, all routing through the same `Image` resource:

```ts theme={null}
// 1. AI-generate (32 credits, async — returns a Job)
const job = await nd.images.generate({
  prompt: "warm morning light over a yoga mat, soft focus",
  aspect_ratio: "16:9",
  key: "hero.background",
});
const finished = await nd.jobs.poll(job.id);
// finished.result => { url, key, width, height }

// 2. Direct upload from a File / Blob / Buffer (1 credit, sync)
await nd.images.upload("hero.background", file, { filename: "hero.jpg" });

// 3. Replace by URL — no AI roundtrip (1 credit, sync)
await nd.images.replace("hero.background", { url: "https://cdn.example.com/x.jpg" });

// Resolve / list / delete
const img = await nd.images.get("hero.background");
const { data } = await nd.images.list({ prefix: "hero.", page_size: 50 });
await nd.images.delete("hero.background");
```

Both `upload()` and `replaceFile()` accept a Web `File`/`Blob` (browser, Bun,
Node 20+) or a Node `Buffer`/`Uint8Array`. For raw buffers, supply
`opts.filename` so the server can derive the file extension.

## Reference

| Endpoint                                                    | Tag                                         |
| ----------------------------------------------------------- | ------------------------------------------- |
| `GET /v1/content/bulk`                                      | [Content](/api-reference#tag/Content)       |
| `GET /v1/content/{key}`                                     | [Content](/api-reference#tag/Content)       |
| `PUT /v1/content/{key}`                                     | [Content](/api-reference#tag/Content)       |
| `PUT /v1/content` (batch)                                   | [Content](/api-reference#tag/Content)       |
| `POST /v1/content/{key}/translate`                          | [Content](/api-reference#tag/Content)       |
| `POST /v1/components/register`                              | [Components](/api-reference#tag/Components) |
| `GET /v1/components`                                        | [Components](/api-reference#tag/Components) |
| `GET /v1/pages`                                             | [Pages](/api-reference#tag/Pages)           |
| `GET /v1/pages/{idOrSlug}`                                  | [Pages](/api-reference#tag/Pages)           |
| `POST /v1/pages`                                            | [Pages](/api-reference#tag/Pages)           |
| `PATCH /v1/pages/{id}`                                      | [Pages](/api-reference#tag/Pages)           |
| `DELETE /v1/pages/{id}`                                     | [Pages](/api-reference#tag/Pages)           |
| `GET /v1/brand`                                             | [Brand](/api-reference#tag/Brand)           |
| `PATCH /v1/brand`                                           | [Brand](/api-reference#tag/Brand)           |
| `GET /v1/images`                                            | [Images](/api-reference#tag/Images)         |
| `GET /v1/images/{key}`                                      | [Images](/api-reference#tag/Images)         |
| `POST /v1/images` (AI generate or multipart upload)         | [Images](/api-reference#tag/Images)         |
| `PUT /v1/images/{key}` (multipart, URL swap, or regenerate) | [Images](/api-reference#tag/Images)         |
| `DELETE /v1/images/{key}`                                   | [Images](/api-reference#tag/Images)         |
| `POST /v1/newsletters/subscribe` (public)                   | [Forms](/api-reference#tag/Forms)           |
| `GET /v1/newsletters`                                       | [Forms](/api-reference#tag/Forms)           |
| `DELETE /v1/newsletters/{id}`                               | [Forms](/api-reference#tag/Forms)           |
| `POST /v1/contact-forms` (public)                           | [Forms](/api-reference#tag/Forms)           |
| `GET /v1/contact-forms`                                     | [Forms](/api-reference#tag/Forms)           |
| `DELETE /v1/contact-forms/{id}`                             | [Forms](/api-reference#tag/Forms)           |

## Forms — newsletter & contact

Most AI-built sites need two simple capture surfaces: an email-collector for
newsletters and a free-form contact form. Neural Draft ships both as
first-class endpoints — no schemas to design, no spam-handling glue, and free
(no credits charged, since these are storage + delivery, not AI).

### Public submit

The submit endpoints are public. Authenticate the project with a server-side
API key in `X-NeuralDraft-Project-Key` (or pass `?project_id=` for
unauthenticated client-side embeds where you don't want to ship a key):

```bash theme={null}
# Newsletter — idempotent. Re-submitting an existing email returns 200 with
# status="already_subscribed" instead of an error.
curl -X POST https://api.neuraldraft.io/v1/newsletters/subscribe \
  -H "X-NeuralDraft-Project-Key: $NEURALDRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com","app_lead":false}'

# Contact form — `data` is free-form JSON for whatever your form collects;
# IP and user-agent are merged in server-side for spam triage.
curl -X POST https://api.neuraldraft.io/v1/contact-forms \
  -H "X-NeuralDraft-Project-Key: $NEURALDRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "subject": "Demo request",
    "message": "We are evaluating Neural Draft for our next launch.",
    "data": {"company":"Acme","headcount":12}
  }'
```

### Reading submissions

Form submissions are not in the outgoing-webhook event whitelist today —
poll the admin list endpoints (`GET /v1/newsletters`, `GET /v1/contact-forms`)
or use the MCP tools `list_newsletter_subscribers` /
`list_contact_form_submissions` to inspect what came in. We may add
`newsletter.subscribed` / `contact_form.submitted` topics to the webhook
whitelist in a future release; subscribe to the [changelog](/changelog) if
you'd like to be notified.

### Admin list & delete

Listing and deleting submissions requires an API key with `forms:read` /
`forms:write` scope (or `*`). Both list endpoints support `?page` /
`?page_size` (max 200) and `?search=`; the newsletter list also accepts
`?app_lead=true` to filter out trial leads from your real subscriber list.
