Areca Docs

Reading content

Query pulled entries with the typed content object from areca.gen.ts, filter and sort them, follow references and build links, and drop down to readContent when you need the raw files.

After the pull, your site reads entries from the local files. The pull also writes areca.gen.ts, which exports a typed content object. Use it for everything in your site: list collections, find entries by slug or ID, filter and sort, follow references and build links. Every collection, field, value and route is typed, so your editor completes them as you type. See typed content for how the module is generated.

content reads files from disk, so it runs only in Node.js code: a server function, a server route, or a build script. Never import it into code that runs in the browser.

Import content

The default location of the module is next to the pulled files:

src/lib/content.ts
import { content } from '../generated/content/areca.gen'

content reads manifest.json on every call, so a new pull is picked up without restarting the process. It keeps each collection in memory after the first read, for the publication it came from. The files only change when you pull again, so this is cheap.

The folder is the one you passed to areca pull --out. To read the files from another location, set ARECA_CONTENT_DIR before the module loads. This is useful in a container, where the working directory differs from the one you pulled in.

List entries

Each collection is a property of content, named after its camelCase key. Every collection has the same four read methods:

src/lib/content.ts
const all = content.blogPosts.all() // every entry, in the pulled order
const newest = content.blogPosts.first() // the first entry, or undefined
const bySlug = content.blogPosts.get('hello-world') // by slug, or undefined
const byId = content.blogPosts.byId('a1b2c3') // by ID, or undefined

The pulled order is the order the CMS sends: newest publishedAt first. Do not rely on it for anything else, and use orderBy when you need a specific order.

get and byId return undefined for an unknown value. Return null or throw a not-found error from your route, so the page can show a 404.

Filter, sort and paginate

all and first take the same options. The work happens in memory, on the entries already in your files:

src/lib/content.ts
export function getNewsArticles() {
  return content.blogPosts.all({
    where: { category: 'news', publishedOn: { isNull: false } },
    orderBy: [{ publishedOn: 'desc' }, { title: 'asc' }],
    limit: 10,
    offset: 0,
  })
}
OptionDescription
whereKeeps the entries whose fields match. Keys are the collection's field names. Several keys must all match.
orderBySorts by one or more fields. An object such as { publishedOn: 'desc' }, or an array of objects for tie-breakers, applied in order.
limitThe maximum number of entries to return.
offsetThe number of entries to skip first. Use it with limit to paginate.
withExpands reference fields into the entries they point to. See references.

where

A direct value means equality. For a few fields, operators are available. The editor only offers the ones that fit the field type:

Field typeOperators
text, textarea, url, slugeq, ne, in, notIn, contains (case-insensitive), isNull when optional
selecteq, ne, in, notIn (the values are the options, when the field has them), isNull when optional
date, datetimeeq, ne, gt, gte, lt, lte, isNull when optional
numbereq, ne, gt, gte, lt, lte, isNull when optional
booleaneq, ne, isNull when optional
image, richtext, referenceisNull, when optional
content.blogPosts.all({
  where: {
    featured: true, // equality
    category: { in: ['news', 'guides'] },
    publishedOn: { gte: '2026-01-01', lt: '2027-01-01' },
    title: { contains: 'release' },
    coverImage: { isNull: false },
  },
})

Some details that affect results:

  • Empty values. null, '' and a missing key all count as empty. isNull: true matches all three. gt, gte, lt and lte never match an empty value. ne and notIn do match one.
  • Datetimes compare as points in time, so 2026-03-01T09:00:00+02:00 equals 2026-03-01T07:00:00Z. date values compare as text, which is correct for YYYY-MM-DD.
  • isNull only works on optional fields. A required field is never empty, so the type checker does not accept the operator for it.

orderBy

Sort by the text, number, date, datetime, boolean and select fields. Images, rich text and references are not sortable:

content.blogPosts.all({ orderBy: { publishedOn: 'desc' } })
content.blogPosts.all({ orderBy: [{ category: 'asc' }, { publishedOn: 'desc' }] })

Empty values always sort last, in both directions. Entries that tie on every key keep their pulled order.

References

A reference field holds a ReferenceValue, with the target's collection key, slug and id. Use content.resolve to get the target entry. It matches on id, so it still works when the target's slug has changed:

src/lib/content.ts
import type { Entry } from '@areca-cms/site-sdk'
import { content } from '../generated/content/areca.gen'

export function getAuthorOf(article: Entry<'blogPosts'>) {
  return content.resolve(article.data.author) // Entry<'authors'> | null
}

resolve accepts '', null and undefined as well, and returns null for them. Each reference field has its target collection in its type, so resolve returns an entry of that collection.

To load the target with the entry, add with. Only fields you list are expanded, and only one level deep:

const posts = content.blogPosts.all({ with: { author: true } })
posts[0]?.data.author?.data.name // the author's name, or undefined when there is none

An expanded field is Entry | null when the field is required. It is Entry | null | undefined when the field is optional, because a missing field stays undefined. Use resolve on a reference inside an expanded entry if you need a second level.

Each collection's route has a $slug placeholder. The typed helpers fill it in and return a template-literal type, so the link is checked against the route:

content.blogPosts.href(entry) // '/blog/hello-world', typed as `/blog/${string}`
content.blogPosts.href('hello-world') // the same, from a slug
const author = article.data.author
if (author) content.href(author) // '/authors/ada', from a reference

href URL-encodes the slug. Use these helpers instead of building the path yourself, so a route change in the schema reaches your links.

Entry shape

Each entry has the same shape in every collection:

PropertyTypeDescription
idstringThe entry's ID. Use it to find the entry from a reference.
slugstringThe entry's slug.
publishedAtstring or nullThe publication time, as an ISO timestamp.
collectionstringThe camelCase key of the entry's collection. The types use it to tell collections apart.
dataobjectThe field values, keyed by camelCase field name.
seoSeoMeta, optionalThe SEO data for the entry. Older publications do not include it.

The values in data depend on the field type. See field types for the exact types, including which fields can be empty:

Field typeValue in data
text, textarea, url, slugstring
selectstring, or one of the options when the field has them
datestring in YYYY-MM-DD format
datetimestring as an ISO timestamp
numbernumber
booleanboolean
imageImageValue
referenceReferenceValue
richtextRichTextValue

Optional fields can also be null, missing or '', as typed content explains.

Call it from a server function

In TanStack Start, route loaders can run in the browser during navigation. Wrap the read in a server function, so the file read always happens on the server. This matches the example site:

src/lib/queries.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { content } from './content'

export const loadArticles = createServerFn({ method: 'GET' }).handler(() =>
  content.blogPosts.all({ orderBy: { publishedOn: 'desc' } }),
)

export const loadArticle = createServerFn({ method: 'GET' })
  .validator(z.object({ slug: z.string().min(1) }))
  .handler(({ data }) => content.blogPosts.get(data.slug) ?? null)

The loader then calls loadArticle({ data: { slug } }). Other frameworks have an equivalent for code that runs only on the server, such as an API route, a server component or a loader that runs in Node.js.

Images

Image fields hold an ImageValue:

PropertyTypeDescription
urlstringThe path of the local copy, such as /areca/hero-banner-3f2a91c0.jpg.
widthnumber or nullThe width in pixels, when known.
heightnumber or nullThe height in pixels, when known.
altstringThe alt text that the editor entered.
mimeTypestringThe file's MIME type, such as image/jpeg.

areca pull copies each image into your project (by default public/areca) and sets url to the path your site serves it from, so visitors' browsers never load anything from the CMS. In seo, image URLs are absolute, built from the site's production URL. If you turn this off with assets: { download: false }, url is an absolute URL on the CMS and the CMS must stay reachable for images to load. See media.

Use alt for the image's alt attribute, and use width and height when you render an <img> tag, so the browser can reserve the space.

Rich text

A richtext field holds a RichTextValue. It contains the Markdown source, a parsed document, and a static HTML version. To render it in React, pass the parsed document to the Markdown renderer. See rich text for the full example.

SEO

Each entry can have a seo object with its title, description, canonical URL, social data and JSON-LD. Convert it into page head tags with the helpers in @areca-cms/site-sdk/seo. See SEO.

Site content

Single sections of values, such as company details, are read from content.globals, not from a collection. Each section is an object of its fields, keyed by its camelCase key:

const company = content.globals.companyInfo
const home = content.globals.homePage // includes $seo when the section has SEO on

Sections have no list, slug or route, so there is nothing to query. Use content.resolve() on a reference field of a section, the same as on an entry. For previews and the typed globals, see site content.

Read the files directly

content is built on readContent, from @areca-cms/site-sdk/pull. Use readContent when you need the raw entries of one collection and do not need the query methods. It takes the folder and a camelCase collection key, and returns the typed entries:

import { readContent } from '@areca-cms/site-sdk/pull'

const articles = readContent('src/generated/content', 'blogPosts') // Entry<'blogPosts'>[]

readContent is synchronous and reads the file each time you call it. There is no type parameter, because the collection key sets the type. To read several collections from one publication, capture the manifest once with readContentManifest and pass it as the third argument. content does this for you.

If the file is missing, readContent throws an error that tells you to run areca pull first. A collection that the publication does not contain is also an error.

Fetch from the CMS directly

Pulling is the normal way to read content. Use the client when you need the manifest or the current snapshot in memory, for example in a one-off script:

import { createArecaClient } from '@areca-cms/site-sdk'

const client = createArecaClient()
const manifest = await client.fetchManifest()
const snapshot = await client.fetchSnapshot()

createArecaClient reads ARECA_CMS_URL and ARECA_SITE_TOKEN from the environment, unless you pass them as options. Its fetchManifest and fetchSnapshot methods make authenticated requests with the site token. Do not call the CMS on every visitor request. These routes are rate limited per token, and the client does not retry failed requests. See rate limits. See client and errors and limits.

Deploy the content files

content and readContent read files at runtime, so the files must be in the deployed output. If you run your site in a container, copy the generated directory into the runtime image. For example:

Dockerfile
COPY --from=build /app/src/generated ./src/generated

If the files are missing in production, reads throw, so check this step in your first deployment.

On this page