Docs
Developer guides

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.

View as Markdown

Capa can turn your entries into three formats that search engines and AI tools read well:

ExportRouteUse it for
JSON-LD/v2/seo/{model}/{id}/json-ld, /v2/seo/{model}/json-ldStructured data in a page's <head>.
Markdown/v2/seo/{model}/{id}/markdownA 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-bundlePlain 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=true to 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_postArticle
newsNewsArticle
postBlogPosting
product, products, itemProduct
event, events, workshop, webinar, conferenceEvent
person, people, author, team, team_member, employeePerson
organization, companyOrganization
location, place, venuePlace
storeLocalBusiness
restaurantRestaurant
recipeRecipe
faqFAQPage
review, testimonialReview
job, job_postingJobPosting
serviceService
video, image, audio, podcastVideoObject, ImageObject, AudioObject, PodcastEpisode
question, book, movie, courseQuestion, Book, Movie, Course
anything elseThing

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:

FieldProperty
title, namename
descriptiondescription
content, bodyarticleBody
image, images, thumbnail, featured_imageimage
authorauthor
slugidentifier
urlurl
Images and video on 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: title becomes headline, and summary or excerpt becomes description.
  • Product: price and currency become an Offer, plus sku, brand, category, availability, rating and review_count.
  • Event: start_date, end_date, location, venue, organizer, ticket_url and ticket_price.
  • Person: first_name, last_name, email, phone, job_title, company, bio and avatar.
  • Recipe: ingredients, instructions, prep_time, cook_time, total_time, servings, cuisine and calories.

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, featured or priority field that holds plain text, a number or true/false.
  • Heading: the first of title, name or headline.
  • Quote: the first of excerpt or summary, cut to 160 characters.
  • Body: the first of content, body, text or description.
  • Sections: other long Markdown or HTML fields, and non-empty lists, each under its field's name.
ParameterDefaultWhat it does
frontmattertruefalse leaves the front matter out.
metadatatruefalse 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.

  • text joins the entry's title and its text fields (content, body, text, description, excerpt, summary), with HTML removed.
  • metadata adds every short field: text, numbers, true/false, dates, options and colours.
  • embedding_text is a shorter line built for embedding models.
ParameterOne modelEvery model
limitentries, 1 to 1,000, default 500entries per model, 1 to 200, default 100
maxTextLengthcharacters of text, default 8,000default 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.

StatusBodyWhen
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.
  • Legacy API for the rest of the /v2 surface.
  • Keys for the two key families.