Skip to content

GraphQL

GraphQL is a way to ask an API for data by describing the shape of the answer you want. Instead of calling several addresses, you send one query to one address, and the answer has exactly the fields you asked for, nothing more.

You do not need GraphQL to build a website with Manablox. The SDK with its REST transport is simpler for beginners. GraphQL is worth learning when your team already uses it, or when a page needs only a few fields of large documents.

Every query is a POST request to /graphql on the public API:

ProjectAddress
local, while you develophttp://localhost:3100/graphql
On a serverhttps://<your public API domain>/graphql

The request body is JSON with a query and, optionally, variables. The public API only returns published content of its one space, and it needs no login or key.

A query lists the fields you want, nested like the answer. This one asks for the title and permalink of the document at /about:

{
contentByPermalink(permalink: "about") {
title
permalink
}
}

The answer mirrors the query:

{
"data": {
"contentByPermalink": {
"title": "About",
"permalink": "about"
}
}
}

Send it from a terminal with curl:

Terminal window
curl -X POST http://localhost:3100/graphql -H 'content-type: application/json' -d '{"query":"{ contentByPermalink(permalink: \"about\") { title permalink } }"}'

You should see the JSON above. If you see "contentByPermalink": null, nothing is published at about in the space the public API serves. If curl cannot connect, start the public API with pnpm dev:public in your CMS project.

A schema is the list of everything you can ask for. Manablox builds it from your content model, so it changes when you add a content type or a field in the admin:

  • Each content type becomes a GraphQL type, named in PascalCase: page becomes Page, blog-post becomes BlogPost.
  • Each block type becomes a type too: teaser becomes Teaser.
  • Field names are written in camelCase: meta_description becomes metaDescription.
  • Fields that only some roles may read are left out of the public schema.

Every content type shares a set of basic fields, through an interface called ContentNode:

FieldMeaning
idThe document’s id
typeNameThe content type’s technical name, for example page
title, slug, permalinkTitle, slug and full path
localeThe language of this version
statusThe publishing status
publishedAt, updatedAtDates

Content types also have parent and children, to walk the page tree.

Fields that belong to one type only are asked for with ... on TypeName { }. This is called an inline fragment; it means “if the document is a Page, also give me these fields”.

QueryWhat it returns
contentByPermalink(permalink: "...", locale: "...")The document at a path. "" is the home page
content(id: "...")One document by its id
contents(type, parentId, under, search, locale, limit, offset)A list of documents. limit defaults to 25
menu(name: "main", locale: "...")A menu with its entries
asset(id: "...")One image or file

locale is optional everywhere and defaults to the space’s default language.

A blocks field has two parts: grid (the layout an editor set, or null) and blocks. Every block has a blockId, a typeName and a layout, plus the fields of its block type. With a space made by the Basic setup:

query Page($permalink: String!) {
contentByPermalink(permalink: $permalink) {
title
... on Page {
summary
components {
grid
blocks {
blockId
typeName
... on Teaser {
headline
body
image {
alt
width
height
card: variant(preset: "card")
}
}
}
}
}
}
}

With the variables { "permalink": "about" } it returns the About page, its summary and its teasers.

A few things this query shows:

  • $permalink is a variable. You send its value next to the query, so the query text never changes.
  • body is a rich text field. It arrives as structured JSON; turn it into HTML with the SDK’s richTextToHtml, see The SDK.
  • Images arrive as objects with url (the original), alt, width and height. variant(preset: "card") returns the address of a resized copy; card: ... renames it in the answer. The presets are thumb, card and hero.
  • Related documents and images are always included as objects. There is nothing to expand.

The first ten articles, with their summaries:

{
contents(type: "article", limit: 10) {
title
permalink
... on Article {
summary
}
}
}

The main menu, two levels deep:

{
menu(name: "main") {
items {
label
url
target
content { permalink }
children {
label
url
content { permalink }
}
}
}
}

A menu entry points either to a document (content) or to an external address (url). Build a link from url when it is set, otherwise from / plus content.permalink. The SDK does this for you and calls the result href.

Writing queries is easier in a tool that suggests field names as you type. The public API keeps its schema private by default, so tools cannot read it. To switch that on while you develop:

  1. Open .env in your CMS project and set PUBLIC_GRAPHQL_INTROSPECTION=true.
  2. Restart pnpm dev:public (stop it with Ctrl+C, then start it again).
  3. Open http://localhost:3100/graphql in your browser.

You should see GraphiQL, an editor for GraphQL queries: type on the left, press the run button, and read the answer on the right. Press Ctrl+Space for suggestions.

Any other GraphQL client works too, including desktop tools such as Postman or Insomnia: point it at the address above.

With the SDK on its GraphQL transport, you only write the part that is specific to a type; the basic fields are added for you:

import { createClient } from '@manablox/public-sdk';
const cms = createClient({ url: 'http://localhost:3100', transport: 'graphql' });
const page = await cms.byPermalink('/about', {
selection: '... on Page { summary components { grid blocks { blockId typeName ... on Teaser { headline body } } } }',
});
const data = await cms.query('{ contents(type: "article", limit: 5) { title permalink } }');

cms.query runs any query you write. Any other GraphQL library, or plain fetch, works the same way.

To protect the server, the public API refuses queries that nest too deep (more than 8 levels) or are too expensive (a complexity score above 1000). Normal page queries stay far below both. There are no mutations (queries that change data): the public API is read-only.