Docs Template
Generate a documentation site from Markdown pages in one request.
Send Pages
Send a name, a theme, navigation groups, and Markdown pages, and Manifest builds a documentation site from its docs template. There's no build step on your side.
The template is a shortcut. For your own navigation, files, images, and design, publish your own build instead.
Save the content as content.json:
content.json {
"template": "docs",
"env": "staging",
"site": {
"name": "Acme Help",
"description": "Answers for Acme customers.",
"theme": {
"tokens": { "color-brand-surface": "oklch(55% 0.2 260)" },
"dark": { "color-brand-surface": "#6677ff" }
}
},
"groups": [{ "slug": "start", "description": "First steps with Acme." }],
"articles": [
{ "group": "Getting started", "group_slug": "start", "title": "Welcome", "slug": "welcome", "markdown": "Hello from Acme.", "order": 1 },
{ "group": "Billing", "title": "Invoices", "slug": "invoices", "markdown": "## Download\n\nUse the portal.", "icon": "lucide:receipt" }
]
}
Then send it:
curl -X POST https://api.manifestx.dev/v1/projects/proj_…/content \
-H "Authorization: Bearer $MANIFEST_ACCOUNT_KEY" \
-H "Idempotency-Key: help-center-sync-0193" \
-H "Content-Type: application/json" \
-d @content.json
The site answers 201 Created and is live on that environment's address straight away:
{
"deployment": { "id": "dep_…", "env": "staging", "status": "ready", "files": 10, "bytes": 26676 },
"template": { "name": "docs", "version": "0.1.2" },
"url": "https://acme-docs-staging.manifestx.ai",
"request_id": "…"
}
The Invoices page is now at /docs/billing/invoices on that address. Promote, rollback, and the deployment list work exactly as for your own builds.
Good to know:
- Projects your keys created only. On your other projects, content answers
422 not_api_project, so it never replaces a project you built another way. Publish a zip to those instead. - Production ingests also return a
prerenderobject, usually{ "status": "pending" }. Manifest renders every page to static HTML in the background, then swaps that version in. - A
warningwith codepost_publish_incompletemeans the site is live but a follow-up step, like setting up the default address, didn't finish. Don't resend. It completes on the next publish that changes the content, andurlcan benulluntil then.
Fields
| Field | Type | Required | Notes |
|---|---|---|---|
template |
string | yes | docs. |
env |
string | no | staging (default) or production. |
site.name |
string | yes | Page title, header, and app name. |
site.description |
string | no | Meta description and the landing-page intro. |
site.theme |
object | no | tokens (light) and dark: theme token names mapped to CSS values. See Theme Values. |
groups |
array | no | { slug, description? } per group. Listed groups come first, in this order, with their description. |
articles |
array | yes | The pages. At least one. |
Each entry in articles is one page:
| Field | Type | Required | Notes |
|---|---|---|---|
group |
string | yes | Group name shown in the navigation. |
group_slug |
string | no | The group's URL segment. Defaults to the group name, lowercased, with hyphens. |
title |
string | yes | The page title. |
slug |
string | yes | The page's URL segment. See Slugs. |
markdown |
string | yes | The page body. If its first line isn't a # heading, the title is added as one. |
icon |
string | no | An Iconify name, like lucide:receipt. Defaults to lucide:file-text. |
order |
number | no | Position within the group. Pages without one follow, in the order sent. |
Any field not listed here is refused, and so is Markdown with control characters. Images and other files aren't part of the request, so link to them by full URL.
Unchanged Content
On staging, content identical to what's live there answers 200 OK with unchanged: true. No deployment is created, and nothing counts toward the daily publish limit. So a scheduled sync can send everything every time, and the site rebuilds only when something changed.
{
"unchanged": true,
"deployment": { "id": "dep_…", "env": "staging", "status": "ready", "files": 10, "bytes": 26676 },
"template": { "name": "docs", "version": "0.1.2" },
"url": "https://acme-docs-staging.manifestx.ai",
"request_id": "…"
}
Good to know:
- Order matters, formatting doesn't. Key order and whitespace are ignored, but the order of
articlesandgroupscounts. - It compares with what's live, not your last request. After a file update or rollback to a version your content didn't build, the next ingest deploys.
- Production always deploys, even when nothing changed.
Slugs
A page is served at /docs/<group_slug>/<slug>. Slugs are lowercase words joined by single hyphens, like payment-methods.
- A page slug is unique across all groups, not only its own.
- A page slug can't match a group slug. Name a
billingpage in abillinggroupbilling-overviewinstead. - Neither can be
docs, the segment the site lives under. - Each
group_slughas one group name, and two groups can't share a name.
Theme Values
tokens and dark map the template's theme token names to CSS values. Write names with or without the leading --, but not both for one token.
- Allowed: colors, numbers, lengths, quoted font names, and the functions
rgb,rgba,hsl,hsla,hwb,lab,lch,oklab,oklch,color,color-mix,var,calc,min,max,clamp, andlight-dark. - Refused:
url(), comments, semicolons, braces, unclosed quotes or parentheses, and values over 200 characters.
Template Version
A project's first successful ingest pins the docs template version it used. Later ingests keep that version, so a template update never changes a live site unasked. The version is in every response.
Limits
| Limit | Value |
|---|---|
| Request body | 12 MB |
| Pages per request | 1,000 |
| Markdown per page | 512 KB |
| Markdown per request, in total | 10 MB |
Entries in groups |
100 |
Slug and group_slug |
80 characters, including a group_slug made from the group name |
| Group name, page title | 200 characters |
| Group description, site description | 1,000 characters |
| Site name | 120 characters |
| Theme tokens | 100 each for light and dark |
| Theme value | 200 characters |
Retries
Send an Idempotency-Key with every request. A retry with the same key and body returns the first answer and creates no second deployment. See Idempotency.
Errors
Every error has the same shape. A content problem answers 422 with a field naming the page and property, and no deployment is created:
{
"error": "invalid_content",
"message": "articles[1].slug: invalid slug \"Bad Slug\".",
"field": "articles[1].slug",
"request_id": "…"
}
| Status | error |
Meaning |
|---|---|---|
| 400 | invalid_input |
The body isn't UTF-8 JSON or an object, has an unknown top-level field, or template or env is wrong. |
| 403 | plan_required |
The project's plan doesn't include publishing to that environment. |
| 403 | storage_cap_reached |
The site would take the account over its storage cap. Nothing changed. See Limits. |
| 404 | not_found |
No such project, or the key can't reach it. |
| 413 | payload_too_large |
The body is over 12 MB. |
| 422 | invalid_content |
A slug, size, theme value, or page field is invalid. field says which. |
| 422 | not_api_project |
The project wasn't created through the API. Publish a zip to it instead. |
| 429 | publish_limit_reached |
A Free project's daily publish limit is reached. Wait the seconds in Retry-After. |
| 500 | ingest_failed |
The deployment failed before going live. What was live is unchanged. Retry. |
| 503 | template_unavailable |
The docs template couldn't load. Retry shortly. |
Article does not exist
There is no documentation at this path.