Skip to content

Advanced

Patterns for larger sites: nesting relations, querying several types at once, reusing parts of queries and controlling how fresh the content is.

Nested relations

Relations can contain relations. The whole query, however deep, is resolved at once, so a navigation tree or a page with its linked content takes one call:

import {cms} from '@/cms'
import {Query} from 'alinea'

const menu = await cms.find({
  root: cms.workspaces.main.pages,
  locale: 'en',
  level: 0,
  select: {
    title: Query.title,
    url: Query.url,
    children: Query.children({
      select: {
        title: Query.title,
        url: Query.url,
        children: Query.children({
          select: {title: Query.title, url: Query.url}
        })
      }
    })
  }
})

Relations work inside link queries too, for example the url of each linked category's parent: BlogPost.categories.find({select: {title: Query.title, parent: Query.parent({select: {url: Query.url}})}}).

Several types at once

Pass an array to type to match any of them. Select the properties they share, and Query.type to tell them apart:

import {cms} from '@/cms'
import {BlogPost, Event} from '@/schema'
import {Query} from 'alinea'

const latest = await cms.find({
  type: [BlogPost, Event],
  select: {type: Query.type, title: Query.title, url: Query.url},
  orderBy: {desc: Query.createdAt},
  take: 10
})

Without a select, each result has the fields of its own type, and the result type is a union of the types.

Reusing selections

Selections are plain objects, so you can define them once and share them between queries. TypeScript infers the result from wherever you use them:

import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'

// Everything a post card needs
export const postCard = {
  title: Query.title,
  url: Query.url,
  date: BlogPost.publishDate,
  cover: BlogPost.cover
}

export type PostCard = Awaited<ReturnType<typeof latestPosts>>[number]

export function latestPosts(take = 3) {
  return cms.find({
    type: BlogPost,
    select: postCard,
    orderBy: {desc: BlogPost.publishDate},
    take
  })
}

export function featuredPosts() {
  return cms.find({
    type: BlogPost,
    filter: {featured: true},
    select: {...postCard, intro: BlogPost.intro}
  })
}

Freshness

Deployed sites keep their copy of the content in sync with the repository: a query first checks whether the content changed and syncs when it did, see Instant publishing. As a fallback it also syncs at most once per syncInterval seconds from your config, 60 by default. Two query options change this per query:

  • syncInterval: the fallback interval for this query, in seconds. 0 syncs every time.

  • disableSync: true: read the local copy as it is, without checking for changes. Use it for queries that run very often and can be a little behind, such as search suggestions.

import {cms} from '@/cms'
import {Query} from 'alinea'

export function suggest(term: string) {
  return cms.find({
    search: term,
    disableSync: true,
    take: 5,
    select: {title: Query.title, url: Query.url}
  })
}

Previews

The preview option carries the unsaved changes an editor is previewing. Alinea sets it for you when Next.js draft mode is on, so you don't pass it yourself: the same queries render published content for visitors and drafts in the live preview.

Performance tips

  • Select only what a page needs. Rich text and list fields can be large, and everything you return from a server component to a client component is serialized.

  • Count with cms.count instead of fetching entries and taking the length.

  • Put related data in the same query instead of looping over results and querying for each of them.

  • Search queries use a full text index that's built the first time you search, so the first search after a deploy can take a little longer.