Skip to content

The SDK

An SDK (software development kit) is a library that does the talking to an API for you. @manablox/public-sdk reads published content from Manablox: you ask for “the page at /about” or “the menu called main” and get plain JavaScript objects back. It works in the browser, in Node and on edge servers, and it has no dependencies of its own.

The starter website already uses it. This page is for when you add it to a website of your own, or want to understand what the starter does.

In your website’s folder:

Terminal window
pnpm add @manablox/public-sdk

(npm install @manablox/public-sdk works the same.) You should see the package appear under dependencies in package.json.

A client is an object that knows where your CMS is. Create one and reuse it for every request:

import { createClient } from '@manablox/public-sdk';
const cms = createClient({
url: 'http://localhost:3100',
transport: 'rest',
});

url is the address of the public API. In a local project that is http://localhost:3100; on a server it is your public API’s domain. transport: 'rest' makes every document arrive complete, which is the simplest choice. Without it the client uses GraphQL, where you list the fields you want, see Choosing a transport.

OptionMeaning
urlThe address of the API. Required
transport'rest' or 'graphql' (the default)
localeThe language to read, for example 'de'. Defaults to the space’s default language
spaceIdOnly when you read from a management API; the public API serves one space
cache{ ttl, max }: keep answers in memory for ttl milliseconds (default 1000), at most max of them. false turns it off
timeoutHow long one request may take, in milliseconds. Default 10000
retry{ attempts, baseDelay, maxDelay }: how often a failed request is tried again. Default 3 attempts
headersExtra HTTP headers for every request
fetchYour own fetch function, if your platform needs one

You can try the SDK without a website. Make an empty folder, install the package there with pnpm add @manablox/public-sdk, and create a file try.mjs:

import { createClient } from '@manablox/public-sdk';
const cms = createClient({ url: 'http://localhost:3100', transport: 'rest' });
const page = await cms.byPermalink('/about');
console.log(page ? page.title : 'Nothing published at /about');

Run it with node try.mjs. With a space made by the Basic setup and the public API running (pnpm dev:public in your CMS project), it prints About.

const page = await cms.byPermalink('/blog/hello-world');
if (!page) {
// nothing is published at this path: show your 404 page
} else {
console.log(page.title); // "Hello world"
console.log(page.type); // "article", the content type's technical name
console.log(page.summary); // a field, by its technical name
}

byPermalink takes the path of the address, with or without slashes. '/' or '' returns the space’s home page. It returns null when nothing is published at that path.

Every field is available directly on the document (page.summary) and also under fields (page.fields.summary). Use whichever reads better.

Other ways to get one document:

CallReturns
cms.byPermalink(path)The document at a path, or null
cms.get(id)One document by its id, or null

list returns several documents at once, for example the latest articles for a blog page:

const result = await cms.list({ type: 'article', limit: 10 });
for (const article of result.items) {
console.log(article.title, '/' + article.permalink);
}
console.log(`${result.total} articles in total`);
ArgumentMeaning
typeOnly documents of this content type (its technical name)
parentIdOnly the direct children of this document id
underEverything below this document id, at any depth
searchA search term
limitHow many to return. Default 25, at most 100 on REST
offsetHow many to skip, for “page 2” links

The answer is { items, total, limit, offset }. On the GraphQL transport total only counts what was returned so far, so do not use it for page numbers there.

To list the articles below the Blog page:

const blog = await cms.byPermalink('/blog');
const articles = blog ? await cms.list({ parentId: blog.id }) : null;

Menus are built by editors in the admin under Menus. Ask for one by its technical name:

const menu = await cms.menu('main');
for (const item of menu?.items ?? []) {
console.log(item.label, item.href, item.target);
// item.children holds the entries nested under this one
}

Each entry has a label, an href you can put straight into a link (a page’s path, or an external address), a target (_self or _blank) and children. menu() returns null when no menu has that name.

A blocks field (the starter model calls it components) holds the blocks an editor stacked on a page. Its value is { grid, blocks }. blocksOf gives you the list:

import { blocksOf } from '@manablox/public-sdk';
for (const block of blocksOf(page.components)) {
if (block.type === 'teaser') {
console.log(block.headline);
}
}

Each block has a blockId, a type (the block type’s technical name) and its fields directly on it. If an editor arranged the blocks on a grid, grid describes the columns per screen size. The SDK exports helpers that turn it into CSS (BLOCK_GRID_CSS, blocksGridStyle, blockLayoutStyle); the starter website shows how they are used.

A rich text field arrives as structured data, not as HTML. Turn it into HTML with richTextToHtml:

import { isRichTextEmpty, richTextToHtml, richTextToText } from '@manablox/public-sdk';
if (!isRichTextEmpty(page.body)) {
const html = richTextToHtml(page.body); // '<p>Some <strong>bold</strong> text</p>'
const plain = richTextToText(page.body); // 'Some bold text', for a meta description
}

richTextToHtml escapes all text and only produces a fixed list of safe tags, so its output is safe to insert with innerHTML, Vue’s v-html or Astro’s set:html. Never insert any other CMS text that way.

An image field holds an asset: a file from the asset library. On the REST transport it arrives as an id unless you ask for it to be expanded (included in full):

import { assetSrcSet, assetUrl, isImage } from '@manablox/public-sdk';
const article = await cms.byPermalink('/blog/hello-world', { expand: ['image'] });
const image = article?.image;
if (image && typeof image === 'object' && isImage(image)) {
const src = assetUrl(image, { preset: 'card' });
const srcset = assetSrcSet(image, { thumb: 320, card: 640, hero: 1920 });
const html = `<img src="${src}" srcset="${srcset}" alt="${image.alt ?? ''}">`;
}

expand takes the technical names of the fields to include in full. It works for images, related documents and users, and also inside blocks. On the GraphQL transport relations always arrive as objects.

A preset is a size the CMS prepares for you. Every project has three:

PresetWidth
thumb320 pixels (fits inside 320 x 320)
card640 pixels
hero1920 pixels

They are served as WebP and respect the crop and focal point an editor set in the admin. assetUrl(image) without a preset returns the original file. An unknown preset also falls back to the original, so a typo never gives a broken image. assetSrcSet builds a srcset, so the browser picks the smallest size that looks sharp.

To load one asset by id: await cms.asset(id).

Every call reads the space’s default language unless you say otherwise:

const german = await cms.byPermalink('/about', { locale: 'de' });
const de = cms.withLocale('de'); // a client that always reads German
const menu = await de.menu('main');

See Languages and translations.

  • Nothing published at a path: byPermalink and get return null. Show a 404 page.
  • The CMS is down or too slow: the call throws an error after a few retries. Show an error page, and do not cache it.
let page = null;
try {
page = await cms.byPermalink(path);
} catch (error) {
// the CMS could not answer: render a 503 error page instead of a 404
console.error(error);
}

The errors the SDK throws itself all extend ManabloxError: ManabloxHttpError (the API answered with an error; it has a status), ManabloxTimeoutError, ManabloxAbortError and ManabloxGraphQLError. When the CMS cannot be reached at all, you get your platform’s normal network error instead, so treat any error as “the CMS could not answer”. Keeping “not found” and “CMS unreachable” apart matters: otherwise a short outage turns every page into a 404.

restgraphql (default)
What arrivesThe whole document, every fieldThe basic fields, plus what you list in selection
Related images and documentsIds, unless named in expandAlways objects
Image presetsassetUrl(image, { preset }) worksAsk for variant(preset: "card") in the selection
NeedsNothingKnowing a little GraphQL

With GraphQL you pass the extra fields you need as selection:

const cms = createClient({ url: 'http://localhost:3100' });
const page = await cms.byPermalink('/about', {
selection: '... on Page { summary }',
});

cms.query(query, variables) runs any GraphQL query you write yourself (GraphQL transport only). See GraphQL.

Each client keeps answers in memory for a short time (one second by default) and merges identical requests that run at the same moment. So when your layout and your page both ask for the main menu, only one request goes out. Pass { fresh: true } to skip the memory for one call:

const page = await cms.byPermalink('/about', { fresh: true });

On a server, create one client per process and keep ttl short. See Caching for how this fits with the caches in the CMS.

If your website uses TypeScript, the SDK can write types for your content model, so your code editor knows which fields a page has and what kind of value each one holds:

Terminal window
pnpm exec manablox-sdk types --url http://localhost:3100 --out src/manablox.d.ts

You should see manablox-sdk: wrote ... types to src/manablox.d.ts. The file has one interface per content type and block type, named after the technical name (page becomes Page, blog-post becomes BlogPost), a Content type that is any of your content types, and a ContentByName map. Use them like this:

import type { Article } from './manablox';
const article = await cms.byPermalink<Article>('/blog/hello-world');
article?.summary; // typed as string, null or missing
article?.date; // your code editor suggests every field of Article

A relation field is typed as “an id or an object”, because it arrives as an id unless you expand it. Run the command again whenever the content model changes, and commit the file. The starter website has it as pnpm types.

OptionMeaning
--urlThe public API. Defaults to the MANABLOX_URL environment variable
--outThe file to write. Without it the types are printed
--prefixPut in front of every interface name, if your own types already use those names

This client only ever reads published content. For reading drafts on your own server, the SDK has a second entry point, @manablox/public-sdk/preview, that talks to the management API with an API key. See Preview and the visual editor.