Skip to content
Menu
Search
Browse documentation & this page

Developer Guide

GraphQL

Query pages, their blocks and what they link to in one request, with types generated from your content type classes, and know the limits a public endpoint enforces.

Cendia serves the same published content over GraphQL as it does over the REST Delivery API. Use it when a front end needs a page and what the page is made of in one request: its blocks, the blocks inside those, its images, its menu.

The short version: POST a query to /api/graphql. There is nothing to configure, and the schema is generated from your content type classes.

{
  page(path: "/news/launch") {
    name
    path
    ... on ArticlePage {
      heading
      body
      hero { ... on Image { url focalPoint { objectPosition } } }
      zone { __typename ... on StandardBlock { heading body } }
    }
    children(navigationOnly: true) { name path }
  }
}

The two endpoints

Endpoint Serves Who may call it
/api/graphql What visitors are served: published, past its publish date, not expired. Anyone.
/preview/graphql The same queries, with drafts in place of published versions. A signed-in editor, or a request carrying a preview token.
/preview/graphql/r/{releaseId} The same, as the site will be when that release is published. As above.

Both accept a POST with a JSON body (query, variables, operationName). The Delivery endpoint also accepts a GET with the same three in the query string. Send one operation per request.

There are no mutations. Content is changed in the CMS.

The schema is your code

  • One type per content type, named for its class: ArticlePage, StandardBlock, Image.
  • One field per property, in camel case: Heading becomes heading.
  • Every content field is nullable, including one marked [Required]. Validation decides whether a page may be published, never the shape of the API, so an administrator changing a rule cannot break your generated types.
  • It changes when you deploy, and at no other time. Generate client types against it with confidence.

Every item also has id, contentType, culture, version and name. A page adds path, publishedAt, alternates, children, ancestors, redirect and includeInSearch; a media item adds url, fileName, mediaType, mimeType, byteSize and focalPoint. If one of your properties has the same name as one of these, yours is exposed with Field on the end (nameField).

Three interfaces let you query without knowing the type: Page, Block and Media, all of which are Content.

How field types map

Property type GraphQL type
string, HtmlString, TextArea, Url, DropDown, RadioList String
bool Boolean
int, Integer, Counter Int
decimal, Number Decimal
DateTimeOffset, DateTimePicker DateTime
Checklist, List<string> [String!]
ContentZone [Block!]
ContentSelector, ContentReference Content
MediaReference Media
Link Link (kind, text, title, url, openInNewTab, page)
Anything else Any (the stored value)

Body text arrives ready to print: links an author made to other pages already carry their current address.

Types with no class

A block built in Settings > Designer, or any content whose class is not compiled into this site, appears as GenericBlock, GenericPage or GenericMedia. Read it through fields, which holds every stored value by field id:

{ content(id: "…") { __typename name fields } }

Root fields

Field Returns
page(path, culture) The page at a path. Omit path for the start page.
content(id, culture) Any page, block or media item by id.
pages(culture, contentTypes, under, where, orderBy, searchable, first, page) A filtered, ordered, paged listing: items, total, withheld.
site The website for this host name: its name, default language, languages and hosts.

pages takes the same filters as the REST query endpoint: where: ["category:eq:sport"], on fields declared [Indexed]. orderBy is published, -published (the default), order, name or -name.

Building a search from it

An author can switch Include in Search off on the Search tab of a page's Settings. The page is still public and still listed; the switch only says it should not appear in your site's own search results (an archive, a thank-you page). It is not a robots directive and does not affect search engines.

  • Every page reports it as includeInSearch. Read it if you feed your own search index.
  • Pass searchable: true to pages when the answer is shown as search results, and those pages are left out. The REST query endpoint takes ?searchable=true and reports the same field.

A page is included in every language until an author says otherwise. The switch is for the whole page; beneath it an author can leave a single language out. includeInSearch is the answer for the language you asked in, with both already applied.

When something is null

A query answers null wherever the REST API would answer 404 or 410: no such page, not published in that language, publish date not reached, expired, or the caller may not read it. It never says which. In a listing, withheld counts the results left out for access rights.

Audiences

By default a query is answered for a visitor in no audience, so personalized regions and zone placements show their everyone version. If your front end has worked out its visitor's audiences, say so:

  • on a GET, add &audiences=vip,returning to the URL;
  • on a POST, send the header X-Cendia-Audiences: vip,returning.

Limits

/api/graphql is public, and a query is written by the caller. These limits are on by default. A request that crosses one is refused with an error naming the limit, never answered short.

Limit Default Setting
How deeply a query may nest 12 levels Cendia:GraphQL:MaxDepth
Content items one request may load (pages, blocks and media together) 500 Cendia:GraphQL:MaxItems
Items in one listing or children 100 Cendia:GraphQL:MaxPageSize
Length of the query text 16,000 characters Cendia:GraphQL:MaxQueryLength
How long a request may run 10 seconds Cendia:GraphQL:TimeoutSeconds

A page with thirty blocks and their images is about sixty items and four levels, so an ordinary front end never meets these. Each request also counts once against your license's API allowance, the same as a REST call, and the site's request rate limit applies.

"Cendia": {
  "GraphQL": {
    "Enabled": true,
    "MaxDepth": 12,
    "MaxItems": 500,
    "AllowIntrospection": true,
    "AllowGet": true
  }
}

Set Enabled to false and both endpoints answer 404. Set AllowIntrospection to false to stop a public endpoint describing its own schema; generate client types against a development build.

Caching

  • A GET to /api/graphql that answers without errors is sent with Cache-Control: public, max-age=60, must-revalidate, the same window as a REST Delivery response, so a CDN can cache it by URL.
  • A POST is never cached.
  • Nothing on the preview endpoints is cached.

Inside the server, published content is remembered between requests (see How pages are cached), so a repeated query reads almost nothing from the database.

Troubleshooting

What you see Likely cause
page is null for a page that exists Not published in that language, gated by a publish date, expired, or restricted. Check it over REST: the status code says which of 404 and 410.
"exceeds the max allowed execution depth" The query nests deeper than MaxDepth. Usually children inside children; ask for the levels you need.
An error with code CENDIA_LIMIT The request asked for more than MaxItems content items. Page the listing, or raise the limit.
A field is Any where you expected a type Its property type has no GraphQL mapping yet. The stored value is returned as it is.
A new content type is missing from the schema The schema is built at startup. Deploy, or restart the site.
401 or a redirect from /preview/graphql It needs an editor's sign-in or a preview token, like the REST preview API.