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:
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:
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 undefinedThe 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:
export function getNewsArticles() {
return content.blogPosts.all({
where: { category: 'news', publishedOn: { isNull: false } },
orderBy: [{ publishedOn: 'desc' }, { title: 'asc' }],
limit: 10,
offset: 0,
})
}| Option | Description |
|---|---|
where | Keeps the entries whose fields match. Keys are the collection's field names. Several keys must all match. |
orderBy | Sorts by one or more fields. An object such as { publishedOn: 'desc' }, or an array of objects for tie-breakers, applied in order. |
limit | The maximum number of entries to return. |
offset | The number of entries to skip first. Use it with limit to paginate. |
with | Expands 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 type | Operators |
|---|---|
text, textarea, url, slug | eq, ne, in, notIn, contains (case-insensitive), isNull when optional |
select | eq, ne, in, notIn (the values are the options, when the field has them), isNull when optional |
date, datetime | eq, ne, gt, gte, lt, lte, isNull when optional |
number | eq, ne, gt, gte, lt, lte, isNull when optional |
boolean | eq, ne, isNull when optional |
image, richtext, reference | isNull, 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: truematches all three.gt,gte,ltandltenever match an empty value.neandnotIndo match one. - Datetimes compare as points in time, so
2026-03-01T09:00:00+02:00equals2026-03-01T07:00:00Z.datevalues compare as text, which is correct forYYYY-MM-DD. isNullonly 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:
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 noneAn 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.
Build links
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 referencehref 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:
| Property | Type | Description |
|---|---|---|
id | string | The entry's ID. Use it to find the entry from a reference. |
slug | string | The entry's slug. |
publishedAt | string or null | The publication time, as an ISO timestamp. |
collection | string | The camelCase key of the entry's collection. The types use it to tell collections apart. |
data | object | The field values, keyed by camelCase field name. |
seo | SeoMeta, optional | The 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 type | Value in data |
|---|---|
text, textarea, url, slug | string |
select | string, or one of the options when the field has them |
date | string in YYYY-MM-DD format |
datetime | string as an ISO timestamp |
number | number |
boolean | boolean |
image | ImageValue |
reference | ReferenceValue |
richtext | RichTextValue |
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:
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:
| Property | Type | Description |
|---|---|---|
url | string | The path of the local copy, such as /areca/hero-banner-3f2a91c0.jpg. |
width | number or null | The width in pixels, when known. |
height | number or null | The height in pixels, when known. |
alt | string | The alt text that the editor entered. |
mimeType | string | The 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 onSections 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:
COPY --from=build /app/src/generated ./src/generatedIf the files are missing in production, reads throw, so check this step in your first deployment.
Related
- Typed content: how
areca.gen.tsis generated and what the pull checks. - Pulling content at build time: create the files that this page reads.
- Field types: the types and options for each field.
- Rich text and SEO: render the richer values.
- Preview: show draft entries on your site before they go live.
Typed content
How areca pull generates areca.gen.ts, so every collection, field, value, reference and route is typed in your site, with camelCase names and validation at pull time.
Site content (globals)
Declare single sections of values such as company details or home-page copy, read them with content.globals, add SEO, and show a section draft in preview.