Data fetching
Overview
createRoute() is the recommended way to set up routes that are connected to the CMS. Give it a collection, and it loads the doc for each URL and renders the route's component with the doc as props.doc.
It also handles the details: draft content for ?preview=true URLs (which the CMS adds to its preview links), the locale for each request, translations, 404s for missing docs, and Cache-Control headers.
// @/routes/[...slug].tsx
import {createRoute} from '@blinkk/root-cms';
import {PagesDoc} from '@/root-cms.js';
export default function Page(props: {doc: PagesDoc}) {
return <h1>{props.doc.fields?.meta?.title}</h1>;
}
// Renders `Pages/<slug>`, e.g. `/about` renders `Pages/about` and `/`
// renders `Pages/index`.
export const {handle} = createRoute({
collection: 'Pages',
slugParam: 'slug',
});
For a site built as static HTML (SSG), export getStaticProps and getStaticPaths instead, which build a page for every published doc in the collection:
export const {getStaticProps, getStaticPaths} = createRoute({
collection: 'BlogPosts',
ssg: true,
});
Route options
Most custom logic maps to an option of createRoute(), so a route rarely needs its own handler:
-
slug: the doc for a single-page route, e.g.{slug: 'index'}inroutes/index.tsx. UseslugFormatorgetSlug(params)for slugs built from several URL params. -
batchRequestandpreRenderHook: load more content for the page and pass it as props (see Batch requests). -
translations: load more translations, such as strings shared by every page (see Translations). -
resolveLocale: choose the locale yourself, e.g. from the user's country. -
resolveDoc: serve a different doc on the same URL, e.g. a regional variant. -
getMode: choose between draft and published content yourself. -
notFoundHook: render your own page when the doc doesn't exist. -
preRenderHook: redirect withprops.$redirect, or set a status code withprops.$statusCode. -
cacheandcacheControl: cache published reads in memory for 60 seconds, and set theCache-Controlheader for published pages.
See the @blinkk/root-cms reference for every option.
Batch requests
Every read from the CMS is a round trip to Firestore. A batch request combines the reads a page needs: its docs, queries and data sources are fetched in parallel. With the v2 translations manager, the translations for every doc in the batch are then fetched together, for the page's locale only.
In createRoute(), add reads with the batchRequest option. They're fetched in the same batch as the route's doc. The response is available as ctx.batchResponse, e.g. in preRenderHook, which passes it to the page as props:
export const {handle} = createRoute({
collection: 'Pages',
slugParam: 'slug',
batchRequest: (req) => {
req.addDoc('Global/header');
req.addQuery('posts', 'BlogPosts', {limit: 3});
req.addDataSource('pricing');
},
preRenderHook: (props, ctx) => ({
...props,
header: ctx.batchResponse!.getDoc('Global/header'),
posts: ctx.batchResponse!.getQuery('posts'),
pricing: ctx.batchResponse!.getDataSource('pricing'),
}),
});
Read the results with the response's getters, using the ids you added: getDoc(docId) and getDataSource(id) return null if the doc or data source doesn't exist, and getQuery(queryId) returns an empty array if the query has no results.
Batch requests outside a route
In a custom handler, getStaticProps() or a plugin, create a batch request with cmsClient.createBatchRequest(). With translate: true and the v2 translations manager, it also loads the translations for the docs it returns, for the locales you pass:
import {RootCMSClient} from '@blinkk/root-cms';
const cmsClient = new RootCMSClient(rootConfig);
const req = cmsClient.createBatchRequest({
mode: 'published',
translate: true,
locales: [locale],
});
req.addDoc('Pages/index');
req.addTranslations('common');
const res = await req.fetch();
const doc = res.getDoc('Pages/index');
const translations = res.getTranslations(locale);
If the locale depends on the doc, fetch the content first and the translations once the locale is known:
const res = await req.fetchContent();
const locale = pickLocale(res.getDoc('Pages/index'));
await req.fetchTranslations(res, {locales: [locale]});
Listing docs
To list the docs in a collection, add a query to the batch request with addQuery(). It takes the same options as cmsClient.listDocs(): limit, offset, orderBy, orderByDirection, and query for Firestore filters.
// @/routes/blog/index.tsx, which renders `Pages/blog`.
export const {handle} = createRoute({
collection: 'Pages',
slug: 'blog',
batchRequest: (req, ctx) => {
req.addQuery('posts', 'BlogPosts', {
orderBy: ctx.mode === 'draft' ? 'sys.createdAt' : 'sys.firstPublishedAt',
orderByDirection: 'desc',
});
},
preRenderHook: (props, ctx) => ({
...props,
posts: ctx.batchResponse!.getQuery('posts'),
}),
});
Custom order
Collections with customSorting: true let editors drag docs into order in the CMS. The order is stored at sys.sortKey:
req.addQuery('guides', 'Guides', {orderBy: 'sys.sortKey'});
Ordering by sys.sortKey leaves out docs that don't have a position yet, e.g. ones created by a script.
Translations
createRoute() loads the translations for the page's locale: the doc's own strings and the strings tagged common. With the v2 translations manager, it loads the translations for docs the page references and docs in its batch request instead of common. In your components, translate strings with useTranslations():
import {useTranslations} from '@blinkk/root';
export default function Page(props: {doc: PagesDoc}) {
const t = useTranslations();
return <h1>{t(props.doc.fields?.meta?.title || '')}</h1>;
}
To load more translations, such as strings shared by every page, return their ids from the translations option:
createRoute({
collection: 'Pages',
translations: () => ({tags: ['common']}),
});
Outside a route, use cmsClient.loadTranslationsForLocale(locale, {tags: ['common']}). With the v2 translations manager, use a batch request with translate: true and res.getTranslations(locale) instead (see Batch requests outside a route), since loadTranslations() and loadTranslationsForLocale() only read the v1 translations.
Localized values
Available as of version 4.2.1.
Fields in the CMS can have localized values, which allows a user to override a value for a specific locale, such as for localized images.
Overrides are stored in the field's @<fieldId> metadata key, next to the field's default value, in a localizedValues map keyed by locale:
{
"hero": {
"title": "Build faster",
"image": {"src": "https://.../hero.png", "alt": "Hero"},
"@image": {
"localizedValues": {
"de": {"src": "https://.../hero-de.png", "alt": "Hero"},
"ja": null
}
}
}
}
An override replaces the field's entire value, with no merging, so de above needs its own alt. A null override is valid and clears the field for that locale, e.g. to hide the image in ja. Locales are matched case-insensitively.
Overrides are supported for fields in a doc's fields, including fields nested in objects, but not for array items, which have no @<fieldId> key, or for fields inside rich text blocks.
Fields with a single value are localizable by default: string, richtext, image, file, number, boolean, date, datetime, select, multiselect, reference and references. object, array and oneof fields aren't, because an override would copy all of their contents for each locale. Set localizable to change the default for a field:
// @/collections/Pages.schema.ts
schema.string({id: 'legalText', localizable: false});
schema.object({id: 'promo', variant: 'inline', localizable: true, fields: [
// ...
]});
localizable has no effect on single-locale sites. Object fields shown as drawers (the default variant) have no field header yet, so they can't be localized in the editor.
Resolving localized values
createRoute() resolves localized values for you, for both server-rendered and static routes. Once the locale is known, it replaces each field with its override for that locale, in the route's doc and in the docs and queries of its batch request, and removes the localizedValues maps. translations(), preRenderHook() and the page's props get the resolved doc.
resolveLocale() and resolveDoc() run before the locale is final, so they get the unresolved doc. To get the raw data in the page, e.g. to resolve it yourself, set localizedValues: false on the route.
Content loaded outside createRoute()'s doc and batch request, e.g. with cmsClient.getDoc() or in getStaticProps(), isn't resolved. Use resolveLocalizedValues(), which works on a doc, a list of docs or any part of one:
import {resolveLocalizedValues} from '@blinkk/root-cms';
const rawDoc = await cmsClient.getDoc('Pages', 'index', {mode: 'published'});
const doc = resolveLocalizedValues(rawDoc, 'de');
It returns a copy and never changes its input. Parts without overrides are shared with the input rather than copied. Pass keepLocalizedValues: true to keep the localizedValues maps in the result.
Localized values don't follow i18n.fallbacks, which only applies to translations. A locale without its own override gets the field's default value, even if a fallback locale has one. createRoute() always resolves the exact locale. With resolveLocalizedValues(), pass fallbacks to check other locales, in order, when the locale has no override:
// Uses the en-CA override, then en-GB, then the default value.
const doc = resolveLocalizedValues(rawDoc, 'en-CA', {fallbacks: ['en-GB']});
Reading docs directly
For a single read, e.g. in a script, use RootCMSClient. Every read takes a mode: published for live content, or draft for the latest edits.
import {RootCMSClient} from '@blinkk/root-cms';
const cmsClient = new RootCMSClient(rootConfig);
const doc = await cmsClient.getDoc('Pages', 'index', {mode: 'published'});
const res = await cmsClient.listDocs('BlogPosts', {mode: 'published'});