Structural
Most queries start by saying which entries you want: of a type, at a url, in a root, in a locale. Then you choose what to get back and in which order. This page covers both; to match on field values see Filtering.
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
const posts = await cms.find({
type: BlogPost,
root: cms.workspaces.main.pages,
locale: 'en',
select: {title: Query.title, url: Query.url},
orderBy: {desc: BlogPost.publishDate},
take: 10
})Selecting entries
type: a type, or an array of types to match any of them:type: [Page, BlogPost]. It also narrows the result type.id: the entry id.url: the entry's url, for example'/blog/hello-world'. Handy in a catch-all route.path: the entry's path, the last segment of its url.parentId: the id of the parent entry.nullmatches entries at the top of a root.level: the depth in the tree.0is the top of a root,1their children, and so on.workspaceandroot: a name, or the object from your CMS instance:cms.workspaces.main,cms.workspaces.main.pages.location: a workspace, a root or a seeded page. With a seeded page, such ascms.workspaces.main.pages.blog, it matches the entries below it.alias: a previous url stored in the entry's metadata aliases, to redirect old urls.createdAtandupdatedAt: the audit timestamps of documents, in Unix seconds.
id, url, path, parentId, level, alias, createdAt, updatedAt, workspace and root take a value or a condition with the operators from filters:
import {cms} from '@/cms'
import {Query} from 'alinea'
// Several ids
await cms.find({id: {in: ['2g8FtR', '2g8FtS']}})
// Everything below /docs
await cms.find({url: {startsWith: '/docs/'}})
// Top-level entries of a root, for a main menu
await cms.find({
root: cms.workspaces.main.pages,
level: 0,
select: {title: Query.title, url: Query.url}
})
// Entries below a seeded page
await cms.find({location: cms.workspaces.main.pages.blog})Locales
In a root with i18n, every translation is a separate entry version. Without a locale, a query matches all of them, so find returns an entry once per language and first returns whichever comes first.
locale: only entries in this locale (compared case-insensitively).nullmatches entries in roots without i18n.preferredLocale: entries in this locale plus entries without a locale. Use it when a query spans translated and untranslated roots.
import {cms} from '@/cms'
import {Query} from 'alinea'
export async function pageByUrl(locale: string, slug: Array<string>) {
return cms.first({
locale,
url: `/${[locale, ...slug].join('/')}`,
select: {title: Query.title, type: Query.type}
})
}Status
status chooses between published, draft and archived versions:
'published'(default): only published entries.'draft'or'archived': only drafts, or only archived entries.'preferDraft': one version per entry: the draft if there is one, then the published version, then the archived one. This is the default in Next.js draft mode.'preferPublished': one version per entry: the published one if there is one, then the archived one, then the draft.'all': every version, so an entry can appear more than once.
Relations inside the query use the same status.
Choosing what to return
select: the fields and properties to return, in any shape. A single expression, such asselect: Query.url, returns plain values. See What a query returns.include: extra values to add to the full entry, typically related entries. It only applies when there's noselect; with aselect, put everything in the select.
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
// All fields of the post, plus its parent
const post = await cms.get({
type: BlogPost,
path: 'hello-world',
include: {
blog: Query.parent({select: {title: Query.title, url: Query.url}})
}
})
// Only what the card needs
const card = await cms.get({
type: BlogPost,
path: 'hello-world',
select: {
title: Query.title,
intro: BlogPost.intro,
blog: Query.parent({select: {title: Query.title, url: Query.url}})
}
})Sorting and pagination
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
const page = 2
const perPage = 10
const posts = await cms.find({
type: BlogPost,
select: {title: Query.title, url: Query.url},
orderBy: [
// Newest first
{desc: BlogPost.publishDate},
// Same date: alphabetical
{asc: Query.title}
],
skip: (page - 1) * perPage,
take: perPage
})orderBy: one{asc: ...}or{desc: ...}, or an array to break ties. Pass a field or aQueryproperty. Text is compared case-insensitively unless you addcaseSensitive: true, and entries without a value come last in both directions.Without
orderBy, entries come in their stored order: the order of siblings in the sidebar. Queries withsearchare ordered by relevance.skipandtake: skip a number of results and return at mosttake. Both must be whole numbers of 0 or more. For the total, runcms.countwith the same options but withoutskipandtake.
One entry per value
groupBy keeps one entry for every distinct value of a field or property: the first match in stored order, or the most relevant one when searching. Sorting and pagination apply afterwards, to the remaining entries.
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
// The first post of every blog
const firstPosts = await cms.find({
type: BlogPost,
groupBy: Query.parentId,
select: {title: Query.title, url: Query.url}
})groupBy takes a single field. Because the entry for each value is picked before sorting, it can't give you "the newest post per blog", and it doesn't return lists of entries per value: for those, query the entries and group them in your own code. Group on plain values such as text, select or date fields; link fields store a unique id per link, so they don't group.