# Content API

Your site's server code reads its pages, SEO tags and sitemap from SuperDuperSEO, then renders them on your own domain. The [SDK](https://superduperseo.com/docs/sdk) wraps all of this.

- **Base URL:** `https://content.superduperseo.com/v1/`
- **Auth:** `Authorization: Bearer <site key>` (`sds_live_...`). The key selects the site. Create it with the MCP tool `create_site_key`, or in the app on the site's Content API page.
- **Server side only.** Keep the key in your server environment (e.g. `SUPERDUPERSEO_CONTENT_KEY`), never in code or the browser. There is no CORS, so browsers cannot call the API.
- **Never public, never indexed.** Without a valid key every request answers 401, and every response carries `X-Robots-Tag: noindex, nofollow, noarchive`. Your domain is the only public copy.
- **Caching:** responses are `Cache-Control: private, max-age=60` with an `ETag`; send `If-None-Match` to get `304`. Cache on your side and refresh on our [webhook](https://superduperseo.com/docs/webhooks).
- **Limits:** 20 requests a second per key (bursts of 200), then `429` with `Retry-After`.
- **Errors:** `401` wrong or revoked key; `404` no such file (a post that does not exist); `429` slow down.

## Serving a request

For every path under your blog, in this order:

1. **Redirects first.** If the path is in `redirects.json`, answer `301` to `to`, or `410` when `to` is null.
2. **The page.** If `posts/{slug}.json` exists and its `path` is the request path, render it.
3. **Otherwise a real `404`,** never a `200` page that says "not found".

Render on the server:
- `<title>` from `seo.title`;
- `head_html` in the `<head>`, removing your own description, canonical, Open Graph and Article JSON-LD so nothing is duplicated;
- the title as the only `<h1>`;
- then `html`.

## Files

### `site.json` (json)

The site: name, routes, feed, structured data.

| Field | Type | Meaning |
|---|---|---|
| `name` | `string` | The site's name (as in og:site_name and structured data) |
| `url` | `string` | The site's home URL |
| `language` | `string` | Default content language |
| `description` | `string \| null` | What the site is about |
| `logo` | `string \| null` | Logo URL |
| `routes` | `{ index, post, author, topic }` | Where each kind of page lives, e.g. post: "/blog/{slug}" |
| `feed` | `string` | The RSS feed URL to serve |
| `json_ld` | `object` | Organization and WebSite structured data, for your home page |

### `posts/index.json` (json)

Published pages, newest first, 100 per file: { page, pages, total, next, posts: [summary] }. Follow `next` (posts/index-2.json...).

| Field | Type | Meaning |
|---|---|---|
| `id` | `string` | Stable id of the page |
| `collection` | `"blog" \| "pages"` | Blog post, or a service/SaaS/product page |
| `slug` | `string` | The public slug |
| `path` | `string` | The exact path to serve it at, e.g. /blog/my-post (no trailing slash) |
| `url` | `string` | The canonical URL on your domain |
| `title` | `string` | Title, rendered as the page's only <h1> |
| `summary` | `string \| null` | One or two sentences: the meta description and the answer first |
| `feature_image` | `{ url, alt, width, height, srcset, credit: { name, url, source } } \| null` | Feature image, with dimensions and srcset; show the credit when present |
| `author` | `{ slug, name, url } \| null` | The byline's author |
| `topics` | `Array<{ slug, name, url }>` | Topic (cluster) the post belongs to |
| `reading_time` | `number` | Minutes |
| `published_at` | `ISO 8601` | First publish date (datePublished) |
| `updated_at` | `ISO 8601` | Last change to the content (dateModified, sitemap lastmod) |

### `posts/{slug}.json` (json)

One page with everything to render it.

| Field | Type | Meaning |
|---|---|---|
| `id` | `string` | Stable id of the page |
| `collection` | `"blog" \| "pages"` | Blog post, or a service/SaaS/product page |
| `slug` | `string` | The public slug |
| `path` | `string` | The exact path to serve it at, e.g. /blog/my-post (no trailing slash) |
| `url` | `string` | The canonical URL on your domain |
| `title` | `string` | Title, rendered as the page's only <h1> |
| `summary` | `string \| null` | One or two sentences: the meta description and the answer first |
| `feature_image` | `{ url, alt, width, height, srcset, credit: { name, url, source } } \| null` | Feature image, with dimensions and srcset; show the credit when present |
| `author` | `{ slug, name, url } \| null` | The byline's author |
| `topics` | `Array<{ slug, name, url }>` | Topic (cluster) the post belongs to |
| `reading_time` | `number` | Minutes |
| `published_at` | `ISO 8601` | First publish date (datePublished) |
| `updated_at` | `ISO 8601` | Last change to the content (dateModified, sitemap lastmod) |
| `html` | `string` | The body: no <h1>, headings with ids, byline, calls to action and footer included. Render it as is, on the server |
| `markdown` | `string` | The same body as markdown (for /blog/{slug}.md or other renderers) |
| `toc` | `Array<{ id, text, level }>` | Table of contents from the h2/h3 headings |
| `word_count` | `number` | Words in the body |
| `language` | `string` | Content language, for <html lang> |
| `version` | `number` | Increases with every content change |
| `content_hash` | `string` | Changes when the content changes |
| `seo` | `{ title, description, canonical, robots, og, twitter }` | Everything for the <head>, as fields |
| `json_ld` | `object` | Structured data (@graph: Organization, WebSite, WebPage, BlogPosting, Person, BreadcrumbList, FAQPage...) |
| `head_html` | `string` | Ready-made <head> tags: description, canonical, robots, Open Graph, Twitter and the JSON-LD. Not the <title>: set it from seo.title |
| `breadcrumbs` | `Array<{ name, url }>` | Home > Blog > Post |
| `related` | `Array<summary>` | Related posts (same topic first) |
| `alternates` | `Array<{ hreflang, url }>` | Other languages of the page (empty until translations exist) |

### `authors/index.json` (json)

Authors: { authors: [{ slug, name, url, job_title, posts }] }.

### `authors/{slug}.json` (json)

An author page: name, job_title, bio, image, profiles, posts, seo, json_ld (ProfilePage), head_html.

### `topics/index.json` (json)

Topics: { topics: [{ slug, name, url, posts, indexable }] }.

### `topics/{slug}.json` (json)

A topic page: name, posts, seo (noindex under 3 posts), json_ld (CollectionPage), head_html.

### `redirects.json` (json)

{ redirects: [{ from, to, status }] }: answer 301 to `to`, or 410 when `to` is null, before rendering anything.

### `sitemap.xml` (xml)

Indexable URLs only, honest lastmod, image entries. Serve at /sitemap.xml or merge into yours.

### `feed.xml` (xml)

RSS 2.0, newest 50 posts. Serve at site.feed.

### `llms.txt` (text)

For AI search engines. Serve at /llms.txt.

### `search.json` (json)

For on-site search: { entries: [{ title, summary, url, topics }] }.

### `changes.json` (json)

What changed, newest first: { cursor, changes: [{ id, event, path, url, version, at }] }. For incremental builds and missed webhooks.

### `indexnow.txt` (text)

The site's IndexNow key: serve it as text at /{key}.txt.
