@btst/stack 1.1.7 → 1.1.9

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 (30) 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/plugin.cjs +51 -5
  8. package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +51 -5
  9. package/dist/plugins/blog/api/index.d.cts +1 -1
  10. package/dist/plugins/blog/api/index.d.mts +1 -1
  11. package/dist/plugins/blog/api/index.d.ts +1 -1
  12. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  13. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  14. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  15. package/dist/plugins/blog/client/index.d.cts +8 -5
  16. package/dist/plugins/blog/client/index.d.mts +8 -5
  17. package/dist/plugins/blog/client/index.d.ts +8 -5
  18. package/dist/plugins/blog/query-keys.d.cts +2 -2
  19. package/dist/plugins/blog/query-keys.d.mts +2 -2
  20. package/dist/plugins/blog/query-keys.d.ts +2 -2
  21. package/dist/plugins/blog/style.css +2 -5
  22. package/package.json +3 -3
  23. package/src/plugins/blog/api/plugin.ts +56 -36
  24. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +4 -4
  25. package/src/plugins/blog/client/components/shared/default-error.tsx +4 -1
  26. package/src/plugins/blog/client/plugin.tsx +64 -6
  27. package/src/plugins/blog/style.css +2 -5
  28. package/dist/shared/{stack.Cr2JoQdo.d.cts → stack.CoPoHVfV.d.cts} +1 -1
  29. package/dist/shared/{stack.Cr2JoQdo.d.mts → stack.CoPoHVfV.d.mts} +1 -1
  30. package/dist/shared/{stack.Cr2JoQdo.d.ts → stack.CoPoHVfV.d.ts} +1 -1
@@ -482,15 +482,12 @@ const blogBackendPlugin = (hooks) => api.defineBackendPlugin({
482
482
  }
483
483
  }
484
484
  const date = query.date;
485
- const previousPost = await adapter.findMany({
485
+ const targetTime = new Date(date).getTime();
486
+ const WINDOW_SIZE = 100;
487
+ const allPosts = await adapter.findMany({
486
488
  model: "post",
487
- limit: 1,
489
+ limit: WINDOW_SIZE,
488
490
  where: [
489
- {
490
- field: "createdAt",
491
- value: date,
492
- operator: "lt"
493
- },
494
491
  {
495
492
  field: "published",
496
493
  value: true,
@@ -502,29 +499,27 @@ const blogBackendPlugin = (hooks) => api.defineBackendPlugin({
502
499
  direction: "desc"
503
500
  }
504
501
  });
505
- const nextPost = await adapter.findMany({
506
- model: "post",
507
- limit: 1,
508
- where: [
509
- {
510
- field: "createdAt",
511
- value: date,
512
- operator: "gt"
513
- },
514
- {
515
- field: "published",
516
- value: true,
517
- operator: "eq"
518
- }
519
- ],
520
- sortBy: {
521
- field: "createdAt",
522
- direction: "asc"
523
- }
502
+ const sortedPosts = allPosts.sort((a, b) => {
503
+ const timeA = new Date(a.createdAt).getTime();
504
+ const timeB = new Date(b.createdAt).getTime();
505
+ return timeB - timeA;
524
506
  });
507
+ let previousPost = null;
508
+ let nextPost = null;
509
+ for (let i = 0; i < sortedPosts.length; i++) {
510
+ const post = sortedPosts[i];
511
+ if (!post) continue;
512
+ const postTime = new Date(post.createdAt).getTime();
513
+ if (postTime > targetTime) {
514
+ nextPost = post;
515
+ } else if (postTime < targetTime) {
516
+ previousPost = post;
517
+ break;
518
+ }
519
+ }
525
520
  const postIds = [
526
- ...previousPost?.[0] ? [previousPost[0].id] : [],
527
- ...nextPost?.[0] ? [nextPost[0].id] : []
521
+ ...previousPost ? [previousPost.id] : [],
522
+ ...nextPost ? [nextPost.id] : []
528
523
  ];
529
524
  const postTagsMap = await loadTagsForPosts(
530
525
  postIds,
@@ -532,13 +527,13 @@ const blogBackendPlugin = (hooks) => api.defineBackendPlugin({
532
527
  postTagCache
533
528
  );
534
529
  return {
535
- previous: previousPost?.[0] ? {
536
- ...previousPost[0],
537
- tags: postTagsMap.get(previousPost[0].id) || []
530
+ previous: previousPost ? {
531
+ ...previousPost,
532
+ tags: postTagsMap.get(previousPost.id) || []
538
533
  } : null,
539
- next: nextPost?.[0] ? {
540
- ...nextPost[0],
541
- tags: postTagsMap.get(nextPost[0].id) || []
534
+ next: nextPost ? {
535
+ ...nextPost,
536
+ tags: postTagsMap.get(nextPost.id) || []
542
537
  } : null
543
538
  };
544
539
  } catch (error) {
@@ -480,15 +480,12 @@ const blogBackendPlugin = (hooks) => defineBackendPlugin({
480
480
  }
481
481
  }
482
482
  const date = query.date;
483
- const previousPost = await adapter.findMany({
483
+ const targetTime = new Date(date).getTime();
484
+ const WINDOW_SIZE = 100;
485
+ const allPosts = await adapter.findMany({
484
486
  model: "post",
485
- limit: 1,
487
+ limit: WINDOW_SIZE,
486
488
  where: [
487
- {
488
- field: "createdAt",
489
- value: date,
490
- operator: "lt"
491
- },
492
489
  {
493
490
  field: "published",
494
491
  value: true,
@@ -500,29 +497,27 @@ const blogBackendPlugin = (hooks) => defineBackendPlugin({
500
497
  direction: "desc"
501
498
  }
502
499
  });
503
- const nextPost = await adapter.findMany({
504
- model: "post",
505
- limit: 1,
506
- where: [
507
- {
508
- field: "createdAt",
509
- value: date,
510
- operator: "gt"
511
- },
512
- {
513
- field: "published",
514
- value: true,
515
- operator: "eq"
516
- }
517
- ],
518
- sortBy: {
519
- field: "createdAt",
520
- direction: "asc"
521
- }
500
+ const sortedPosts = allPosts.sort((a, b) => {
501
+ const timeA = new Date(a.createdAt).getTime();
502
+ const timeB = new Date(b.createdAt).getTime();
503
+ return timeB - timeA;
522
504
  });
505
+ let previousPost = null;
506
+ let nextPost = null;
507
+ for (let i = 0; i < sortedPosts.length; i++) {
508
+ const post = sortedPosts[i];
509
+ if (!post) continue;
510
+ const postTime = new Date(post.createdAt).getTime();
511
+ if (postTime > targetTime) {
512
+ nextPost = post;
513
+ } else if (postTime < targetTime) {
514
+ previousPost = post;
515
+ break;
516
+ }
517
+ }
523
518
  const postIds = [
524
- ...previousPost?.[0] ? [previousPost[0].id] : [],
525
- ...nextPost?.[0] ? [nextPost[0].id] : []
519
+ ...previousPost ? [previousPost.id] : [],
520
+ ...nextPost ? [nextPost.id] : []
526
521
  ];
527
522
  const postTagsMap = await loadTagsForPosts(
528
523
  postIds,
@@ -530,13 +525,13 @@ const blogBackendPlugin = (hooks) => defineBackendPlugin({
530
525
  postTagCache
531
526
  );
532
527
  return {
533
- previous: previousPost?.[0] ? {
534
- ...previousPost[0],
535
- tags: postTagsMap.get(previousPost[0].id) || []
528
+ previous: previousPost ? {
529
+ ...previousPost,
530
+ tags: postTagsMap.get(previousPost.id) || []
536
531
  } : null,
537
- next: nextPost?.[0] ? {
538
- ...nextPost[0],
539
- tags: postTagsMap.get(nextPost[0].id) || []
532
+ next: nextPost ? {
533
+ ...nextPost,
534
+ tags: postTagsMap.get(nextPost.id) || []
540
535
  } : null
541
536
  };
542
537
  } catch (error) {
@@ -63,7 +63,7 @@ function PostPage({ slug }) {
63
63
  childrenTop: /* @__PURE__ */ jsxRuntime.jsx(PostHeaderTop, { post })
64
64
  }
65
65
  ),
66
- post.image && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2 mt-6 aspect-video w-full relative", children: /* @__PURE__ */ jsxRuntime.jsx(
66
+ post.image && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2 my-6 aspect-video w-full relative", children: /* @__PURE__ */ jsxRuntime.jsx(
67
67
  Image,
68
68
  {
69
69
  src: post.image,
@@ -92,9 +92,9 @@ function PostHeaderTop({ post }) {
92
92
  Link: defaults.DefaultLink
93
93
  });
94
94
  const basePath = context.useBasePath();
95
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-row items-center gap-2 flex-wrap mt-8", children: [
95
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-row items-center justify-center gap-2 flex-wrap mt-8", children: [
96
96
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-light text-muted-foreground text-sm", children: dateFns.formatDate(post.createdAt, "MMMM d, yyyy") }),
97
- post.tags && post.tags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: post.tags.map((tag) => /* @__PURE__ */ jsxRuntime.jsx(Link, { href: `${basePath}/blog/tag/${tag.slug}`, children: /* @__PURE__ */ jsxRuntime.jsx(badge.Badge, { variant: "secondary", className: "text-xs", children: tag.name }) }, tag.id)) })
97
+ post.tags && post.tags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: post.tags.map((tag) => /* @__PURE__ */ jsxRuntime.jsx(Link, { href: `${basePath}/blog/tag/${tag.slug}`, children: /* @__PURE__ */ jsxRuntime.jsx(badge.Badge, { variant: "secondary", className: "text-xs", children: tag.name }) }, tag.id)) })
98
98
  ] });
99
99
  }
100
100
 
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { usePluginOverrides, useBasePath } from '@btst/stack/context';
4
4
  import { formatDate } from 'date-fns';
5
5
  import { useSuspensePost, useNextPreviousPosts, useRecentPosts } from '../../hooks/blog-hooks.mjs';
@@ -61,7 +61,7 @@ function PostPage({ slug }) {
61
61
  childrenTop: /* @__PURE__ */ jsx(PostHeaderTop, { post })
62
62
  }
63
63
  ),
64
- post.image && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 mt-6 aspect-video w-full relative", children: /* @__PURE__ */ jsx(
64
+ post.image && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 my-6 aspect-video w-full relative", children: /* @__PURE__ */ jsx(
65
65
  Image,
66
66
  {
67
67
  src: post.image,
@@ -90,9 +90,9 @@ function PostHeaderTop({ post }) {
90
90
  Link: DefaultLink
91
91
  });
92
92
  const basePath = useBasePath();
93
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center gap-2 flex-wrap mt-8", children: [
93
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-center gap-2 flex-wrap mt-8", children: [
94
94
  /* @__PURE__ */ jsx("span", { className: "font-light text-muted-foreground text-sm", children: formatDate(post.createdAt, "MMMM d, yyyy") }),
95
- post.tags && post.tags.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: post.tags.map((tag) => /* @__PURE__ */ jsx(Link, { href: `${basePath}/blog/tag/${tag.slug}`, children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: tag.name }) }, tag.id)) })
95
+ post.tags && post.tags.length > 0 && /* @__PURE__ */ jsx(Fragment, { children: post.tags.map((tag) => /* @__PURE__ */ jsx(Link, { href: `${basePath}/blog/tag/${tag.slug}`, children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: tag.name }) }, tag.id)) })
96
96
  ] });
97
97
  }
98
98
 
@@ -11,7 +11,7 @@ function DefaultError({ error }) {
11
11
  localization: index.BLOG_LOCALIZATION
12
12
  });
13
13
  const title = localization.BLOG_GENERIC_ERROR_TITLE;
14
- const message = error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
14
+ const message = process.env.NODE_ENV === "production" ? localization.BLOG_GENERIC_ERROR_MESSAGE : error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
15
15
  return /* @__PURE__ */ jsxRuntime.jsx(errorPlaceholder.ErrorPlaceholder, { title, message });
16
16
  }
17
17
 
@@ -9,7 +9,7 @@ function DefaultError({ error }) {
9
9
  localization: BLOG_LOCALIZATION
10
10
  });
11
11
  const title = localization.BLOG_GENERIC_ERROR_TITLE;
12
- const message = error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
12
+ const message = process.env.NODE_ENV === "production" ? localization.BLOG_GENERIC_ERROR_MESSAGE : error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
13
13
  return /* @__PURE__ */ jsx(ErrorPlaceholder, { title, message });
14
14
  }
15
15
 
@@ -18,7 +18,8 @@ function createPostsLoader(published, config) {
18
18
  path: published ? "/blog" : "/blog/drafts",
19
19
  isSSR: true,
20
20
  apiBaseURL,
21
- apiBasePath
21
+ apiBasePath,
22
+ headers
22
23
  };
23
24
  try {
24
25
  if (hooks?.beforeLoadPosts) {
@@ -46,7 +47,14 @@ function createPostsLoader(published, config) {
46
47
  await queryClient.prefetchQuery(tagsQuery);
47
48
  if (hooks?.afterLoadPosts) {
48
49
  const posts = queryClient.getQueryData(listQuery.queryKey) || null;
49
- await hooks.afterLoadPosts(posts, { published }, context);
50
+ const canContinue = await hooks.afterLoadPosts(
51
+ posts,
52
+ { published },
53
+ context
54
+ );
55
+ if (canContinue === false) {
56
+ throw new Error("Load prevented by afterLoadPosts hook");
57
+ }
50
58
  }
51
59
  const queryState = queryClient.getQueryState(listQuery.queryKey);
52
60
  if (queryState?.error) {
@@ -72,7 +80,8 @@ function createPostLoader(slug, config) {
72
80
  params: { slug },
73
81
  isSSR: true,
74
82
  apiBaseURL,
75
- apiBasePath
83
+ apiBasePath,
84
+ headers
76
85
  };
77
86
  try {
78
87
  if (hooks?.beforeLoadPost) {
@@ -90,7 +99,10 @@ function createPostLoader(slug, config) {
90
99
  await queryClient.prefetchQuery(postQuery);
91
100
  if (hooks?.afterLoadPost) {
92
101
  const post = queryClient.getQueryData(postQuery.queryKey) || null;
93
- await hooks.afterLoadPost(post, slug, context);
102
+ const canContinue = await hooks.afterLoadPost(post, slug, context);
103
+ if (canContinue === false) {
104
+ throw new Error("Load prevented by afterLoadPost hook");
105
+ }
94
106
  }
95
107
  const queryState = queryClient.getQueryState(postQuery.queryKey);
96
108
  if (queryState?.error) {
@@ -107,6 +119,38 @@ function createPostLoader(slug, config) {
107
119
  }
108
120
  };
109
121
  }
122
+ function createNewPostLoader(config) {
123
+ return async () => {
124
+ if (typeof window === "undefined") {
125
+ const { apiBasePath, apiBaseURL, hooks, headers } = config;
126
+ const context = {
127
+ path: "/blog/new",
128
+ isSSR: true,
129
+ apiBaseURL,
130
+ apiBasePath,
131
+ headers
132
+ };
133
+ try {
134
+ if (hooks?.beforeLoadNewPost) {
135
+ const canLoad = await hooks.beforeLoadNewPost(context);
136
+ if (!canLoad) {
137
+ throw new Error("Load prevented by beforeLoadNewPost hook");
138
+ }
139
+ }
140
+ if (hooks?.afterLoadNewPost) {
141
+ const canContinue = await hooks.afterLoadNewPost(context);
142
+ if (canContinue === false) {
143
+ throw new Error("Load prevented by afterLoadNewPost hook");
144
+ }
145
+ }
146
+ } catch (error) {
147
+ if (hooks?.onLoadError) {
148
+ await hooks.onLoadError(error, context);
149
+ }
150
+ }
151
+ }
152
+ };
153
+ }
110
154
  function createTagLoader(tagSlug, config) {
111
155
  return async () => {
112
156
  if (typeof window === "undefined") {
@@ -116,7 +160,8 @@ function createTagLoader(tagSlug, config) {
116
160
  params: { tagSlug },
117
161
  isSSR: true,
118
162
  apiBaseURL,
119
- apiBasePath
163
+ apiBasePath,
164
+ headers
120
165
  };
121
166
  try {
122
167
  const limit = 10;
@@ -382,6 +427,7 @@ const blogClientPlugin = (config) => client.defineClientPlugin({
382
427
  newPost: yar.createRoute("/blog/new", () => {
383
428
  return {
384
429
  PageComponent: newPostPage.NewPostPageComponent,
430
+ loader: createNewPostLoader(config),
385
431
  meta: createNewPostMeta(config)
386
432
  };
387
433
  }),
@@ -16,7 +16,8 @@ function createPostsLoader(published, config) {
16
16
  path: published ? "/blog" : "/blog/drafts",
17
17
  isSSR: true,
18
18
  apiBaseURL,
19
- apiBasePath
19
+ apiBasePath,
20
+ headers
20
21
  };
21
22
  try {
22
23
  if (hooks?.beforeLoadPosts) {
@@ -44,7 +45,14 @@ function createPostsLoader(published, config) {
44
45
  await queryClient.prefetchQuery(tagsQuery);
45
46
  if (hooks?.afterLoadPosts) {
46
47
  const posts = queryClient.getQueryData(listQuery.queryKey) || null;
47
- await hooks.afterLoadPosts(posts, { published }, context);
48
+ const canContinue = await hooks.afterLoadPosts(
49
+ posts,
50
+ { published },
51
+ context
52
+ );
53
+ if (canContinue === false) {
54
+ throw new Error("Load prevented by afterLoadPosts hook");
55
+ }
48
56
  }
49
57
  const queryState = queryClient.getQueryState(listQuery.queryKey);
50
58
  if (queryState?.error) {
@@ -70,7 +78,8 @@ function createPostLoader(slug, config) {
70
78
  params: { slug },
71
79
  isSSR: true,
72
80
  apiBaseURL,
73
- apiBasePath
81
+ apiBasePath,
82
+ headers
74
83
  };
75
84
  try {
76
85
  if (hooks?.beforeLoadPost) {
@@ -88,7 +97,10 @@ function createPostLoader(slug, config) {
88
97
  await queryClient.prefetchQuery(postQuery);
89
98
  if (hooks?.afterLoadPost) {
90
99
  const post = queryClient.getQueryData(postQuery.queryKey) || null;
91
- await hooks.afterLoadPost(post, slug, context);
100
+ const canContinue = await hooks.afterLoadPost(post, slug, context);
101
+ if (canContinue === false) {
102
+ throw new Error("Load prevented by afterLoadPost hook");
103
+ }
92
104
  }
93
105
  const queryState = queryClient.getQueryState(postQuery.queryKey);
94
106
  if (queryState?.error) {
@@ -105,6 +117,38 @@ function createPostLoader(slug, config) {
105
117
  }
106
118
  };
107
119
  }
120
+ function createNewPostLoader(config) {
121
+ return async () => {
122
+ if (typeof window === "undefined") {
123
+ const { apiBasePath, apiBaseURL, hooks, headers } = config;
124
+ const context = {
125
+ path: "/blog/new",
126
+ isSSR: true,
127
+ apiBaseURL,
128
+ apiBasePath,
129
+ headers
130
+ };
131
+ try {
132
+ if (hooks?.beforeLoadNewPost) {
133
+ const canLoad = await hooks.beforeLoadNewPost(context);
134
+ if (!canLoad) {
135
+ throw new Error("Load prevented by beforeLoadNewPost hook");
136
+ }
137
+ }
138
+ if (hooks?.afterLoadNewPost) {
139
+ const canContinue = await hooks.afterLoadNewPost(context);
140
+ if (canContinue === false) {
141
+ throw new Error("Load prevented by afterLoadNewPost hook");
142
+ }
143
+ }
144
+ } catch (error) {
145
+ if (hooks?.onLoadError) {
146
+ await hooks.onLoadError(error, context);
147
+ }
148
+ }
149
+ }
150
+ };
151
+ }
108
152
  function createTagLoader(tagSlug, config) {
109
153
  return async () => {
110
154
  if (typeof window === "undefined") {
@@ -114,7 +158,8 @@ function createTagLoader(tagSlug, config) {
114
158
  params: { tagSlug },
115
159
  isSSR: true,
116
160
  apiBaseURL,
117
- apiBasePath
161
+ apiBasePath,
162
+ headers
118
163
  };
119
164
  try {
120
165
  const limit = 10;
@@ -380,6 +425,7 @@ const blogClientPlugin = (config) => defineClientPlugin({
380
425
  newPost: createRoute("/blog/new", () => {
381
426
  return {
382
427
  PageComponent: NewPostPageComponent,
428
+ loader: createNewPostLoader(config),
383
429
  meta: createNewPostMeta(config)
384
430
  };
385
431
  }),
@@ -2,6 +2,6 @@ export { B as BlogApiContext, c as BlogApiRouter, a as BlogBackendHooks, N as Ne
2
2
  import '@btst/stack/plugins/api';
3
3
  import 'better-call';
4
4
  import 'zod';
5
- import '../../../shared/stack.Cr2JoQdo.cjs';
5
+ import '../../../shared/stack.CoPoHVfV.cjs';
6
6
  import '@tanstack/react-query';
7
7
  import '@btst/stack/plugins/client';
@@ -2,6 +2,6 @@ export { B as BlogApiContext, c as BlogApiRouter, a as BlogBackendHooks, N as Ne
2
2
  import '@btst/stack/plugins/api';
3
3
  import 'better-call';
4
4
  import 'zod';
5
- import '../../../shared/stack.Cr2JoQdo.mjs';
5
+ import '../../../shared/stack.CoPoHVfV.mjs';
6
6
  import '@tanstack/react-query';
7
7
  import '@btst/stack/plugins/client';
@@ -2,6 +2,6 @@ export { B as BlogApiContext, c as BlogApiRouter, a as BlogBackendHooks, N as Ne
2
2
  import '@btst/stack/plugins/api';
3
3
  import 'better-call';
4
4
  import 'zod';
5
- import '../../../shared/stack.Cr2JoQdo.js';
5
+ import '../../../shared/stack.CoPoHVfV.js';
6
6
  import '@tanstack/react-query';
7
7
  import '@btst/stack/plugins/client';
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.Cr2JoQdo.cjs';
2
+ import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.CoPoHVfV.cjs';
3
3
  import { z } from 'zod';
4
4
 
5
5
  interface UsePostsOptions {
@@ -81,8 +81,8 @@ declare function useSuspenseTags(): {
81
81
  };
82
82
  /** Create a new post */
83
83
  declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
84
- title: string;
85
84
  published: boolean;
85
+ title: string;
86
86
  content: string;
87
87
  excerpt: string;
88
88
  tags: ({
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.Cr2JoQdo.mjs';
2
+ import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.CoPoHVfV.mjs';
3
3
  import { z } from 'zod';
4
4
 
5
5
  interface UsePostsOptions {
@@ -81,8 +81,8 @@ declare function useSuspenseTags(): {
81
81
  };
82
82
  /** Create a new post */
83
83
  declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
84
- title: string;
85
84
  published: boolean;
85
+ title: string;
86
86
  content: string;
87
87
  excerpt: string;
88
88
  tags: ({
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.Cr2JoQdo.js';
2
+ import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.CoPoHVfV.js';
3
3
  import { z } from 'zod';
4
4
 
5
5
  interface UsePostsOptions {
@@ -81,8 +81,8 @@ declare function useSuspenseTags(): {
81
81
  };
82
82
  /** Create a new post */
83
83
  declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
84
- title: string;
85
84
  published: boolean;
85
+ title: string;
86
86
  content: string;
87
87
  excerpt: string;
88
88
  tags: ({
@@ -3,7 +3,7 @@ import * as react from 'react';
3
3
  import { ComponentType } from 'react';
4
4
  import * as _btst_yar from '@btst/yar';
5
5
  import { QueryClient } from '@tanstack/react-query';
6
- import { P as Post, S as SerializedPost } from '../../../shared/stack.Cr2JoQdo.cjs';
6
+ import { P as Post, S as SerializedPost } from '../../../shared/stack.CoPoHVfV.cjs';
7
7
  export { UsePostsOptions, UsePostsResult } from './hooks/index.cjs';
8
8
  import 'zod';
9
9
 
@@ -25,6 +25,7 @@ interface LoaderContext {
25
25
  isSSR: boolean;
26
26
  apiBaseURL: string;
27
27
  apiBasePath: string;
28
+ headers?: Headers;
28
29
  [key: string]: any;
29
30
  }
30
31
  /**
@@ -45,7 +46,7 @@ interface BlogClientConfig {
45
46
  defaultImage?: string;
46
47
  };
47
48
  hooks?: BlogClientHooks;
48
- headers?: HeadersInit;
49
+ headers?: Headers;
49
50
  }
50
51
  /**
51
52
  * Hooks for blog client plugin
@@ -57,9 +58,11 @@ interface BlogClientHooks {
57
58
  }, context: LoaderContext) => Promise<boolean> | boolean;
58
59
  afterLoadPosts?: (posts: Post[] | null, filter: {
59
60
  published: boolean;
60
- }, context: LoaderContext) => Promise<void> | void;
61
+ }, context: LoaderContext) => Promise<boolean> | boolean;
61
62
  beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
62
- afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<void> | void;
63
+ afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
64
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
65
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
63
66
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
64
67
  }
65
68
  /**
@@ -130,7 +133,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
130
133
  PageComponent?: react.ComponentType<unknown> | undefined;
131
134
  LoadingComponent?: react.ComponentType<unknown> | undefined;
132
135
  ErrorComponent?: react.ComponentType<unknown> | undefined;
133
- loader?: (() => any) | undefined;
136
+ loader?: (() => Promise<void>) | undefined;
134
137
  meta?: (() => ({
135
138
  title: string;
136
139
  name?: undefined;
@@ -3,7 +3,7 @@ import * as react from 'react';
3
3
  import { ComponentType } from 'react';
4
4
  import * as _btst_yar from '@btst/yar';
5
5
  import { QueryClient } from '@tanstack/react-query';
6
- import { P as Post, S as SerializedPost } from '../../../shared/stack.Cr2JoQdo.mjs';
6
+ import { P as Post, S as SerializedPost } from '../../../shared/stack.CoPoHVfV.mjs';
7
7
  export { UsePostsOptions, UsePostsResult } from './hooks/index.mjs';
8
8
  import 'zod';
9
9
 
@@ -25,6 +25,7 @@ interface LoaderContext {
25
25
  isSSR: boolean;
26
26
  apiBaseURL: string;
27
27
  apiBasePath: string;
28
+ headers?: Headers;
28
29
  [key: string]: any;
29
30
  }
30
31
  /**
@@ -45,7 +46,7 @@ interface BlogClientConfig {
45
46
  defaultImage?: string;
46
47
  };
47
48
  hooks?: BlogClientHooks;
48
- headers?: HeadersInit;
49
+ headers?: Headers;
49
50
  }
50
51
  /**
51
52
  * Hooks for blog client plugin
@@ -57,9 +58,11 @@ interface BlogClientHooks {
57
58
  }, context: LoaderContext) => Promise<boolean> | boolean;
58
59
  afterLoadPosts?: (posts: Post[] | null, filter: {
59
60
  published: boolean;
60
- }, context: LoaderContext) => Promise<void> | void;
61
+ }, context: LoaderContext) => Promise<boolean> | boolean;
61
62
  beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
62
- afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<void> | void;
63
+ afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
64
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
65
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
63
66
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
64
67
  }
65
68
  /**
@@ -130,7 +133,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
130
133
  PageComponent?: react.ComponentType<unknown> | undefined;
131
134
  LoadingComponent?: react.ComponentType<unknown> | undefined;
132
135
  ErrorComponent?: react.ComponentType<unknown> | undefined;
133
- loader?: (() => any) | undefined;
136
+ loader?: (() => Promise<void>) | undefined;
134
137
  meta?: (() => ({
135
138
  title: string;
136
139
  name?: undefined;
@@ -3,7 +3,7 @@ import * as react from 'react';
3
3
  import { ComponentType } from 'react';
4
4
  import * as _btst_yar from '@btst/yar';
5
5
  import { QueryClient } from '@tanstack/react-query';
6
- import { P as Post, S as SerializedPost } from '../../../shared/stack.Cr2JoQdo.js';
6
+ import { P as Post, S as SerializedPost } from '../../../shared/stack.CoPoHVfV.js';
7
7
  export { UsePostsOptions, UsePostsResult } from './hooks/index.js';
8
8
  import 'zod';
9
9
 
@@ -25,6 +25,7 @@ interface LoaderContext {
25
25
  isSSR: boolean;
26
26
  apiBaseURL: string;
27
27
  apiBasePath: string;
28
+ headers?: Headers;
28
29
  [key: string]: any;
29
30
  }
30
31
  /**
@@ -45,7 +46,7 @@ interface BlogClientConfig {
45
46
  defaultImage?: string;
46
47
  };
47
48
  hooks?: BlogClientHooks;
48
- headers?: HeadersInit;
49
+ headers?: Headers;
49
50
  }
50
51
  /**
51
52
  * Hooks for blog client plugin
@@ -57,9 +58,11 @@ interface BlogClientHooks {
57
58
  }, context: LoaderContext) => Promise<boolean> | boolean;
58
59
  afterLoadPosts?: (posts: Post[] | null, filter: {
59
60
  published: boolean;
60
- }, context: LoaderContext) => Promise<void> | void;
61
+ }, context: LoaderContext) => Promise<boolean> | boolean;
61
62
  beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
62
- afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<void> | void;
63
+ afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
64
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
65
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
63
66
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
64
67
  }
65
68
  /**
@@ -130,7 +133,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
130
133
  PageComponent?: react.ComponentType<unknown> | undefined;
131
134
  LoadingComponent?: react.ComponentType<unknown> | undefined;
132
135
  ErrorComponent?: react.ComponentType<unknown> | undefined;
133
- loader?: (() => any) | undefined;
136
+ loader?: (() => Promise<void>) | undefined;
134
137
  meta?: (() => ({
135
138
  title: string;
136
139
  name?: undefined;
@@ -1,7 +1,7 @@
1
1
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
2
2
  import * as better_call from 'better-call';
3
3
  import { z } from 'zod';
4
- import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.Cr2JoQdo.cjs';
4
+ import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.CoPoHVfV.cjs';
5
5
  import * as _tanstack_react_query from '@tanstack/react-query';
6
6
  import { createApiClient } from '@btst/stack/plugins/client';
7
7
 
@@ -173,12 +173,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
173
173
  options: {
174
174
  method: "POST";
175
175
  body: z.ZodObject<{
176
- title: z.ZodString;
177
176
  slug: z.ZodOptional<z.ZodString>;
178
177
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
179
178
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
180
179
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
181
180
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
181
+ title: z.ZodString;
182
182
  content: z.ZodString;
183
183
  excerpt: z.ZodString;
184
184
  image: z.ZodOptional<z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
2
2
  import * as better_call from 'better-call';
3
3
  import { z } from 'zod';
4
- import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.Cr2JoQdo.mjs';
4
+ import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.CoPoHVfV.mjs';
5
5
  import * as _tanstack_react_query from '@tanstack/react-query';
6
6
  import { createApiClient } from '@btst/stack/plugins/client';
7
7
 
@@ -173,12 +173,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
173
173
  options: {
174
174
  method: "POST";
175
175
  body: z.ZodObject<{
176
- title: z.ZodString;
177
176
  slug: z.ZodOptional<z.ZodString>;
178
177
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
179
178
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
180
179
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
181
180
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
181
+ title: z.ZodString;
182
182
  content: z.ZodString;
183
183
  excerpt: z.ZodString;
184
184
  image: z.ZodOptional<z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
2
2
  import * as better_call from 'better-call';
3
3
  import { z } from 'zod';
4
- import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.Cr2JoQdo.js';
4
+ import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.CoPoHVfV.js';
5
5
  import * as _tanstack_react_query from '@tanstack/react-query';
6
6
  import { createApiClient } from '@btst/stack/plugins/client';
7
7
 
@@ -173,12 +173,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
173
173
  options: {
174
174
  method: "POST";
175
175
  body: z.ZodObject<{
176
- title: z.ZodString;
177
176
  slug: z.ZodOptional<z.ZodString>;
178
177
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
179
178
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
180
179
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
181
180
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
181
+ title: z.ZodString;
182
182
  content: z.ZodString;
183
183
  excerpt: z.ZodString;
184
184
  image: z.ZodOptional<z.ZodString>;
@@ -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.7",
3
+ "version": "1.1.9",
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
  }
@@ -32,6 +32,7 @@ export interface LoaderContext {
32
32
  isSSR: boolean;
33
33
  apiBaseURL: string;
34
34
  apiBasePath: string;
35
+ headers?: Headers;
35
36
  [key: string]: any;
36
37
  }
37
38
 
@@ -60,7 +61,7 @@ export interface BlogClientConfig {
60
61
  hooks?: BlogClientHooks;
61
62
 
62
63
  // Optional headers for SSR (e.g., forwarding cookies)
63
- headers?: HeadersInit;
64
+ headers?: Headers;
64
65
  }
65
66
 
66
67
  /**
@@ -68,7 +69,7 @@ export interface BlogClientConfig {
68
69
  * All hooks are optional and allow consumers to customize behavior
69
70
  */
70
71
  export interface BlogClientHooks {
71
- // Loader Hooks - called during data loading (SSR or CSR)
72
+ // Loader Hooks - called during data loading (SSR)
72
73
  beforeLoadPosts?: (
73
74
  filter: { published: boolean },
74
75
  context: LoaderContext,
@@ -77,7 +78,7 @@ export interface BlogClientHooks {
77
78
  posts: Post[] | null,
78
79
  filter: { published: boolean },
79
80
  context: LoaderContext,
80
- ) => Promise<void> | void;
81
+ ) => Promise<boolean> | boolean;
81
82
  beforeLoadPost?: (
82
83
  slug: string,
83
84
  context: LoaderContext,
@@ -86,7 +87,9 @@ export interface BlogClientHooks {
86
87
  post: Post | null,
87
88
  slug: string,
88
89
  context: LoaderContext,
89
- ) => Promise<void> | void;
90
+ ) => Promise<boolean> | boolean;
91
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
92
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
90
93
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
91
94
  }
92
95
 
@@ -101,6 +104,7 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
101
104
  isSSR: true,
102
105
  apiBaseURL,
103
106
  apiBasePath,
107
+ headers,
104
108
  };
105
109
 
106
110
  try {
@@ -143,7 +147,14 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
143
147
  if (hooks?.afterLoadPosts) {
144
148
  const posts =
145
149
  queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
146
- await hooks.afterLoadPosts(posts, { published }, context);
150
+ const canContinue = await hooks.afterLoadPosts(
151
+ posts,
152
+ { published },
153
+ context,
154
+ );
155
+ if (canContinue === false) {
156
+ throw new Error("Load prevented by afterLoadPosts hook");
157
+ }
147
158
  }
148
159
 
149
160
  // Check if there was an error after afterLoadPosts hook
@@ -181,6 +192,7 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
181
192
  isSSR: true,
182
193
  apiBaseURL,
183
194
  apiBasePath,
195
+ headers,
184
196
  };
185
197
 
186
198
  try {
@@ -208,7 +220,10 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
208
220
  if (hooks?.afterLoadPost) {
209
221
  const post =
210
222
  queryClient.getQueryData<Post>(postQuery.queryKey) || null;
211
- await hooks.afterLoadPost(post, slug, context);
223
+ const canContinue = await hooks.afterLoadPost(post, slug, context);
224
+ if (canContinue === false) {
225
+ throw new Error("Load prevented by afterLoadPost hook");
226
+ }
212
227
  }
213
228
 
214
229
  // Check if there was an error after afterLoadPost hook
@@ -235,6 +250,47 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
235
250
  };
236
251
  }
237
252
 
253
+ function createNewPostLoader(config: BlogClientConfig) {
254
+ return async () => {
255
+ if (typeof window === "undefined") {
256
+ const { apiBasePath, apiBaseURL, hooks, headers } = config;
257
+
258
+ const context: LoaderContext = {
259
+ path: "/blog/new",
260
+ isSSR: true,
261
+ apiBaseURL,
262
+ apiBasePath,
263
+ headers,
264
+ };
265
+
266
+ try {
267
+ // Before hook
268
+ if (hooks?.beforeLoadNewPost) {
269
+ const canLoad = await hooks.beforeLoadNewPost(context);
270
+ if (!canLoad) {
271
+ throw new Error("Load prevented by beforeLoadNewPost hook");
272
+ }
273
+ }
274
+
275
+ // After hook
276
+ if (hooks?.afterLoadNewPost) {
277
+ const canContinue = await hooks.afterLoadNewPost(context);
278
+ if (canContinue === false) {
279
+ throw new Error("Load prevented by afterLoadNewPost hook");
280
+ }
281
+ }
282
+ } catch (error) {
283
+ // Error hook - log the error but don't throw during SSR
284
+ // Let Error Boundaries handle errors when components render
285
+ if (hooks?.onLoadError) {
286
+ await hooks.onLoadError(error as Error, context);
287
+ }
288
+ // Don't re-throw - let Error Boundary catch it during render
289
+ }
290
+ }
291
+ };
292
+ }
293
+
238
294
  function createTagLoader(tagSlug: string, config: BlogClientConfig) {
239
295
  return async () => {
240
296
  if (typeof window === "undefined") {
@@ -246,6 +302,7 @@ function createTagLoader(tagSlug: string, config: BlogClientConfig) {
246
302
  isSSR: true,
247
303
  apiBaseURL,
248
304
  apiBasePath,
305
+ headers,
249
306
  };
250
307
 
251
308
  try {
@@ -584,6 +641,7 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
584
641
  newPost: createRoute("/blog/new", () => {
585
642
  return {
586
643
  PageComponent: NewPostPageComponent,
644
+ loader: createNewPostLoader(config),
587
645
  meta: createNewPostMeta(config),
588
646
  };
589
647
  }),
@@ -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}";
@@ -35,12 +35,12 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
35
35
  }
36
36
 
37
37
  declare const createPostSchema: z.ZodObject<{
38
- title: z.ZodString;
39
38
  slug: z.ZodOptional<z.ZodString>;
40
39
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
41
40
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
42
41
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
43
42
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
43
+ title: z.ZodString;
44
44
  content: z.ZodString;
45
45
  excerpt: z.ZodString;
46
46
  image: z.ZodOptional<z.ZodString>;
@@ -35,12 +35,12 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
35
35
  }
36
36
 
37
37
  declare const createPostSchema: z.ZodObject<{
38
- title: z.ZodString;
39
38
  slug: z.ZodOptional<z.ZodString>;
40
39
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
41
40
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
42
41
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
43
42
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
43
+ title: z.ZodString;
44
44
  content: z.ZodString;
45
45
  excerpt: z.ZodString;
46
46
  image: z.ZodOptional<z.ZodString>;
@@ -35,12 +35,12 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
35
35
  }
36
36
 
37
37
  declare const createPostSchema: z.ZodObject<{
38
- title: z.ZodString;
39
38
  slug: z.ZodOptional<z.ZodString>;
40
39
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
41
40
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
42
41
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
43
42
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
43
+ title: z.ZodString;
44
44
  content: z.ZodString;
45
45
  excerpt: z.ZodString;
46
46
  image: z.ZodOptional<z.ZodString>;