SEO and AI exports
Get schema.org JSON-LD, a Markdown copy of an entry, and a plain-text bundle of your content for search and retrieval.
Capa can turn your entries into three formats that search engines and AI tools read well:
| Export | Route | Use it for |
|---|---|---|
| JSON-LD | /v2/seo/{model}/{id}/json-ld, /v2/seo/{model}/json-ld | Structured data in a page's <head>. |
| Markdown | /v2/seo/{model}/{id}/markdown | A Markdown copy of a page, such as /blog/my-post.md or an llms.txt entry. |
| AI bundle | /v2/seo/{model}/ai-bundle, /v2/seo/ai-bundle | Plain text and metadata, ready for search indexing or embeddings. |
These routes are part of the legacy /v2 surface. They take the legacy key your site already uses (pk_, sk_, or an older key with no prefix). A cap_ key is refused with 401.
None of them call an AI model. They reshape your content and nothing else.
Drafts
The key decides what you get, as on every read:
- A production key (
pk_) gets published content only. No parameter changes that. - A development key (
sk_) gets drafts by default. Add?published=trueto see exactly what is live.
JSON-LD for one entry
curl -G https://cdn.capacms.com/v2/seo/article/<entryId>/json-ld \
-H "x-api-key: $CAPA_KEY" \
--data-urlencode 'baseUrl=https://northpeak.example'{
"@context": "https://schema.org",
"@type": "Article",
"@id": "https://northpeak.example/api/article/<entryId>",
"identifier": "winter-field-guide-2026",
"headline": "Winter Field Guide 2026: Layering Above Treeline",
"description": "How to stay warm and dry above treeline this winter.",
"articleBody": "…",
"dateCreated": "2026-09-30T14:02:11.000Z",
"dateModified": "2026-10-01T09:15:40.000Z",
"datePublished": "2026-10-01T09:15:40.000Z",
"name": "Winter Field Guide 2026: Layering Above Treeline",
"url": "https://northpeak.example/article/winter-field-guide-2026"
}The response is application/ld+json. Put it in your page inside <script type="application/ld+json">.
identifier is the entry's id, or its slug field when the model has one. name falls back to the entry's title.
How the type is chosen
The schema.org @type comes from the model's namespace:
| Namespace | @type |
|---|---|
article, blog, blog_post | Article |
news | NewsArticle |
post | BlogPosting |
product, products, item | Product |
event, events, workshop, webinar, conference | Event |
person, people, author, team, team_member, employee | Person |
organization, company | Organization |
location, place, venue | Place |
store | LocalBusiness |
restaurant | Restaurant |
recipe | Recipe |
faq | FAQPage |
review, testimonial | Review |
job, job_posting | JobPosting |
service | Service |
video, image, audio, podcast | VideoObject, ImageObject, AudioObject, PodcastEpisode |
question, book, movie, course | Question, Book, Movie, Course |
| anything else | Thing |
The match is exact, after lowercasing and turning - into _. An articles model is Thing, not Article.
How fields are mapped
Fields map to schema.org properties by their namespace. Some names work on every type:
| Field | Property |
|---|---|
title, name | name |
description | description |
content, body | articleBody |
image, images, thumbnail, featured_image | image |
author | author |
slug | identifier |
url | url |
cdn.capacms.com can appear in search, so they work in rich results. Other files there, such as PDFs, are sent with X-Robots-Tag: noindex and stay out of search results.Some types add their own:
- Article:
titlebecomesheadline, andsummaryorexcerptbecomesdescription. - Product:
priceandcurrencybecome anOffer, plussku,brand,category,availability,ratingandreview_count. - Event:
start_date,end_date,location,venue,organizer,ticket_urlandticket_price. - Person:
first_name,last_name,email,phone,job_title,company,bioandavatar. - Recipe:
ingredients,instructions,prep_time,cook_time,total_time,servings,cuisineandcalories.
A field with no mapping is left out. Image fields become an ImageObject, and a relation becomes a reference by @id.
baseUrl
Pass your site's origin as baseUrl. Capa uses it for @id, and, when the model has a slug field, for url as <baseUrl>/<namespace>/<slug>. Without baseUrl there is no url.
If your URLs are shaped differently, overwrite url in your own code before you print it.
JSON-LD for a list
curl -G https://cdn.capacms.com/v2/seo/article/json-ld \
-H "x-api-key: $CAPA_KEY" \
--data-urlencode 'baseUrl=https://northpeak.example' \
--data-urlencode 'limit=20'This returns an ItemList with one ListItem per entry. limit is 1 to 500, and the default is 100.
Markdown for one entry
curl https://cdn.capacms.com/v2/seo/article/<entryId>/markdown \
-H "x-api-key: $CAPA_KEY"---
id: "<entryId>"
model: "article"
title: "Winter Field Guide 2026: Layering Above Treeline"
slug: "winter-field-guide-2026"
status: "published"
created_at: "2026-09-30T14:02:11.000Z"
updated_at: "2026-10-01T09:15:40.000Z"
published_at: "2026-10-01T09:15:40.000Z"
tags:
- "winter"
---
# Winter Field Guide 2026: Layering Above Treeline
> How to stay warm and dry above treeline this winter.
The body of the entry, with HTML converted to Markdown.The response is text/markdown, sent as a download named <model>-<slug>.md.
What goes where:
- Front matter: the entry's id, model, title, slug, status, dates and tags, plus any
author,category,featuredorpriorityfield that holds plain text, a number or true/false. - Heading: the first of
title,nameorheadline. - Quote: the first of
excerptorsummary, cut to 160 characters. - Body: the first of
content,body,textordescription. - Sections: other long Markdown or HTML fields, and non-empty lists, each under its field's name.
| Parameter | Default | What it does |
|---|---|---|
frontmatter | true | false leaves the front matter out. |
metadata | true | false leaves the dates and tags out of the front matter. |
The AI bundle
One model:
curl -G https://cdn.capacms.com/v2/seo/article/ai-bundle \
-H "x-api-key: $CAPA_KEY" \
--data-urlencode 'limit=200'Every model in the project:
curl https://cdn.capacms.com/v2/seo/ai-bundle -H "x-api-key: $CAPA_KEY"{
"schema": { "namespace": "article", "modelName": "Article",
"fields": [ { "namespace": "title", "name": "Title", "type": "string", "required": true } ] },
"documents": [
{ "id": "<entryId>",
"modelNamespace": "article",
"text": "Winter Field Guide 2026: Layering Above Treeline\n\nHow to stay warm and dry above treeline this winter.\n\n…",
"metadata": { "status": "published", "tags": ["winter"], "title": "Winter Field Guide 2026: Layering Above Treeline",
"slug": "winter-field-guide-2026", "createdAt": "…", "updatedAt": "…", "publishedAt": "…" },
"embedding_text": "[Article] Winter Field Guide 2026: Layering Above Treeline | How to stay warm and dry … | Tags: winter" } ],
"stats": { "documentCount": 1, "totalTextLength": 1840, "avgTextLength": 1840 }
}The project-wide bundle returns schemas (one per model) instead of schema, and stats with modelCount, documentCount and totalTextLength.
textjoins the entry's title and its text fields (content,body,text,description,excerpt,summary), with HTML removed.metadataadds every short field: text, numbers, true/false, dates, options and colours.embedding_textis a shorter line built for embedding models.
| Parameter | One model | Every model |
|---|---|---|
limit | entries, 1 to 1,000, default 500 | entries per model, 1 to 200, default 100 |
maxTextLength | characters of text, default 8,000 | default 4,000 |
Caching and errors
With a production key, these routes are cached at the edge for 60 seconds and purged when you publish. Responses to a development key, and errors, are never cached.
| Status | Body | When |
|---|---|---|
401 | {"error":"API key required"} | No x-api-key header. |
401 | {"error":"Invalid API key"} | Unknown, deactivated or expired, or a cap_ key. |
402 | {"success":false,"error":"…"} | The project has no active subscription. |
404 | {"error":"Model not found"} | No model with that namespace. |
404 | {"error":"Instance not found"} | No such entry, or one this key cannot see. |
Related
- Legacy API for the rest of the
/v2surface. - Keys for the two key families.