Skip to content

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. null matches entries at the top of a root.

  • level: the depth in the tree. 0 is the top of a root, 1 their children, and so on.

  • workspace and root: 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 as cms.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.

  • createdAt and updatedAt: 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). null matches 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 as select: 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 no select; with a select, 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 a Query property. Text is compared case-insensitively unless you add caseSensitive: 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 with search are ordered by relevance.

  • skip and take: skip a number of results and return at most take. Both must be whole numbers of 0 or more. For the total, run cms.count with the same options but without skip and take.

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.