@btst/stack 2.9.2 → 2.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +22 -11
  2. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +22 -11
  3. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +71 -39
  4. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +71 -39
  5. package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +62 -2
  6. package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +63 -3
  7. package/dist/packages/stack/src/plugins/comments/client/utils.cjs +2 -11
  8. package/dist/packages/stack/src/plugins/comments/client/utils.mjs +2 -11
  9. package/dist/packages/stack/src/plugins/comments/error-utils.cjs +15 -0
  10. package/dist/packages/stack/src/plugins/comments/error-utils.mjs +13 -0
  11. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +59 -31
  12. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +59 -31
  13. package/dist/packages/stack/src/plugins/media/db.cjs +10 -2
  14. package/dist/packages/stack/src/plugins/media/db.mjs +10 -2
  15. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +52 -25
  16. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +53 -26
  17. package/dist/packages/stack/src/plugins/utils.cjs +6 -0
  18. package/dist/packages/stack/src/plugins/utils.mjs +5 -1
  19. package/dist/plugins/client/index.cjs +2 -0
  20. package/dist/plugins/client/index.d.cts +15 -1
  21. package/dist/plugins/client/index.d.mts +15 -1
  22. package/dist/plugins/client/index.d.ts +15 -1
  23. package/dist/plugins/client/index.mjs +1 -1
  24. package/dist/plugins/comments/client/index.d.cts +5 -0
  25. package/dist/plugins/comments/client/index.d.mts +5 -0
  26. package/dist/plugins/comments/client/index.d.ts +5 -0
  27. package/dist/plugins/comments/query-keys.cjs +4 -4
  28. package/dist/plugins/comments/query-keys.mjs +1 -1
  29. package/package.json +1 -1
  30. package/src/__tests__/client-plugin-ssr-loaders.test.ts +329 -0
  31. package/src/plugins/blog/client/plugin.tsx +23 -14
  32. package/src/plugins/client/index.ts +2 -0
  33. package/src/plugins/cms/client/plugin.tsx +73 -42
  34. package/src/plugins/comments/client/plugin.tsx +82 -2
  35. package/src/plugins/comments/client/utils.ts +2 -14
  36. package/src/plugins/comments/error-utils.ts +17 -0
  37. package/src/plugins/comments/query-keys.ts +1 -1
  38. package/src/plugins/form-builder/client/plugin.tsx +59 -35
  39. package/src/plugins/media/__tests__/plugin.test.ts +4 -4
  40. package/src/plugins/media/db.ts +8 -0
  41. package/src/plugins/ui-builder/client/plugin.tsx +57 -27
  42. package/src/plugins/utils.ts +18 -0
@@ -0,0 +1,329 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { QueryClient } from "@tanstack/react-query";
3
+ import { createApiClient, SSR_LOADER_ERROR_MESSAGE } from "../plugins/client";
4
+ import { blogClientPlugin } from "../plugins/blog/client";
5
+ import type { BlogApiRouter } from "../plugins/blog/api";
6
+ import { createBlogQueryKeys } from "../plugins/blog/query-keys";
7
+ import { cmsClientPlugin } from "../plugins/cms/client";
8
+ import type { CMSApiRouter } from "../plugins/cms/api";
9
+ import { createCMSQueryKeys } from "../plugins/cms/query-keys";
10
+ import { formBuilderClientPlugin } from "../plugins/form-builder/client";
11
+ import type { FormBuilderApiRouter } from "../plugins/form-builder/api";
12
+ import { createFormBuilderQueryKeys } from "../plugins/form-builder/query-keys";
13
+ import { uiBuilderClientPlugin } from "../plugins/ui-builder/client";
14
+ import { UI_BUILDER_TYPE_SLUG } from "../plugins/ui-builder";
15
+ import { commentsClientPlugin } from "../plugins/comments/client";
16
+ import type { CommentsApiRouter } from "../plugins/comments/api";
17
+ import { createCommentsQueryKeys } from "../plugins/comments/query-keys";
18
+
19
+ const API_BASE_URL = "http://localhost:3000";
20
+ const API_BASE_PATH = "/api/data";
21
+ const SITE_BASE_URL = "http://localhost:3000";
22
+ const SITE_BASE_PATH = "/pages";
23
+ const TEST_HEADERS = new Headers();
24
+
25
+ function getErrorMessage(
26
+ queryClient: QueryClient,
27
+ queryKey: readonly unknown[],
28
+ ) {
29
+ const error = queryClient.getQueryState(queryKey)?.error;
30
+ return error instanceof Error ? error.message : null;
31
+ }
32
+
33
+ function mockFetchApiError(errorMessage: string) {
34
+ return vi.spyOn(globalThis, "fetch").mockResolvedValue(
35
+ new Response(JSON.stringify({ message: errorMessage }), {
36
+ status: 500,
37
+ headers: { "content-type": "application/json" },
38
+ }),
39
+ );
40
+ }
41
+
42
+ describe("client plugin SSR loaders", () => {
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ it("blog drafts loader seeds query error when beforeLoadPosts throws", async () => {
48
+ const queryClient = new QueryClient();
49
+ const expectedError = new Error("blog drafts blocked");
50
+
51
+ const plugin = blogClientPlugin({
52
+ apiBaseURL: API_BASE_URL,
53
+ apiBasePath: API_BASE_PATH,
54
+ siteBaseURL: SITE_BASE_URL,
55
+ siteBasePath: SITE_BASE_PATH,
56
+ queryClient,
57
+ headers: TEST_HEADERS,
58
+ hooks: {
59
+ beforeLoadPosts: () => {
60
+ throw expectedError;
61
+ },
62
+ },
63
+ });
64
+
65
+ const route = plugin.routes().drafts();
66
+ await route.loader?.();
67
+
68
+ const client = createApiClient<BlogApiRouter>({
69
+ baseURL: API_BASE_URL,
70
+ basePath: API_BASE_PATH,
71
+ });
72
+ const queries = createBlogQueryKeys(client, TEST_HEADERS);
73
+ const listQuery = queries.posts.list({
74
+ query: undefined,
75
+ limit: 10,
76
+ published: false,
77
+ });
78
+
79
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
80
+ SSR_LOADER_ERROR_MESSAGE,
81
+ );
82
+ });
83
+
84
+ it("cms content list loader seeds query error when beforeLoadContentList throws", async () => {
85
+ const queryClient = new QueryClient();
86
+ const expectedError = new Error("cms list blocked");
87
+ const typeSlug = "article";
88
+
89
+ const plugin = cmsClientPlugin({
90
+ apiBaseURL: API_BASE_URL,
91
+ apiBasePath: API_BASE_PATH,
92
+ siteBaseURL: SITE_BASE_URL,
93
+ siteBasePath: SITE_BASE_PATH,
94
+ queryClient,
95
+ headers: TEST_HEADERS,
96
+ hooks: {
97
+ beforeLoadContentList: () => {
98
+ throw expectedError;
99
+ },
100
+ },
101
+ });
102
+
103
+ const route = plugin.routes().contentList({ params: { typeSlug } });
104
+ await route.loader?.();
105
+
106
+ const client = createApiClient<CMSApiRouter>({
107
+ baseURL: API_BASE_URL,
108
+ basePath: API_BASE_PATH,
109
+ });
110
+ const queries = createCMSQueryKeys(client, TEST_HEADERS);
111
+ const listQuery = queries.cmsContent.list({
112
+ typeSlug,
113
+ limit: 20,
114
+ offset: 0,
115
+ });
116
+
117
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
118
+ SSR_LOADER_ERROR_MESSAGE,
119
+ );
120
+ });
121
+
122
+ it("form list loader seeds query error when beforeLoadFormList throws", async () => {
123
+ const queryClient = new QueryClient();
124
+ const expectedError = new Error("form list blocked");
125
+
126
+ const plugin = formBuilderClientPlugin({
127
+ apiBaseURL: API_BASE_URL,
128
+ apiBasePath: API_BASE_PATH,
129
+ siteBaseURL: SITE_BASE_URL,
130
+ siteBasePath: SITE_BASE_PATH,
131
+ queryClient,
132
+ headers: TEST_HEADERS,
133
+ hooks: {
134
+ beforeLoadFormList: () => {
135
+ throw expectedError;
136
+ },
137
+ },
138
+ });
139
+
140
+ const route = plugin.routes().formList();
141
+ await route.loader?.();
142
+
143
+ const client = createApiClient<FormBuilderApiRouter>({
144
+ baseURL: API_BASE_URL,
145
+ basePath: API_BASE_PATH,
146
+ });
147
+ const queries = createFormBuilderQueryKeys(client, TEST_HEADERS);
148
+ const listQuery = queries.forms.list({ limit: 20, offset: 0 });
149
+
150
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
151
+ SSR_LOADER_ERROR_MESSAGE,
152
+ );
153
+ });
154
+
155
+ it("ui-builder list loader seeds query error when beforeLoadPageList throws", async () => {
156
+ const queryClient = new QueryClient();
157
+ const expectedError = new Error("ui-builder list blocked");
158
+
159
+ const plugin = uiBuilderClientPlugin({
160
+ apiBaseURL: API_BASE_URL,
161
+ apiBasePath: API_BASE_PATH,
162
+ siteBaseURL: SITE_BASE_URL,
163
+ siteBasePath: SITE_BASE_PATH,
164
+ queryClient,
165
+ headers: TEST_HEADERS,
166
+ componentRegistry: {},
167
+ hooks: {
168
+ beforeLoadPageList: () => {
169
+ throw expectedError;
170
+ },
171
+ },
172
+ });
173
+
174
+ const route = plugin.routes().pageList();
175
+ await route.loader?.();
176
+
177
+ const client = createApiClient<CMSApiRouter>({
178
+ baseURL: API_BASE_URL,
179
+ basePath: API_BASE_PATH,
180
+ });
181
+ const queries = createCMSQueryKeys(client, TEST_HEADERS);
182
+ const listQuery = queries.cmsContent.list({
183
+ typeSlug: UI_BUILDER_TYPE_SLUG,
184
+ limit: 20,
185
+ offset: 0,
186
+ });
187
+ const uiBuilderQueryKey = [...listQuery.queryKey, "ui-builder"] as const;
188
+
189
+ expect(getErrorMessage(queryClient, uiBuilderQueryKey)).toBe(
190
+ SSR_LOADER_ERROR_MESSAGE,
191
+ );
192
+ });
193
+
194
+ it("comments moderation loader seeds query error when beforeLoadModeration throws", async () => {
195
+ const queryClient = new QueryClient();
196
+ const expectedError = new Error("comments moderation blocked");
197
+
198
+ const plugin = commentsClientPlugin({
199
+ apiBaseURL: API_BASE_URL,
200
+ apiBasePath: API_BASE_PATH,
201
+ siteBaseURL: SITE_BASE_URL,
202
+ siteBasePath: SITE_BASE_PATH,
203
+ queryClient,
204
+ headers: TEST_HEADERS,
205
+ hooks: {
206
+ beforeLoadModeration: () => {
207
+ throw expectedError;
208
+ },
209
+ },
210
+ });
211
+
212
+ const route = plugin.routes().moderation();
213
+ await route.loader?.();
214
+
215
+ const client = createApiClient<CommentsApiRouter>({
216
+ baseURL: API_BASE_URL,
217
+ basePath: API_BASE_PATH,
218
+ });
219
+ const queries = createCommentsQueryKeys(client, TEST_HEADERS);
220
+ const listQuery = queries.comments.list({
221
+ status: "pending",
222
+ limit: 20,
223
+ offset: 0,
224
+ });
225
+
226
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
227
+ SSR_LOADER_ERROR_MESSAGE,
228
+ );
229
+ });
230
+
231
+ it("comments user loader seeds query error with user-scoped key when beforeLoadUserComments throws", async () => {
232
+ const queryClient = new QueryClient();
233
+ const expectedError = new Error("comments user view blocked");
234
+ const currentUserId = "user-123";
235
+
236
+ const plugin = commentsClientPlugin({
237
+ apiBaseURL: API_BASE_URL,
238
+ apiBasePath: API_BASE_PATH,
239
+ siteBaseURL: SITE_BASE_URL,
240
+ siteBasePath: SITE_BASE_PATH,
241
+ queryClient,
242
+ headers: TEST_HEADERS,
243
+ hooks: {
244
+ beforeLoadUserComments: (context) => {
245
+ context.currentUserId = currentUserId;
246
+ throw expectedError;
247
+ },
248
+ },
249
+ });
250
+
251
+ const route = plugin.routes().userComments();
252
+ await route.loader?.();
253
+
254
+ const client = createApiClient<CommentsApiRouter>({
255
+ baseURL: API_BASE_URL,
256
+ basePath: API_BASE_PATH,
257
+ });
258
+ const queries = createCommentsQueryKeys(client, TEST_HEADERS);
259
+ const listQuery = queries.comments.list({
260
+ authorId: currentUserId,
261
+ sort: "desc",
262
+ limit: 20,
263
+ offset: 0,
264
+ });
265
+
266
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
267
+ SSR_LOADER_ERROR_MESSAGE,
268
+ );
269
+ });
270
+
271
+ it("comments moderation loader calls onLoadError when prefetch stores API error", async () => {
272
+ const queryClient = new QueryClient();
273
+ const apiErrorMessage = "comments moderation api failed";
274
+ const onLoadError = vi.fn();
275
+ mockFetchApiError(apiErrorMessage);
276
+
277
+ const plugin = commentsClientPlugin({
278
+ apiBaseURL: API_BASE_URL,
279
+ apiBasePath: API_BASE_PATH,
280
+ siteBaseURL: SITE_BASE_URL,
281
+ siteBasePath: SITE_BASE_PATH,
282
+ queryClient,
283
+ headers: TEST_HEADERS,
284
+ hooks: {
285
+ onLoadError,
286
+ },
287
+ });
288
+
289
+ const route = plugin.routes().moderation();
290
+ await route.loader?.();
291
+
292
+ expect(onLoadError).toHaveBeenCalledTimes(1);
293
+ const [errorArg] = onLoadError.mock.calls[0] ?? [];
294
+ expect(errorArg).toBeInstanceOf(Error);
295
+ expect((errorArg as Error).message).toBe(apiErrorMessage);
296
+ });
297
+
298
+ it("comments user loader calls onLoadError when user-scoped prefetch stores API error", async () => {
299
+ const queryClient = new QueryClient();
300
+ const apiErrorMessage = "comments user api failed";
301
+ const onLoadError = vi.fn();
302
+ const currentUserId = "user-123";
303
+ mockFetchApiError(apiErrorMessage);
304
+
305
+ const plugin = commentsClientPlugin({
306
+ apiBaseURL: API_BASE_URL,
307
+ apiBasePath: API_BASE_PATH,
308
+ siteBaseURL: SITE_BASE_URL,
309
+ siteBasePath: SITE_BASE_PATH,
310
+ queryClient,
311
+ headers: TEST_HEADERS,
312
+ hooks: {
313
+ beforeLoadUserComments: (context) => {
314
+ context.currentUserId = currentUserId;
315
+ },
316
+ onLoadError,
317
+ },
318
+ });
319
+
320
+ const route = plugin.routes().userComments();
321
+ await route.loader?.();
322
+
323
+ expect(onLoadError).toHaveBeenCalledTimes(1);
324
+ const [errorArg, contextArg] = onLoadError.mock.calls[0] ?? [];
325
+ expect(errorArg).toBeInstanceOf(Error);
326
+ expect((errorArg as Error).message).toBe(apiErrorMessage);
327
+ expect(contextArg).toMatchObject({ currentUserId });
328
+ });
329
+ });
@@ -7,6 +7,7 @@ import {
7
7
  import { createRoute } from "@btst/yar";
8
8
  import type { ComponentType } from "react";
9
9
  import type { QueryClient } from "@tanstack/react-query";
10
+ import { createSanitizedSSRLoaderError } from "../../utils";
10
11
  import type { BlogApiRouter } from "../api";
11
12
  import { createBlogQueryKeys } from "../query-keys";
12
13
  import type { Post, SerializedPost, SerializedTag } from "../types";
@@ -183,6 +184,18 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
183
184
  apiBasePath,
184
185
  headers,
185
186
  };
187
+ const limit = 10;
188
+ const client = createApiClient<BlogApiRouter>({
189
+ baseURL: apiBaseURL,
190
+ basePath: apiBasePath,
191
+ });
192
+ // note: for a module not to be bundled with client, and to be shared by client and server we need to add it to build.config.ts as an entry
193
+ const queries = createBlogQueryKeys(client, headers);
194
+ const listQuery = queries.posts.list({
195
+ query: undefined,
196
+ limit,
197
+ published: published,
198
+ });
186
199
 
187
200
  try {
188
201
  // Before hook
@@ -193,20 +206,6 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
193
206
  );
194
207
  }
195
208
 
196
- const limit = 10;
197
- const client = createApiClient<BlogApiRouter>({
198
- baseURL: apiBaseURL,
199
- basePath: apiBasePath,
200
- });
201
-
202
- // note: for a module not to be bundled with client, and to be shared by client and server we need to add it to build.config.ts as an entry
203
- const queries = createBlogQueryKeys(client, headers);
204
- const listQuery = queries.posts.list({
205
- query: undefined,
206
- limit,
207
- published: published,
208
- });
209
-
210
209
  await queryClient.prefetchInfiniteQuery({
211
210
  ...listQuery,
212
211
  initialPageParam: 0,
@@ -250,6 +249,16 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
250
249
  "[btst/blog] route.loader() failed — no server running at build time. " +
251
250
  "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
252
251
  );
252
+ } else {
253
+ const errToStore = createSanitizedSSRLoaderError();
254
+ await queryClient.prefetchInfiniteQuery({
255
+ queryKey: listQuery.queryKey,
256
+ queryFn: () => {
257
+ throw errToStore;
258
+ },
259
+ initialPageParam: 0,
260
+ retry: false,
261
+ });
253
262
  }
254
263
  if (hooks?.onLoadError) {
255
264
  await hooks.onLoadError(error as Error, context);
@@ -20,8 +20,10 @@ export type {
20
20
 
21
21
  export {
22
22
  createApiClient,
23
+ createSanitizedSSRLoaderError,
23
24
  isConnectionError,
24
25
  runClientHookWithShim,
26
+ SSR_LOADER_ERROR_MESSAGE,
25
27
  } from "../utils";
26
28
 
27
29
  // Re-export Yar types needed for plugins
@@ -8,6 +8,7 @@ import {
8
8
  import { createRoute } from "@btst/yar";
9
9
  import type { ComponentType } from "react";
10
10
  import type { QueryClient } from "@tanstack/react-query";
11
+ import { createSanitizedSSRLoaderError } from "../../utils";
11
12
  import type { CMSApiRouter } from "../api";
12
13
  import { createCMSQueryKeys } from "../query-keys";
13
14
 
@@ -163,6 +164,12 @@ function createDashboardLoader(config: CMSClientConfig) {
163
164
  apiBasePath,
164
165
  headers,
165
166
  };
167
+ const client = createApiClient<CMSApiRouter>({
168
+ baseURL: apiBaseURL,
169
+ basePath: apiBasePath,
170
+ });
171
+ const queries = createCMSQueryKeys(client, headers);
172
+ const typesQuery = queries.cmsTypes.list();
166
173
 
167
174
  try {
168
175
  // Before hook - authorization check
@@ -173,13 +180,7 @@ function createDashboardLoader(config: CMSClientConfig) {
173
180
  );
174
181
  }
175
182
 
176
- const client = createApiClient<CMSApiRouter>({
177
- baseURL: apiBaseURL,
178
- basePath: apiBasePath,
179
- });
180
- const queries = createCMSQueryKeys(client, headers);
181
-
182
- await queryClient.prefetchQuery(queries.cmsTypes.list());
183
+ await queryClient.prefetchQuery(typesQuery);
183
184
 
184
185
  // After hook
185
186
  if (hooks?.afterLoadDashboard) {
@@ -187,9 +188,7 @@ function createDashboardLoader(config: CMSClientConfig) {
187
188
  }
188
189
 
189
190
  // Check if there was an error
190
- const queryState = queryClient.getQueryState(
191
- queries.cmsTypes.list().queryKey,
192
- );
191
+ const queryState = queryClient.getQueryState(typesQuery.queryKey);
193
192
  if (queryState?.error && hooks?.onLoadError) {
194
193
  const error =
195
194
  queryState.error instanceof Error
@@ -205,6 +204,15 @@ function createDashboardLoader(config: CMSClientConfig) {
205
204
  "[btst/cms] route.loader() failed — no server running at build time. " +
206
205
  "Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
207
206
  );
207
+ } else {
208
+ const errToStore = createSanitizedSSRLoaderError();
209
+ await queryClient.prefetchQuery({
210
+ queryKey: typesQuery.queryKey,
211
+ queryFn: () => {
212
+ throw errToStore;
213
+ },
214
+ retry: false,
215
+ });
208
216
  }
209
217
  if (hooks?.onLoadError) {
210
218
  await hooks.onLoadError(error as Error, context);
@@ -231,6 +239,18 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
231
239
  apiBasePath,
232
240
  headers,
233
241
  };
242
+ const client = createApiClient<CMSApiRouter>({
243
+ baseURL: apiBaseURL,
244
+ basePath: apiBasePath,
245
+ });
246
+ const queries = createCMSQueryKeys(client, headers);
247
+ const limit = 20;
248
+ const typesQuery = queries.cmsTypes.list();
249
+ const listQuery = queries.cmsContent.list({
250
+ typeSlug,
251
+ limit,
252
+ offset: 0,
253
+ });
234
254
 
235
255
  try {
236
256
  // Before hook - authorization check
@@ -241,22 +261,10 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
241
261
  );
242
262
  }
243
263
 
244
- const client = createApiClient<CMSApiRouter>({
245
- baseURL: apiBaseURL,
246
- basePath: apiBasePath,
247
- });
248
- const queries = createCMSQueryKeys(client, headers);
249
- const limit = 20;
250
-
251
264
  // Prefetch content types
252
- await queryClient.prefetchQuery(queries.cmsTypes.list());
265
+ await queryClient.prefetchQuery(typesQuery);
253
266
 
254
267
  // Prefetch content list using infinite query (matches useSuspenseInfiniteQuery in hooks)
255
- const listQuery = queries.cmsContent.list({
256
- typeSlug,
257
- limit,
258
- offset: 0,
259
- });
260
268
  await queryClient.prefetchInfiniteQuery({
261
269
  queryKey: listQuery.queryKey,
262
270
  queryFn: async ({ pageParam = 0 }) => {
@@ -285,9 +293,7 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
285
293
  }
286
294
 
287
295
  // Check if there was an error in either query
288
- const typesState = queryClient.getQueryState(
289
- queries.cmsTypes.list().queryKey,
290
- );
296
+ const typesState = queryClient.getQueryState(typesQuery.queryKey);
291
297
  const listState = queryClient.getQueryState(listQuery.queryKey);
292
298
  const queryError = typesState?.error || listState?.error;
293
299
  if (queryError && hooks?.onLoadError) {
@@ -305,6 +311,16 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
305
311
  "[btst/cms] route.loader() failed — no server running at build time. " +
306
312
  "Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
307
313
  );
314
+ } else {
315
+ const errToStore = createSanitizedSSRLoaderError();
316
+ await queryClient.prefetchInfiniteQuery({
317
+ queryKey: listQuery.queryKey,
318
+ queryFn: () => {
319
+ throw errToStore;
320
+ },
321
+ initialPageParam: 0,
322
+ retry: false,
323
+ });
308
324
  }
309
325
  if (hooks?.onLoadError) {
310
326
  await hooks.onLoadError(error as Error, context);
@@ -335,6 +351,15 @@ function createContentEditorLoader(
335
351
  apiBasePath,
336
352
  headers,
337
353
  };
354
+ const client = createApiClient<CMSApiRouter>({
355
+ baseURL: apiBaseURL,
356
+ basePath: apiBasePath,
357
+ });
358
+ const queries = createCMSQueryKeys(client, headers);
359
+ const typesQuery = queries.cmsTypes.list();
360
+ const detailQuery = id
361
+ ? queries.cmsContent.detail(typeSlug, id)
362
+ : undefined;
338
363
 
339
364
  try {
340
365
  // Before hook - authorization check
@@ -345,17 +370,9 @@ function createContentEditorLoader(
345
370
  );
346
371
  }
347
372
 
348
- const client = createApiClient<CMSApiRouter>({
349
- baseURL: apiBaseURL,
350
- basePath: apiBasePath,
351
- });
352
- const queries = createCMSQueryKeys(client, headers);
353
-
354
- const promises = [queryClient.prefetchQuery(queries.cmsTypes.list())];
373
+ const promises = [queryClient.prefetchQuery(typesQuery)];
355
374
  if (id) {
356
- promises.push(
357
- queryClient.prefetchQuery(queries.cmsContent.detail(typeSlug, id)),
358
- );
375
+ promises.push(queryClient.prefetchQuery(detailQuery!));
359
376
  }
360
377
  await Promise.all(promises);
361
378
 
@@ -365,13 +382,9 @@ function createContentEditorLoader(
365
382
  }
366
383
 
367
384
  // Check if there was an error
368
- const typesState = queryClient.getQueryState(
369
- queries.cmsTypes.list().queryKey,
370
- );
385
+ const typesState = queryClient.getQueryState(typesQuery.queryKey);
371
386
  const itemState = id
372
- ? queryClient.getQueryState(
373
- queries.cmsContent.detail(typeSlug, id).queryKey,
374
- )
387
+ ? queryClient.getQueryState(detailQuery!.queryKey)
375
388
  : null;
376
389
  const queryError = typesState?.error || itemState?.error;
377
390
  if (queryError && hooks?.onLoadError) {
@@ -389,6 +402,24 @@ function createContentEditorLoader(
389
402
  "[btst/cms] route.loader() failed — no server running at build time. " +
390
403
  "Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
391
404
  );
405
+ } else {
406
+ const errToStore = createSanitizedSSRLoaderError();
407
+ await queryClient.prefetchQuery({
408
+ queryKey: typesQuery.queryKey,
409
+ queryFn: () => {
410
+ throw errToStore;
411
+ },
412
+ retry: false,
413
+ });
414
+ if (detailQuery) {
415
+ await queryClient.prefetchQuery({
416
+ queryKey: detailQuery.queryKey,
417
+ queryFn: () => {
418
+ throw errToStore;
419
+ },
420
+ retry: false,
421
+ });
422
+ }
392
423
  }
393
424
  if (hooks?.onLoadError) {
394
425
  await hooks.onLoadError(error as Error, context);