Skip to content

Filtering

Use filter to match entries on the values of their fields, and search to find entries by the words they contain. Both combine with the structural options such as type and locale.

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

const featured = await cms.find({
  type: BlogPost,
  filter: {
    featured: true,
    publishDate: {gte: '2026-01-01'}
  }
})

Filter keys

A filter is an object with a condition per key. The keys are:

  • the fields of the queried type, such as featured or publishDate. TypeScript checks them when you pass a type.

  • the entry's own properties with an underscore: _id, _type, _parentId, _path, _url, _status, _locale, _workspace, _root, _index, _createdAt and _updatedAt.

Every key has to match, so several keys in one filter mean "and".

Operators

A plain value checks for equality. For anything else, pass an object with one or more operators:

  • is / isNot: equal or not equal. {category: 'news'} is short for {category: {is: 'news'}}.

  • in / notIn: the value is one of (or none of) a list.

  • gt, gte, lt, lte: greater than, greater or equal, less than, less or equal. Numbers compare as numbers, text compares alphabetically, which works for ISO dates such as '2026-09-23'.

  • startsWith: text that starts with a prefix. It's case-sensitive.

  • or: any of several conditions on the same value.

Operators in one object must all match:

import {cms} from '@/cms'
import {Event} from '@/schema'

// Events in 2026 that cost less than 20, or have no price set
const events = await cms.find({
  type: Event,
  filter: {
    date: {gte: '2026-01-01', lt: '2027-01-01'},
    price: {or: [{lt: 20}, null]}
  }
})

Missing values

Entries that never got a value for a field, and entries created before you added it, have no value rather than an empty one:

  • null matches missing values and null: {cover: null} finds posts without a cover image.

  • isNot, notIn and the comparisons never match a missing value. {featured: {isNot: true}} skips entries where featured was never set; use {featured: {in: [false, null]}} to include them.

Objects and lists

Some fields store an object or an array. Filter inside them with:

  • has: a filter on the keys of an object, for object fields and single links.

  • includes: at least one item of an array matches. Pass a value for arrays of plain values, such as a multiple select, or a filter for arrays of objects, such as lists and multiple links.

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

const authorId = '2g8FtRVKqrzBhqNzjRvCTJlTWkn'
const categoryIds = ['2g8Fu0PUfDUNMH4KNvqHYCcRCyX', '2g8Fu4lyGY6m5sN4YhAp8ag0WcY']

// Posts by one author: a single entry link stores {_entry: id}
await cms.find({type: BlogPost, filter: {author: {has: {_entry: authorId}}}})

// Posts in any of these categories
await cms.find({
  type: BlogPost,
  filter: {categories: {includes: {_entry: {in: categoryIds}}}}
})

// Posts tagged "release"
await cms.find({type: BlogPost, filter: {tags: {includes: 'release'}}})

Links store the id of their target in _entry, which is why link filters look like this. See Related content for more patterns.

and / or

To combine whole filters, use and or or with an array of filters. They must be the only key of their object:

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

// Featured, or tagged as a release
await cms.find({
  type: BlogPost,
  filter: {or: [{featured: true}, {tags: {includes: 'release'}}]}
})

// Posts in every one of these categories
const categoryIds = ['2g8Fu0PUfDUNMH4KNvqHYCcRCyX', '2g8Fu4lyGY6m5sN4YhAp8ag0WcY']
await cms.find({
  type: BlogPost,
  filter: {
    and: categoryIds.map(id => ({categories: {includes: {_entry: id}}}))
  }
})

Filter keys with an undefined value are skipped, and so are undefined items in and and or. That makes optional filters easy:

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

export async function listPosts(tag?: 'news' | 'release', from?: string) {
  return cms.find({
    type: BlogPost,
    filter: {
      tags: tag ? {includes: tag} : undefined,
      publishDate: from ? {gte: from} : undefined
    }
  })
}

search finds entries by words. Pass a string or an array of words:

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

export async function searchSite(terms: string, locale: string) {
  return cms.find({
    search: terms,
    locale,
    take: 20,
    select: {
      title: Query.title,
      url: Query.url,
      snippet: Query.snippet('<mark>', '</mark>', '…', 24)
    }
  })
}

How it matches:

  • The input is split into words, and an entry must match every word.

  • Words match as prefixes, so alin finds "Alinea".

  • Small typos are tolerated: one wrong letter for words of 5 to 14 characters, more for longer words. Case and accents don't matter.

  • Entry titles are always searched. Other fields only when you mark them searchable: true: text and rich text fields support it, also inside lists, objects and rich text blocks.

  • Results are ordered by relevance: entries whose title starts with the first word come first, and title matches weigh more than body matches. Add an orderBy to sort differently.

Query.snippet(start, end, cutOff, limit) returns a short excerpt of the searchable text around the matches, with matches wrapped in start and end. The defaults are '<mark>', '</mark>', '...' and 64, and limit (the length in words) can be at most 64. It can only be used together with search. The excerpt is plain text from your content: escape it, or render the markers yourself instead of using dangerouslySetInnerHTML.