Loading data for a block
A block stores an id. `.prefetch()` turns it into the record on the way out, or runs a query of your own, once per read and batched across the page.
The renderer needs a URL and the column holds "asset_77". This page covers the
three ways to close that gap on the server, what each one costs a read, and when
the work does not run at all.
Expanding a field you already store
Name the fields to expand. Each one is fetched and placed under
_data[blockId], beside the id that is still in _values.
.prefetch({ with: { backgroundImage: true } })Only relation and upload fields expand. A name in with that is not one of
those is skipped in silence, never half resolved. Expansions are batched by
target collection, field kind and nested with, so twenty heroes carrying one
upload each cost one query, not twenty.
To go a level deeper, nest with. The inner object is handed straight to the
target collection's find, so it behaves exactly like a normal relation load.
.prefetch({ with: { author: { with: { avatar: true } } } })Running your own query
For anything you cannot reach by following an id, pass a function. It receives
the block's typed values and a context, and whatever it returns becomes that
block's _data.
import { block } from "#questpie/factories";
export const latestNewsBlock = block("latest-news")
.fields(({ f }) => ({
count: f.number().default(3),
}))
.prefetch(async ({ values, ctx }) => {
const res = await ctx.collections.news.find({
limit: values.count ?? 3,
orderBy: { publishedAt: "desc" },
});
return { news: res.docs };
});ctx is the app context with blockId, blockType and locale added, so
ctx.collections, ctx.db and ctx.session are all in reach.
Both at once
Pass a loader alongside with and it runs after the expansion, with the
expanded records already in hand.
.prefetch({
with: { backgroundImage: true },
loader: async ({ values, expanded, ctx }) => ({
analytics: await getStats(expanded.backgroundImage?.id),
}),
})The two results are merged. Where a key appears in both, the loader wins.
What the renderer gets
An expanded field arrives as the whole record, or null when nothing comes
back for that id, whether the row is gone or the caller may not read it.
data.backgroundImage; // { id: "asset_77", url: "…", filename: "hero.jpg", … } | nullThe record is typed as Record<string, unknown> & { id: string }, which is why
reading a specific column in a renderer needs a cast. A field holding several
ids comes back as an array of the records that were found, with misses dropped.
The array case is typed as one record
For a field storing several ids the runtime writes an array, while the
inferred type still says one record or null. Narrow it yourself before you
map over it.
When it runs, and when it does not
Prefetch runs in the blocks field's afterRead hook, once per document, on
every read that returns the column and on the document a create or an update
hands back. It runs after the transaction commits, never inside it.
A read taken outside a request context, a direct database query with no app
context around it, returns the stored value untouched, so _data is simply
absent. Guard on it rather than assuming it.
A failed prefetch does not fail the page
If a loader throws, the error is logged, that one block's _data becomes
{ _error: "Prefetch failed" }, and every other block still renders.
Access
An upload field skips the asset collection's own read check and rides the parent
row's. The ids are content an editor put on a row the caller could already read,
so the asset comes back with it, minus any field the asset collection hides. A
plain relation gets no such treatment and goes through its own collection's read
rules, which is how a block can come back with a null where a private row was.
Related
- Blocks for the block file this chains onto.
- Loading related rows for the
withclause this reuses. - Access control for the read rules applied to an expanded relation.
Rendering a page
BlockRenderer walks the saved tree and calls one of your components per node. QUESTPIE never renders your site, it hands you the values and gets out of the way.
The block picker
`.admin()` is the only thing standing between an editor and thirty unlabelled block types. It sets the name, the icon and the group each block appears under.