Why route.loader() fails at build time
route.loader() makes HTTP requests. No server exists during next build, so fetches fail silently — static pages render empty. Solution: expose prefetchForRoute on the raw factory to seed React Query directly from the DB.
Required files per plugin
| File | Purpose |
|---|---|
api/query-key-defs.ts |
Shared key shapes — import into both query-keys.ts and prefetchForRoute |
api/serializers.ts |
Convert Date fields → ISO strings before setQueryData |
api/getters.ts |
Add any ID-based getters prefetchForRoute needs |
api/plugin.ts |
RouteKey type + typed overloads + wire prefetchForRoute into raw factory |
api/index.ts |
Re-export RouteKey, serializers, PLUGIN_QUERY_KEYS |
query-keys.ts |
Import discriminator fn from api/query-key-defs.ts |
client/plugin.tsx |
isConnectionError warn in each loader catch block |
Key rules
- Serialize
Date→ ISO string before everysetQueryDatacall — DB returnsDateobjects, HTTP cache holds strings. useInfiniteQuerylists require{ pages: [...], pageParams: [...] }shape insetQueryData. Flat arrays break hydration.- Share key builders via
api/query-key-defs.ts— never hardcode key shapes in two places. - One-time init steps (e.g. CMS
ensureSynced) — call once at the top ofprefetchForRoute; it's idempotent and safe for concurrent SSG. - In Next.js, keep request-aware routes under
app/(request)/pagesand SSG/ISR routes underapp/(static)/pages. Both groups still publish/pages/*URLs. - Put the reusable client
StackProvidershell inapp/pages/client-layout.tsx. Wrap it from a request layout that resolves origins from trusted request headers and a header-free static layout so SSG does not become dynamic.
Plugins with SSG support
| Plugin | Prefetched keys | Skipped |
|---|---|---|
| Blog | posts, drafts, post, tag, editPost |
newPost |
| CMS | dashboard, contentList, editContent |
newContent |
| Form Builder | formList, editForm, submissions |
newForm |
| Kanban | boards, board |
newBoard |
| AI Chat | — (per-user, not static) | all |
isConnectionError in loader catch
import { isConnectionError } from "@btst/stack/plugins/client"
// in each loader catch block:
if (isConnectionError(error)) {
console.warn("[btst/my-plugin] route.loader() failed — no server at build time. Use myStack.raw.myPlugin.prefetchForRoute() for SSG.")
}
Gotchas
route.loader()silently fails at build time — useprefetchForRoutein SSG pages instead.- Query key drift — always import discriminator fns from
api/query-key-defs.ts; never hardcode key shapes in two places. - Wrong shape for infinite queries —
setQueryDataneeds{ pages: [...], pageParams: [...] }, not a flat array. - Dates not serialized — always pass data through a serializer before
setQueryData.
Full code patterns
See REFERENCE.md for complete query-key-defs.ts, serializers.ts, prefetchForRoute overloads, and SSG page.tsx boilerplate.