Features Apograph CMS on GitHub

Reading entries

Content plugin @apograph/content-server@apograph/content-admin

Listing, fetching, searching, sorting, paging and sparse fieldsets on the public read endpoints.

Documents 0.5.2 Updated Edit this page Report a problem

On this page
Example language
curl 'https://cms.example.com/api/v1/content/article?pageSize=10&sort=-publishedAt' \
-H 'Authorization: Bearer apograph_…'
const response = await fetch(
  'https://cms.example.com/api/v1/content/article?pageSize=10&sort=-publishedAt',
  { headers: { Authorization: 'Bearer ' + process.env.APOGRAPH_TOKEN } }
);
const { items, total } = await response.json();
query Latest {
  articles(pageSize: 10, sort: "-publishedAt") {
      total
      items {
          id
          title
          slug
          publishedAt
      }
  }
}

Listing

GET /api/v1/content/:typeName

ParameterDefaultEffect
page11-based page number.
pageSize25Rows per page, capped at 100.
sort-updatedAtA whitelisted column, - prefixed for descending.
searchFree-text across the type’s text-like columns.
filterA structured filter tree as JSON. See filtering.
fieldsallComma-separated field names to return in values.
localedefaultWhich locale to read, on a localized type.
statuspublisheddraft and any require a full token.

The response is a page envelope:

{ "items": [], "page": 1, "pageSize": 25, "total": 128 }

Sorting

The whitelist is the envelope timestamps, publishedAt, locale, and the type’s own scalar fields. An unknown key falls back to -updatedAt rather than erroring, and id is always the final tiebreaker — so paging is stable even when the sort column has ties.

?sort=title        ascending
?sort=-publishedAt descending

?search= is a case-insensitive match across the type’s text-like columns — text, richtext and select. LIKE metacharacters are matched literally, so a search for 100% finds 100% rather than everything.

Search is ILIKE, not a search engine

There is no index behind ?search=, no relevance ranking, no stemming, no typo tolerance and no highlighting. It is a substring match, and on a large table with large rich-text bodies it is a sequential scan. If you need real search, put one in front of Apograph and feed it from the API.

Sparse fieldsets

?fields=title,slug narrows what comes back in values. It narrows the SQL projection too, so an unselected rich-text column is never read from the database, let alone serialised — which is the difference between a listing page that is fast and one that is not.

?fields=title,slug,publishedAt

The envelope is always returned and is not selectable. An unknown name is a 400, and so is naming a relation or media field — this parameter cannot return those, and silently ignoring the request would be worse.

Reading one entry

GET /api/v1/content/:typeName/:id

Takes the same expansion, fields and locale parameters as the list. A missing entry, an entry in another workspace and an unpublished entry are all 404 to a read token.

Localized reads

On a localized type, ?locale= chooses the language, and the default locale is used when it is absent. An unknown slug is a 400 — never a silent read of the default.

?locale=de

Scoping is strict: a translation group with no row in the requested language is simply absent from the results.

Addressing by translation group

Every id-addressed route has a group-addressed twin:

GET /api/v1/content/article/group/:localeGroupId?locale=de

This is what a front end usually wants. A story has one stable group id across every language, so a URL like /de/article/<group> resolves without keeping a per-locale id map.

RouteReturns
GET …/group/:groupIdThat group’s entry for the requested locale
GET …/group/:groupId/relations/:fieldOne relation field’s links, by group
GET …/group/:groupId/mediaThat entry’s media assets, by group
GET …/group/:groupId/translationsThe group’s other locale rows

Sibling translations

?translations=preview attaches an entry’s sibling translations — the rest of its localeGroupId group — as translations. It is a 400 on a type that is not localized.

That is how you build a language switcher: one read gives you the entry and every language it exists in.

Discovering the schema

RouteReturns
GET /api/v1/content-typesEvery type this token can read
GET /api/v1/content-types/:nameOne type’s field schema

Both are pruned to the workspace’s granted types, so this is also the honest answer to “what can this token see”. A type the workspace was not granted is not listed and is not reachable by name.

Caching

Reads are ordinary GET requests and cache like any other. Apograph sends no Cache-Control of its own, so a CDN in front of this API caches on whatever max-age you give it. For an event rather than a timer — re-render a page the moment it is published — subscribe a webhook: see reacting to changes, and preview and drafts for what the API does and does not cache.

  • Filtering — the structured ?filter= tree, every operator, and relation hops.
  • Expanding relations and media — turning a shallow read into one that carries its author and cover image.
  • Preview and drafts — reading unpublished rows with a full token, without shipping it to the browser.