Documentation

Public list API

Reference for the VitNode Content Engine public list helpers in @vitnode/core/tanstack/content - defineContentList, contentListQueryOptions, contentListPageHead, ContentListResults, ContentListPagination and useContentListSearchInput.

Every helper on this page is exported from @vitnode/core/tanstack/content and is universal unless noted. For a guided setup, follow Public lists. Examples use the blog plugin's /blog list.

defineContentList

Declares a list's sorts, URL filters and page size, and returns pure helpers for the URL state.

const blogPostList = defineContentList({
  defaultSort: 'newest',
  filters: { category: contentListIdFilter },
  pageSize: 12,
  sorts: {
    newest: { order: 'desc', orderBy: 'publishedAt' },
    oldest: { order: 'asc', orderBy: 'publishedAt' },
  },
})
NameTypeRequiredDefaultDescription
sortsRecord<string, { orderBy; order: "asc" | "desc" }>YesEach URL value of sort. orderBy must be in publicApi.orderableFields or be publishedAt
defaultSorta key of sortsYesThe sort a URL without sort uses. Never written to the URL
filtersRecord<string, (raw: unknown) => value | undefined>NononeURL filters by URL name. A parser returns a string, number or boolean, or undefined to ignore it
pageSizenumberNo12Sent as first. Clamped to 1-50, the public list route's limit

contentListIdFilter parses a positive integer id, such as ?category=3.

MethodReturnsBehavior
parseSearch(input)ContentListSearchThe route's search validator. Keeps page above 1, a trimmed q of up to 256 characters, a non-default sort and valid filters. Drops everything else
withSearch(current, patch)ContentListSearchApplies a change. Changing anything but page resets it to the first page. undefined clears a value
isFiltered(search)booleantrue when a term, a filter or a non-default sort is set. page does not count
apiQuery(search)ContentListApiQuery{ first, order, orderBy, page, search? } for the list route. Map filters yourself
href(pathname, search)stringThe internal URL of one state, e.g. /blog?category=3&page=2
sortsstring[]Every sort, in declaration order
defaultSort, pageSizevalueThe resolved config

contentListQueryOptions

TanStack Query options for one page of a list. Use them in a loader with queryClient.query(...) and in a component with useQuery(...).

NameTypeRequiredDefaultDescription
queryKeyQueryKeyYesThe prefix every page shares, e.g. ["@vitnode/blog", "posts"]
localestringYesThe page's locale. Pass it on to the API as ?locale=
searchobjectYesThe normalized state from parseSearch, or a fixed one such as {}
fetch({ locale, search }) => Promise<TData>YesMakes the request. Write the fetcher call here so its route stays typed
staleTimenumberNo60_000Milliseconds a page stays fresh, RECORD_STALE_TIME

The key is [...queryKey, { locale, ...search }]. Add meta: LOADER_DATA_QUERY_META when a route returns the data from its loader, so it is not sent to the browser twice.

contentListPageHead

Builds a plugin route head for a list page.

NameTypeRequiredDefaultDescription
pathnamestringYesThe list's internal path, e.g. /blog
searchobjectYesThe normalized state
titlestringNoThe page title, already translated
descriptionstringNoThe meta description
localesreadonly string[]Noevery configured localeThe locales the page exists in

With an empty search, the head is index, follow with a canonical link and one hreflang alternate per locale. Any other state is noindex, follow without a canonical.

ContentListResults

Renders one of the list's states. A React component; item rendering and every label come from you.

PropTypeRequiredDefaultDescription
itemsTItem[] | undefinedYesundefined while the first page loads
renderItem(item, index) => ReactNodeYesOne item, inside an <li>
getKey(item) => KeyYesThe React key
labelstringYesThe list's accessible name
isFilteredbooleanYesPicks noResults over empty for an empty page
loading, error, empty, noResultsReactNodeYesWhat each state shows
isErrorbooleanNofalseShows error while there are no items
isFetchingbooleanNofalseMarks the list aria-busy and dims it
as"ul" | "ol"No"ul"The list element
className, itemClassNamestringNoClasses for the list and each <li>

ContentListPagination

Numbered page links from the list route's pageInfo. Renders nothing for a single page. Links go through HrefLink, so the locale prefix is added and every page works without JavaScript.

PropTypeRequiredDefaultDescription
pageInfoContentListPageInfoYesThe list response's pageInfo
hrefFor(page: number) => stringYesA page's internal URL, usually list.href(...)
labelstringNo"Pagination"The <nav>'s accessible name
onNavigate(event) => voidNoCalled on a page link click
classNamestringNoClasses for the <nav>

useContentListSearchInput

Keeps a search field's text apart from the URL while somebody types. A React hook.

NameTypeRequiredDefaultDescription
valuestring | undefinedYesThe term the URL holds
onCommit(term: string | undefined) => voidYesRuns with the trimmed term, or undefined
delaynumberNo500Milliseconds to wait after the last keystroke, CONTENT_LIST_SEARCH_DEBOUNCE_MS

It returns { value, setValue, commit }. commit() skips the wait, for example on Enter. When the URL's term changes on its own (back, forward, a "clear filters" link), value follows it.

Server helpers

These live in @vitnode/core/content/server and are server-only. Use them in a plugin's own public route, as the blog's GET /categories does:

ExportBehavior
resolveContentRequestLocale(c, definition)The request's locale - ?locale=, then Accept-Language, then the default - or null for an explicit locale the install does not serve
contentLocaleHeaders(resolved)Content-Language, plus Vary: Accept-Language when the header decided the locale
rememberContentPublicRead(c, { contentTypeId, key, load })Caches a read that is the same for every reader until the content type's next write
contentListPageCacheTags(definition)The tags a cached page that read the content type's records needs, for setPageCacheTags