@btst/stack 1.1.6 → 1.1.8

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 (37) hide show
  1. package/dist/packages/better-stack/src/plugins/blog/api/plugin.cjs +29 -34
  2. package/dist/packages/better-stack/src/plugins/blog/api/plugin.mjs +29 -34
  3. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +3 -3
  4. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +4 -4
  5. package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.cjs +1 -1
  6. package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.mjs +1 -1
  7. package/dist/packages/better-stack/src/plugins/blog/client/hooks/blog-hooks.cjs +16 -16
  8. package/dist/packages/better-stack/src/plugins/blog/client/hooks/blog-hooks.mjs +16 -16
  9. package/dist/packages/better-stack/src/plugins/blog/client/plugin.cjs +50 -8
  10. package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +50 -8
  11. package/dist/plugins/blog/api/index.d.cts +1 -1
  12. package/dist/plugins/blog/api/index.d.mts +1 -1
  13. package/dist/plugins/blog/api/index.d.ts +1 -1
  14. package/dist/plugins/blog/client/hooks/index.d.cts +3 -3
  15. package/dist/plugins/blog/client/hooks/index.d.mts +3 -3
  16. package/dist/plugins/blog/client/hooks/index.d.ts +3 -3
  17. package/dist/plugins/blog/client/index.d.cts +11 -4
  18. package/dist/plugins/blog/client/index.d.mts +11 -4
  19. package/dist/plugins/blog/client/index.d.ts +11 -4
  20. package/dist/plugins/blog/query-keys.cjs +19 -13
  21. package/dist/plugins/blog/query-keys.d.cts +6 -6
  22. package/dist/plugins/blog/query-keys.d.mts +6 -6
  23. package/dist/plugins/blog/query-keys.d.ts +6 -6
  24. package/dist/plugins/blog/query-keys.mjs +19 -13
  25. package/dist/plugins/blog/style.css +2 -5
  26. package/package.json +3 -3
  27. package/src/plugins/blog/api/plugin.ts +56 -36
  28. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +4 -4
  29. package/src/plugins/blog/client/components/shared/default-error.tsx +4 -1
  30. package/src/plugins/blog/client/hooks/blog-hooks.tsx +16 -16
  31. package/src/plugins/blog/client/overrides.ts +4 -0
  32. package/src/plugins/blog/client/plugin.tsx +67 -11
  33. package/src/plugins/blog/query-keys.ts +13 -3
  34. package/src/plugins/blog/style.css +2 -5
  35. package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CoPoHVfV.d.cts} +2 -2
  36. package/dist/shared/{stack.CbuN2zVV.d.mts → stack.CoPoHVfV.d.mts} +2 -2
  37. package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CoPoHVfV.d.ts} +2 -2
@@ -16,13 +16,13 @@ function toError(error) {
16
16
  }
17
17
  return new Error(String(error));
18
18
  }
19
- function createBlogQueryKeys(client) {
20
- const posts = createPostsQueries(client);
21
- const drafts = createDraftsQueries(client);
22
- const tags = createTagsQueries(client);
19
+ function createBlogQueryKeys(client, headers) {
20
+ const posts = createPostsQueries(client, headers);
21
+ const drafts = createDraftsQueries(client, headers);
22
+ const tags = createTagsQueries(client, headers);
23
23
  return mergeQueryKeys(posts, drafts, tags);
24
24
  }
25
- function createPostsQueries(client) {
25
+ function createPostsQueries(client, headers) {
26
26
  return createQueryKeys("posts", {
27
27
  list: (params) => ({
28
28
  queryKey: [
@@ -43,7 +43,8 @@ function createPostsQueries(client) {
43
43
  limit: params?.limit ?? 10,
44
44
  published: params?.published !== void 0 ? params.published ? "true" : "false" : void 0,
45
45
  tagSlug: params?.tagSlug
46
- }
46
+ },
47
+ headers
47
48
  });
48
49
  if (isErrorResponse(response)) {
49
50
  const errorResponse = response;
@@ -63,7 +64,8 @@ function createPostsQueries(client) {
63
64
  try {
64
65
  const response = await client("/posts", {
65
66
  method: "GET",
66
- query: { slug, limit: 1 }
67
+ query: { slug, limit: 1 },
68
+ headers
67
69
  });
68
70
  if (isErrorResponse(response)) {
69
71
  const errorResponse = response;
@@ -85,7 +87,8 @@ function createPostsQueries(client) {
85
87
  method: "GET",
86
88
  query: {
87
89
  date: dateValue.toISOString()
88
- }
90
+ },
91
+ headers
89
92
  });
90
93
  if (isErrorResponse(response)) {
91
94
  const errorResponse = response;
@@ -105,7 +108,8 @@ function createPostsQueries(client) {
105
108
  query: {
106
109
  limit: params?.limit ?? 5,
107
110
  published: "true"
108
- }
111
+ },
112
+ headers
109
113
  });
110
114
  if (isErrorResponse(response)) {
111
115
  const errorResponse = response;
@@ -123,7 +127,7 @@ function createPostsQueries(client) {
123
127
  })
124
128
  });
125
129
  }
126
- function createDraftsQueries(client) {
130
+ function createDraftsQueries(client, headers) {
127
131
  return createQueryKeys("drafts", {
128
132
  list: (params) => ({
129
133
  queryKey: [
@@ -140,7 +144,8 @@ function createDraftsQueries(client) {
140
144
  offset: pageParam ?? 0,
141
145
  limit: params?.limit ?? 10,
142
146
  published: "false"
143
- }
147
+ },
148
+ headers
144
149
  });
145
150
  if (isErrorResponse(response)) {
146
151
  const errorResponse = response;
@@ -154,14 +159,15 @@ function createDraftsQueries(client) {
154
159
  })
155
160
  });
156
161
  }
157
- function createTagsQueries(client) {
162
+ function createTagsQueries(client, headers) {
158
163
  return createQueryKeys("tags", {
159
164
  list: () => ({
160
165
  queryKey: ["tags"],
161
166
  queryFn: async () => {
162
167
  try {
163
168
  const response = await client("/tags", {
164
- method: "GET"
169
+ method: "GET",
170
+ headers
165
171
  });
166
172
  if (isErrorResponse(response)) {
167
173
  const errorResponse = response;
@@ -11,11 +11,8 @@
11
11
  /* Scan this package's source files for Tailwind classes */
12
12
  @source "../../../src/**/*.{ts,tsx}";
13
13
 
14
- /* Scan UI package components (if in monorepo) */
15
- @source "../../../../@workspace/ui/src/**/*.{ts,tsx}";
16
-
17
- /* Scan UI package components (if installed as npm package) */
18
- @source "../../../node_modules/@workspace/ui/src/**/*.{ts,tsx}";
14
+ /* Scan UI package components (when installed as npm package the UI package will be in this dir) */
15
+ @source "../../packages/ui/src";
19
16
 
20
17
  /*
21
18
  * alternatively consumer can use @source "../node_modules/@btst/stack/src/**\/*.{ts,tsx}";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -157,7 +157,7 @@
157
157
  }
158
158
  },
159
159
  "dependencies": {
160
- "@btst/db": "1.0.2",
160
+ "@btst/db": "1.0.3",
161
161
  "@lukemorales/query-key-factory": "^1.3.4",
162
162
  "@milkdown/crepe": "^7.17.1",
163
163
  "@milkdown/kit": "^7.17.1",
@@ -196,7 +196,7 @@
196
196
  "zod": ">=3.24.0"
197
197
  },
198
198
  "devDependencies": {
199
- "@btst/adapter-memory": "1.0.2",
199
+ "@btst/adapter-memory": "1.0.3",
200
200
  "@btst/yar": "1.1.1",
201
201
  "@types/react": "^19.0.0",
202
202
  "@types/slug": "^5.0.9",
@@ -661,17 +661,27 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
661
661
  }
662
662
 
663
663
  const date = query.date;
664
-
665
- // Get previous post (createdAt < date, newest first)
666
- const previousPost = await adapter.findMany<Post>({
664
+ const targetTime = new Date(date).getTime();
665
+
666
+ // Window-based approach for finding next/previous posts
667
+ // This avoids relying on comparison operators (lt/gt) which may not be
668
+ // consistently implemented across all database adapters (e.g., Drizzle).
669
+ //
670
+ // Strategy:
671
+ // 1. Fetch a window of recent published posts (sorted by date DESC)
672
+ // 2. Filter in memory to find posts immediately before/after target date
673
+ //
674
+ // Trade-offs:
675
+ // - Works reliably across all adapters (only uses eq operator and sorting)
676
+ // - Efficient for typical blog sizes (100 most recent posts)
677
+ // - Limitation: If target post is outside the window, we may not find neighbors
678
+ // (acceptable for typical blog navigation where users browse recent content)
679
+ const WINDOW_SIZE = 100;
680
+
681
+ const allPosts = await adapter.findMany<Post>({
667
682
  model: "post",
668
- limit: 1,
683
+ limit: WINDOW_SIZE,
669
684
  where: [
670
- {
671
- field: "createdAt",
672
- value: date,
673
- operator: "lt" as const,
674
- },
675
685
  {
676
686
  field: "published",
677
687
  value: true,
@@ -684,30 +694,40 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
684
694
  },
685
695
  });
686
696
 
687
- const nextPost = await adapter.findMany<Post>({
688
- model: "post",
689
- limit: 1,
690
- where: [
691
- {
692
- field: "createdAt",
693
- value: date,
694
- operator: "gt" as const,
695
- },
696
- {
697
- field: "published",
698
- value: true,
699
- operator: "eq" as const,
700
- },
701
- ],
702
- sortBy: {
703
- field: "createdAt",
704
- direction: "asc",
705
- },
697
+ // Sort posts by createdAt descending (newest first)
698
+ const sortedPosts = allPosts.sort((a, b) => {
699
+ const timeA = new Date(a.createdAt).getTime();
700
+ const timeB = new Date(b.createdAt).getTime();
701
+ return timeB - timeA;
706
702
  });
707
703
 
704
+ // Find posts immediately before and after the target date
705
+ // In DESC sorted array: newer posts come before, older posts come after
706
+ let previousPost: Post | null = null;
707
+ let nextPost: Post | null = null;
708
+
709
+ for (let i = 0; i < sortedPosts.length; i++) {
710
+ const post = sortedPosts[i];
711
+ if (!post) continue;
712
+
713
+ const postTime = new Date(post.createdAt).getTime();
714
+
715
+ if (postTime > targetTime) {
716
+ // This post is newer than target - it's a next post candidate
717
+ // Keep the last one we find (closest to target date)
718
+ nextPost = post;
719
+ } else if (postTime < targetTime) {
720
+ // This is the first post older than target - this is the previous post
721
+ previousPost = post;
722
+ // We've found both neighbors, no need to continue
723
+ break;
724
+ }
725
+ // Skip posts with exactly the same timestamp (the current post itself)
726
+ }
727
+
708
728
  const postIds = [
709
- ...(previousPost?.[0] ? [previousPost[0].id] : []),
710
- ...(nextPost?.[0] ? [nextPost[0].id] : []),
729
+ ...(previousPost ? [previousPost.id] : []),
730
+ ...(nextPost ? [nextPost.id] : []),
711
731
  ];
712
732
  const postTagsMap = await loadTagsForPosts(
713
733
  postIds,
@@ -716,16 +736,16 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
716
736
  );
717
737
 
718
738
  return {
719
- previous: previousPost?.[0]
739
+ previous: previousPost
720
740
  ? {
721
- ...previousPost[0],
722
- tags: postTagsMap.get(previousPost[0].id) || [],
741
+ ...previousPost,
742
+ tags: postTagsMap.get(previousPost.id) || [],
723
743
  }
724
744
  : null,
725
- next: nextPost?.[0]
745
+ next: nextPost
726
746
  ? {
727
- ...nextPost[0],
728
- tags: postTagsMap.get(nextPost[0].id) || [],
747
+ ...nextPost,
748
+ tags: postTagsMap.get(nextPost.id) || [],
729
749
  }
730
750
  : null,
731
751
  };
@@ -84,7 +84,7 @@ export function PostPage({ slug }: { slug: string }) {
84
84
  />
85
85
 
86
86
  {post.image && (
87
- <div className="flex flex-col gap-2 mt-6 aspect-video w-full relative">
87
+ <div className="flex flex-col gap-2 my-6 aspect-video w-full relative">
88
88
  <Image
89
89
  src={post.image}
90
90
  alt={post.title}
@@ -122,12 +122,12 @@ function PostHeaderTop({ post }: { post: SerializedPost }) {
122
122
  });
123
123
  const basePath = useBasePath();
124
124
  return (
125
- <div className="flex flex-row items-center gap-2 flex-wrap mt-8">
125
+ <div className="flex flex-row items-center justify-center gap-2 flex-wrap mt-8">
126
126
  <span className="font-light text-muted-foreground text-sm">
127
127
  {formatDate(post.createdAt, "MMMM d, yyyy")}
128
128
  </span>
129
129
  {post.tags && post.tags.length > 0 && (
130
- <div className="flex flex-wrap gap-2">
130
+ <>
131
131
  {post.tags.map((tag) => (
132
132
  <Link key={tag.id} href={`${basePath}/blog/tag/${tag.slug}`}>
133
133
  <Badge variant="secondary" className="text-xs">
@@ -135,7 +135,7 @@ function PostHeaderTop({ post }: { post: SerializedPost }) {
135
135
  </Badge>
136
136
  </Link>
137
137
  ))}
138
- </div>
138
+ </>
139
139
  )}
140
140
  </div>
141
141
  );
@@ -15,6 +15,9 @@ export function DefaultError({ error }: FallbackProps) {
15
15
  localization: BLOG_LOCALIZATION,
16
16
  });
17
17
  const title = localization.BLOG_GENERIC_ERROR_TITLE;
18
- const message = error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
18
+ const message =
19
+ process.env.NODE_ENV === "production"
20
+ ? localization.BLOG_GENERIC_ERROR_MESSAGE
21
+ : (error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE);
19
22
  return <ErrorPlaceholder title={title} message={message} />;
20
23
  }
@@ -86,7 +86,7 @@ export type PostUpdateInput = z.infer<typeof updatePostSchema>;
86
86
  * Hook for fetching paginated posts with load more functionality
87
87
  */
88
88
  export function usePosts(options: UsePostsOptions = {}): UsePostsResult {
89
- const { apiBaseURL, apiBasePath } =
89
+ const { apiBaseURL, apiBasePath, headers } =
90
90
  usePluginOverrides<BlogPluginOverrides>("blog");
91
91
  const client = createApiClient<BlogApiRouter>({
92
92
  baseURL: apiBaseURL,
@@ -100,7 +100,7 @@ export function usePosts(options: UsePostsOptions = {}): UsePostsResult {
100
100
  query,
101
101
  published,
102
102
  } = options;
103
- const queries = createBlogQueryKeys(client);
103
+ const queries = createBlogQueryKeys(client, headers);
104
104
 
105
105
  const queryParams = {
106
106
  tag,
@@ -155,7 +155,7 @@ export function useSuspensePosts(options: UsePostsOptions = {}): {
155
155
  isLoadingMore: boolean;
156
156
  refetch: () => Promise<unknown>;
157
157
  } {
158
- const { apiBaseURL, apiBasePath } =
158
+ const { apiBaseURL, apiBasePath, headers } =
159
159
  usePluginOverrides<BlogPluginOverrides>("blog");
160
160
  const client = createApiClient<BlogApiRouter>({
161
161
  baseURL: apiBaseURL,
@@ -169,7 +169,7 @@ export function useSuspensePosts(options: UsePostsOptions = {}): {
169
169
  query,
170
170
  published,
171
171
  } = options;
172
- const queries = createBlogQueryKeys(client);
172
+ const queries = createBlogQueryKeys(client, headers);
173
173
 
174
174
  const queryParams = { tag, tagSlug, limit, query, published };
175
175
  const basePosts = queries.posts.list(queryParams);
@@ -214,13 +214,13 @@ export function useSuspensePosts(options: UsePostsOptions = {}): {
214
214
  * Hook for fetching a single post by slug
215
215
  */
216
216
  export function usePost(slug?: string): UsePostResult {
217
- const { apiBaseURL, apiBasePath } =
217
+ const { apiBaseURL, apiBasePath, headers } =
218
218
  usePluginOverrides<BlogPluginOverrides>("blog");
219
219
  const client = createApiClient<BlogApiRouter>({
220
220
  baseURL: apiBaseURL,
221
221
  basePath: apiBasePath,
222
222
  });
223
- const queries = createBlogQueryKeys(client);
223
+ const queries = createBlogQueryKeys(client, headers);
224
224
 
225
225
  const basePost = queries.posts.detail(slug ?? "");
226
226
  const { data, isLoading, error, refetch } = useQuery<
@@ -247,13 +247,13 @@ export function useSuspensePost(slug: string): {
247
247
  post: SerializedPost | null;
248
248
  refetch: () => Promise<unknown>;
249
249
  } {
250
- const { apiBaseURL, apiBasePath } =
250
+ const { apiBaseURL, apiBasePath, headers } =
251
251
  usePluginOverrides<BlogPluginOverrides>("blog");
252
252
  const client = createApiClient<BlogApiRouter>({
253
253
  baseURL: apiBaseURL,
254
254
  basePath: apiBasePath,
255
255
  });
256
- const queries = createBlogQueryKeys(client);
256
+ const queries = createBlogQueryKeys(client, headers);
257
257
  const basePost = queries.posts.detail(slug);
258
258
  const { data, refetch, error, isFetching } = useSuspenseQuery<
259
259
  SerializedPost | null,
@@ -283,13 +283,13 @@ export function useTags(): {
283
283
  error: Error | null;
284
284
  refetch: () => void;
285
285
  } {
286
- const { apiBaseURL, apiBasePath } =
286
+ const { apiBaseURL, apiBasePath, headers } =
287
287
  usePluginOverrides<BlogPluginOverrides>("blog");
288
288
  const client = createApiClient<BlogApiRouter>({
289
289
  baseURL: apiBaseURL,
290
290
  basePath: apiBasePath,
291
291
  });
292
- const queries = createBlogQueryKeys(client);
292
+ const queries = createBlogQueryKeys(client, headers);
293
293
  const baseTags = queries.tags.list();
294
294
  const { data, isLoading, error, refetch } = useQuery<
295
295
  SerializedTag[] | null,
@@ -315,13 +315,13 @@ export function useSuspenseTags(): {
315
315
  tags: SerializedTag[];
316
316
  refetch: () => Promise<unknown>;
317
317
  } {
318
- const { apiBaseURL, apiBasePath } =
318
+ const { apiBaseURL, apiBasePath, headers } =
319
319
  usePluginOverrides<BlogPluginOverrides>("blog");
320
320
  const client = createApiClient<BlogApiRouter>({
321
321
  baseURL: apiBaseURL,
322
322
  basePath: apiBasePath,
323
323
  });
324
- const queries = createBlogQueryKeys(client);
324
+ const queries = createBlogQueryKeys(client, headers);
325
325
  const baseTags = queries.tags.list();
326
326
  const { data, refetch, error, isFetching } = useSuspenseQuery<
327
327
  SerializedTag[] | null,
@@ -555,13 +555,13 @@ export function useNextPreviousPosts(
555
555
  ref: (node: Element | null) => void;
556
556
  inView: boolean;
557
557
  } {
558
- const { apiBaseURL, apiBasePath } =
558
+ const { apiBaseURL, apiBasePath, headers } =
559
559
  usePluginOverrides<BlogPluginOverrides>("blog");
560
560
  const client = createApiClient<BlogApiRouter>({
561
561
  baseURL: apiBaseURL,
562
562
  basePath: apiBasePath,
563
563
  });
564
- const queries = createBlogQueryKeys(client);
564
+ const queries = createBlogQueryKeys(client, headers);
565
565
 
566
566
  const { ref, inView } = useInView({
567
567
  // start a little early so the data is ready as it scrolls in
@@ -619,13 +619,13 @@ export function useRecentPosts(
619
619
  ref: (node: Element | null) => void;
620
620
  inView: boolean;
621
621
  } {
622
- const { apiBaseURL, apiBasePath } =
622
+ const { apiBaseURL, apiBasePath, headers } =
623
623
  usePluginOverrides<BlogPluginOverrides>("blog");
624
624
  const client = createApiClient<BlogApiRouter>({
625
625
  baseURL: apiBaseURL,
626
626
  basePath: apiBasePath,
627
627
  });
628
- const queries = createBlogQueryKeys(client);
628
+ const queries = createBlogQueryKeys(client, headers);
629
629
 
630
630
  const { ref, inView } = useInView({
631
631
  // start a little early so the data is ready as it scrolls in
@@ -57,6 +57,10 @@ export interface BlogPluginOverrides {
57
57
  * Whether to show the attribution
58
58
  */
59
59
  showAttribution?: boolean;
60
+ /**
61
+ * Optional headers to pass with API requests (e.g., for SSR auth)
62
+ */
63
+ headers?: HeadersInit;
60
64
 
61
65
  // Lifecycle Hooks (optional)
62
66
  /**
@@ -58,6 +58,9 @@ export interface BlogClientConfig {
58
58
 
59
59
  // Optional hooks
60
60
  hooks?: BlogClientHooks;
61
+
62
+ // Optional headers for SSR (e.g., forwarding cookies)
63
+ headers?: HeadersInit;
61
64
  }
62
65
 
63
66
  /**
@@ -65,7 +68,7 @@ export interface BlogClientConfig {
65
68
  * All hooks are optional and allow consumers to customize behavior
66
69
  */
67
70
  export interface BlogClientHooks {
68
- // Loader Hooks - called during data loading (SSR or CSR)
71
+ // Loader Hooks - called during data loading (SSR)
69
72
  beforeLoadPosts?: (
70
73
  filter: { published: boolean },
71
74
  context: LoaderContext,
@@ -74,7 +77,7 @@ export interface BlogClientHooks {
74
77
  posts: Post[] | null,
75
78
  filter: { published: boolean },
76
79
  context: LoaderContext,
77
- ) => Promise<void> | void;
80
+ ) => Promise<boolean> | boolean;
78
81
  beforeLoadPost?: (
79
82
  slug: string,
80
83
  context: LoaderContext,
@@ -83,7 +86,9 @@ export interface BlogClientHooks {
83
86
  post: Post | null,
84
87
  slug: string,
85
88
  context: LoaderContext,
86
- ) => Promise<void> | void;
89
+ ) => Promise<boolean> | boolean;
90
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
91
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
87
92
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
88
93
  }
89
94
 
@@ -91,7 +96,7 @@ export interface BlogClientHooks {
91
96
  function createPostsLoader(published: boolean, config: BlogClientConfig) {
92
97
  return async () => {
93
98
  if (typeof window === "undefined") {
94
- const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
99
+ const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
95
100
 
96
101
  const context: LoaderContext = {
97
102
  path: published ? "/blog" : "/blog/drafts",
@@ -116,7 +121,7 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
116
121
  });
117
122
 
118
123
  // 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
119
- const queries = createBlogQueryKeys(client);
124
+ const queries = createBlogQueryKeys(client, headers);
120
125
  const listQuery = queries.posts.list({
121
126
  query: undefined,
122
127
  limit,
@@ -140,7 +145,14 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
140
145
  if (hooks?.afterLoadPosts) {
141
146
  const posts =
142
147
  queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
143
- await hooks.afterLoadPosts(posts, { published }, context);
148
+ const canContinue = await hooks.afterLoadPosts(
149
+ posts,
150
+ { published },
151
+ context,
152
+ );
153
+ if (canContinue === false) {
154
+ throw new Error("Load prevented by afterLoadPosts hook");
155
+ }
144
156
  }
145
157
 
146
158
  // Check if there was an error after afterLoadPosts hook
@@ -170,7 +182,7 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
170
182
  function createPostLoader(slug: string, config: BlogClientConfig) {
171
183
  return async () => {
172
184
  if (typeof window === "undefined") {
173
- const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
185
+ const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
174
186
 
175
187
  const context: LoaderContext = {
176
188
  path: `/blog/${slug}`,
@@ -193,7 +205,7 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
193
205
  baseURL: apiBaseURL,
194
206
  basePath: apiBasePath,
195
207
  });
196
- const queries = createBlogQueryKeys(client);
208
+ const queries = createBlogQueryKeys(client, headers);
197
209
  const postQuery = queries.posts.detail(slug);
198
210
  await queryClient.prefetchQuery(postQuery);
199
211
 
@@ -205,7 +217,10 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
205
217
  if (hooks?.afterLoadPost) {
206
218
  const post =
207
219
  queryClient.getQueryData<Post>(postQuery.queryKey) || null;
208
- await hooks.afterLoadPost(post, slug, context);
220
+ const canContinue = await hooks.afterLoadPost(post, slug, context);
221
+ if (canContinue === false) {
222
+ throw new Error("Load prevented by afterLoadPost hook");
223
+ }
209
224
  }
210
225
 
211
226
  // Check if there was an error after afterLoadPost hook
@@ -232,10 +247,50 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
232
247
  };
233
248
  }
234
249
 
250
+ function createNewPostLoader(config: BlogClientConfig) {
251
+ return async () => {
252
+ if (typeof window === "undefined") {
253
+ const { apiBasePath, apiBaseURL, hooks } = config;
254
+
255
+ const context: LoaderContext = {
256
+ path: "/blog/new",
257
+ isSSR: true,
258
+ apiBaseURL,
259
+ apiBasePath,
260
+ };
261
+
262
+ try {
263
+ // Before hook
264
+ if (hooks?.beforeLoadNewPost) {
265
+ const canLoad = await hooks.beforeLoadNewPost(context);
266
+ if (!canLoad) {
267
+ throw new Error("Load prevented by beforeLoadNewPost hook");
268
+ }
269
+ }
270
+
271
+ // After hook
272
+ if (hooks?.afterLoadNewPost) {
273
+ const canContinue = await hooks.afterLoadNewPost(context);
274
+ if (canContinue === false) {
275
+ throw new Error("Load prevented by afterLoadNewPost hook");
276
+ }
277
+ }
278
+ } catch (error) {
279
+ // Error hook - log the error but don't throw during SSR
280
+ // Let Error Boundaries handle errors when components render
281
+ if (hooks?.onLoadError) {
282
+ await hooks.onLoadError(error as Error, context);
283
+ }
284
+ // Don't re-throw - let Error Boundary catch it during render
285
+ }
286
+ }
287
+ };
288
+ }
289
+
235
290
  function createTagLoader(tagSlug: string, config: BlogClientConfig) {
236
291
  return async () => {
237
292
  if (typeof window === "undefined") {
238
- const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
293
+ const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
239
294
 
240
295
  const context: LoaderContext = {
241
296
  path: `/blog/tag/${tagSlug}`,
@@ -252,7 +307,7 @@ function createTagLoader(tagSlug: string, config: BlogClientConfig) {
252
307
  basePath: apiBasePath,
253
308
  });
254
309
 
255
- const queries = createBlogQueryKeys(client);
310
+ const queries = createBlogQueryKeys(client, headers);
256
311
  const listQuery = queries.posts.list({
257
312
  query: undefined,
258
313
  limit,
@@ -581,6 +636,7 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
581
636
  newPost: createRoute("/blog/new", () => {
582
637
  return {
583
638
  PageComponent: NewPostPageComponent,
639
+ loader: createNewPostLoader(config),
584
640
  meta: createNewPostMeta(config),
585
641
  };
586
642
  }),