Querying content
Read content with the methods on your CMS instance. Queries are plain objects, typed from your schema, and run on the server: in server components, route handlers, generateMetadata and generateStaticParams. They read from a local copy of your content that ships with your deploy and is kept in sync when the content changes, see Instant publishing.
import {cms} from '@/cms'
import {HomePage} from '@/schema'
export default async function Page() {
const home = await cms.get({type: HomePage})
return <h1>{home.title}</h1>
}Methods
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
// All matches, as an array (possibly empty)
const posts = await cms.find({type: BlogPost})
// The first match, or null
const latest = await cms.first({
type: BlogPost,
orderBy: {desc: BlogPost.publishDate}
})
// The first match, or throws "Entry not found"
const post = await cms.get({type: BlogPost, path: 'hello-world'})
// The number of matches
const total = await cms.count({type: BlogPost})Use first when a missing entry is a normal case, such as a url that may not exist (call notFound() when it returns null), and get for entries that must exist, like a seeded home page.
What a query returns
Without a select, you get the fields of the type you passed as type, plus the entry's own properties, prefixed with an underscore. A query without type returns only those properties.
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
const post = await cms.get({type: BlogPost, path: 'hello-world'})
post.title // a field of the type
post.publishDate // "2026-09-23"
post.author // link fields are resolved: {title, url, ...} or null
post._id // the entry id
post._url // "/blog/hello-world"
post._locale // "en", or null in roots without i18nThe entry properties are _id, _type, _index, _workspace, _root, _status, _parentId, _locale, _path, _url, _createdAt and _updatedAt.
With select you get exactly what you ask for, in the shape you ask for. Select fields through the type (BlogPost.publishDate) and entry properties through Query:
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
const cards = await cms.find({
type: BlogPost,
select: {
title: Query.title,
url: Query.url,
date: BlogPost.publishDate,
cover: BlogPost.cover
}
})
// Array<{title: string, url: string, date: string, cover: ImageLink | null}>
// A single expression returns plain values
const urls = await cms.find({type: BlogPost, select: Query.url})
// Array<string>Query has id, title, type, index, workspace, root, status, parentId, locale, path, url, aliases, createdAt and updatedAt, plus the relation helpers such as Query.children described in Related content and Query.snippet for search results.
Selecting only what a page needs keeps the data you pass to client components small, and TypeScript infers the result either way. To name the type of a result, use Awaited<ReturnType<typeof yourQueryFunction>>.
Query options
Which entries:
type,id,path,url,parentId,workspace,root,location,level,locale,statusand more, see Structural queries.Matching content:
filteron field values andsearchfor full text, see Filtering.Shape and order:
select,include,orderBy,groupBy,skipandtake, see Structural queries.Related entries: parents, children, siblings, translations and linked entries in the same query, see Related content.
Options you pass as undefined are ignored, which makes it easy to build a query from optional parameters.
Drafts and previews
Queries return published content by default. When Next.js draft mode is on, for example while an editor looks at a live preview, Alinea switches to drafts where they exist and applies the unsaved changes of the preview, without any change to your queries. Pass status to override this.
Example schema
The examples in this chapter use this schema:
import {Config, Field} from 'alinea'
export const Author = Config.document('Author', {
fields: {
avatar: Field.image('Avatar'),
bio: Field.text('Bio', {multiline: true})
}
})
export const Category = Config.document('Category', {fields: {}})
export const Blog = Config.document('Blog', {
contains: ['BlogPost'],
fields: {}
})
export const BlogPost = Config.document('Blog post', {
fields: {
publishDate: Field.date('Publish date'),
intro: Field.text('Intro', {multiline: true, searchable: true}),
featured: Field.check('Featured'),
cover: Field.image('Cover image'),
author: Field.entry('Author', {condition: {_type: 'Author'}}),
categories: Field.entry.multiple('Categories', {
condition: {_type: 'Category'}
}),
tags: Field.select.multiple('Tags', {
options: {news: 'News', release: 'Release', tutorial: 'Tutorial'}
}),
body: Field.richText('Body', {searchable: true})
}
})