Show navigationHide navigation
Query API
The query() function provides a type-safe, chainable content selector for reading collection entries. It works in App Router server components, Pages Router getServerSideProps / getStaticProps, and API route handlers.
query is created by the createQuery() factory and bound to your app's config and types by the auto-generated cms/__generated__/query.ts.
Setup#
query is auto-generated into cms/__generated__/query.ts whenever you run npx octocms types:gen. It binds createQuery from the package to your app's config and generated EntryMap:
ts// cms/__generated__/query.ts (auto-generated — do not edit) import { createQuery } from 'octocms/query'; import { config, type OctoConfig } from 'octocms/config'; import type { EntryMap } from './types'; export const query = createQuery<EntryMap, OctoConfig>(config as unknown as OctoConfig);
Import query from cms/__generated__/query (the cms/__generated__ path alias resolves to ./cms/__generated__/):
tsimport { query } from 'cms/__generated__/query';
Basic usage#
tsimport { query } from 'cms/__generated__/query'; // All posts const posts = await query('post').toArray(); // ^^^^ autocomplete: 'post' | 'item' | 'homePage' | ... // Single entry const home = await query('homePage').first(); // Access typed fields posts[0].fields.title; // string posts[0].fields.publishedAt; // string | null posts[0].fields.body; // string
Entry status filtering#
query() automatically excludes entries with sys.status of draft or archived. Only published, changed, and merged entries are returned by default. This ensures drafts and soft-deleted entries never appear on public pages.
Legacy entries without a sys.status field are treated as merged.
To include all entries regardless of status (e.g. for Next.js Draft Mode previews), call .includeDrafts():
tsconst allPosts = await query('post').includeDrafts().toArray();
See nextjs-integration.md — Draft Mode for the full pattern.
Filtering#
Predicate function#
Full control with a callback:
tsconst published = await query('post') .filter((p) => p.fields.publishedAt !== null) .toArray();
Equality object#
Simple field-value matching:
tsconst enabled = await query('item') .filter({ enabled: 'true' }) .toArray();
Combining filters#
Multiple .filter() calls are ANDed:
tsconst results = await query('item') .filter((e) => e.fields.enabled === 'true') .filter((e) => (e.fields.sortOrder ?? 0) > 1) .toArray();
Sorting#
ts// Ascending (default) const oldest = await query('post').sort('publishedAt').toArray(); // Descending const newest = await query('post').sort('publishedAt', 'desc').toArray();
null values sort last regardless of direction.
Pagination#
skip / limit#
tsconst page2 = await query('post') .sort('publishedAt', 'desc') .skip(10) .limit(10) .toArray();
paginate()#
Returns { items, total, hasMore } for building pagination UI:
tsconst page = await query('item') .filter({ enabled: 'true' }) .sort('sortOrder', 'asc') .skip(0) .limit(10) .paginate(); // page.items — Entry<'item'>[] // page.total — number (all entries matching filters) // page.hasMore — boolean
Methods#
| Method | Returns | Description |
|---|---|---|
.conditions(conds) | QueryBuilder | Set branch selections for conditional fields |
.filter(fn | obj) | QueryBuilder | Add a filter (AND with previous filters) |
.sort(field, dir?) | QueryBuilder | Sort by field name, 'asc' (default) or 'desc' |
.skip(n) | QueryBuilder | Skip first n results |
.limit(n) | QueryBuilder | Cap results at n entries |
.includeDrafts() | QueryBuilder | Include draft and archived entries (for preview/Draft Mode) |
.toArray() | Promise<Entry[]> | Execute and return matching entries |
.first() | Promise<Entry | null> | Execute and return first match or null |
.paginate() | Promise<{ items, total, hasMore }> | Execute with pagination metadata |
Usage by context#
query() works in any server-side Node.js context. For a comprehensive guide with all patterns, see nextjs-integration.md.
App Router — Server Component#
ts// src/app/blog/page.tsx import { query } from 'octocms/query'; export default async function BlogPage() { const posts = await query('post') .sort('publishedAt', 'desc') .toArray(); return <ul>{posts.map((p) => <li key={p.sys.id}>{p.fields.title}</li>)}</ul>; }
App Router — with "use cache"#
Co-locate the cached data fetcher with the page that uses it — define a private async helper at the top of the page file and mark it 'use cache'. Calls from both generateMetadata and the default export hit the same cache entry.
tsx// app/<route>/page.tsx import { cacheLife, cacheTag } from 'next/cache'; import { OCTOCMS_PUBLIC_CONTENT_CACHE_TAG } from 'octocms/lib/publicContentCacheTag'; import { query } from 'cms/__generated__/query'; async function loadPageData() { 'use cache'; cacheTag(OCTOCMS_PUBLIC_CONTENT_CACHE_TAG); cacheLife('hours'); return query('<your-collection>') .filter((entry) => /* filter */) .sort('<field>', 'desc') .toArray(); } export default async function Page() { const items = await loadPageData(); // … }
A single shared cache tag covers every page that follows the pattern, so buildJsons() (admin save / create / delete) refreshes them all together.
App Router — generateStaticParams + generateMetadata#
ts// src/app/blog/[slug]/page.tsx import { query } from 'octocms/query'; import type { Metadata } from 'next'; export async function generateStaticParams() { const posts = await query('post') .filter((p) => p.fields.publishedAt != null) .toArray(); return posts.map((p) => ({ slug: p.fields.slug })); } export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> { const { slug } = await params; const post = await query('post').filter({ slug }).first(); return { title: post?.fields.title ?? 'Not Found' }; }
App Router — Route Handler#
ts// src/app/api/posts/route.ts import { query } from 'octocms/query'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const page = Number(searchParams.get('page') ?? '1'); const perPage = 10; const result = await query('post') .sort('publishedAt', 'desc') .skip((page - 1) * perPage) .limit(perPage) .paginate(); return Response.json(result); }
App Router — Layout#
ts// src/app/layout.tsx import { query } from 'octocms/query'; export default async function RootLayout({ children }: { children: React.ReactNode }) { const homePage = await query('homePage').first(); return ( <html lang="en"> <body> <header>{homePage?.fields.title}</header> <main>{children}</main> </body> </html> ); }
App Router — Draft Mode#
tsimport { draftMode } from 'next/headers'; import { query } from 'octocms/query'; export default async function BlogPage() { const { isEnabled } = await draftMode(); const q = query('post').sort('publishedAt', 'desc'); const posts = isEnabled ? await q.includeDrafts().toArray() : await q.filter((p) => p.fields.publishedAt != null).toArray(); // ... }
Pages Router — getStaticProps with ISR#
ts// pages/blog.tsx import { query } from 'octocms/query'; export async function getStaticProps() { const posts = await query('post') .sort('publishedAt', 'desc') .limit(20) .toArray(); return { props: { posts }, revalidate: 3600 }; }
Pages Router — getStaticPaths#
ts// pages/blog/[slug].tsx import { query } from 'octocms/query'; export async function getStaticPaths() { const posts = await query('post') .filter((p) => p.fields.publishedAt != null) .toArray(); return { paths: posts.map((p) => ({ params: { slug: p.fields.slug } })), fallback: 'blocking', }; }
Pages Router — getServerSideProps#
ts// pages/blog.tsx import { query } from 'octocms/query'; export async function getServerSideProps() { const posts = await query('post') .sort('publishedAt', 'desc') .limit(20) .toArray(); return { props: { posts } }; }
Pages Router — API Route#
ts// pages/api/posts.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { query } from 'octocms/query'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const result = await query('post') .sort('publishedAt', 'desc') .limit(10) .paginate(); res.json(result); }
Type utilities#
The package exports several type utilities you can use directly:
| Type | Description |
|---|---|
CollectionNames<C> | Union of collection name strings |
InferFields<C, Name> | Typed fields object for a collection |
InferEntry<C, Name> | Full { sys, fields } entry type |
tsimport type { OctoConfig } from 'octocms/config'; import type { InferEntry } from 'octocms/defineConfig'; type PostEntry = InferEntry<OctoConfig, 'post'>; // { sys: { id: string; type: 'post' }; fields: { title: string; publishedAt: string | null; ... } }
Generated types (recommended)#
For concrete, readable interfaces with fully resolved reference fields, use the auto-generated types in cms/__generated__/. See generated-types.md for full documentation.
tsimport type { PostEntry, AuthorEntry } from 'cms/__generated__'; import { CollectionName, ItemCategoryOption } from 'cms/__generated__';
The query() API uses these generated types internally, so return values already have resolved reference types (e.g., AuthorEntry[] instead of unknown). Run npx octocms types:gen after changing your schema to regenerate.
Content processing#
Each query() read resolves entries the same way as the rest of the stack:
- Image fields: UUID values are resolved to a
ResolvedImageFieldobject:{ src, alt, width, height, blurDataURL }.srcis/media/<uuid>.<ext>;altcomes from the media entrytitle. See the image field docs for rendering examples. - Markdown fields: Content is read from companion
.mdfiles alongside the entry JSON (e.g.post-123.body.md). Returned as a plainstring. If the companion file is missing, the field defaults to"". - Reference fields: Recursively resolved to full entry objects (deep nesting supported)
- Other fields: Passed through as-is
Filtering: equality filters such as .filter({ featuredImage: someUuid }) no longer match processed entries. Use a predicate, e.g. .filter((p) => p.fields.featuredImage?.src.includes(uuid)), or compare alt / other fields as needed.
Caching on the sample public site#
query() has no built-in cache. Wrap it in a helper with "use cache", cacheTag(), and cacheLife() to enable caching and instant invalidation on save.
Cached helpers (getHomePage / getBlog / getPublishedPosts) | Raw query(collection) | |
|---|---|---|
| Reads | Fixed collection(s) per helper | Any collection; add .filter() / .sort() / .limit() as needed |
| Caching | "use cache" + tags | None unless you wrap it yourself |
| Type safety | Inferred from query() | Full autocomplete for collections and fields |
Additional cached readers should use the same OCTOCMS_PUBLIC_CONTENT_CACHE_TAG unless you add your own invalidation. See the Tutorial for a complete cached helper example.