@btst/stack 1.1.7 → 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 (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 +44 -2
  8. package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +44 -2
  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 +6 -4
  16. package/dist/plugins/blog/client/index.d.mts +6 -4
  17. package/dist/plugins/blog/client/index.d.ts +6 -4
  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 +58 -5
  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
 
@@ -46,7 +46,14 @@ function createPostsLoader(published, config) {
46
46
  await queryClient.prefetchQuery(tagsQuery);
47
47
  if (hooks?.afterLoadPosts) {
48
48
  const posts = queryClient.getQueryData(listQuery.queryKey) || null;
49
- await hooks.afterLoadPosts(posts, { published }, context);
49
+ const canContinue = await hooks.afterLoadPosts(
50
+ posts,
51
+ { published },
52
+ context
53
+ );
54
+ if (canContinue === false) {
55
+ throw new Error("Load prevented by afterLoadPosts hook");
56
+ }
50
57
  }
51
58
  const queryState = queryClient.getQueryState(listQuery.queryKey);
52
59
  if (queryState?.error) {
@@ -90,7 +97,10 @@ function createPostLoader(slug, config) {
90
97
  await queryClient.prefetchQuery(postQuery);
91
98
  if (hooks?.afterLoadPost) {
92
99
  const post = queryClient.getQueryData(postQuery.queryKey) || null;
93
- 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
+ }
94
104
  }
95
105
  const queryState = queryClient.getQueryState(postQuery.queryKey);
96
106
  if (queryState?.error) {
@@ -107,6 +117,37 @@ function createPostLoader(slug, config) {
107
117
  }
108
118
  };
109
119
  }
120
+ function createNewPostLoader(config) {
121
+ return async () => {
122
+ if (typeof window === "undefined") {
123
+ const { apiBasePath, apiBaseURL, hooks } = config;
124
+ const context = {
125
+ path: "/blog/new",
126
+ isSSR: true,
127
+ apiBaseURL,
128
+ apiBasePath
129
+ };
130
+ try {
131
+ if (hooks?.beforeLoadNewPost) {
132
+ const canLoad = await hooks.beforeLoadNewPost(context);
133
+ if (!canLoad) {
134
+ throw new Error("Load prevented by beforeLoadNewPost hook");
135
+ }
136
+ }
137
+ if (hooks?.afterLoadNewPost) {
138
+ const canContinue = await hooks.afterLoadNewPost(context);
139
+ if (canContinue === false) {
140
+ throw new Error("Load prevented by afterLoadNewPost hook");
141
+ }
142
+ }
143
+ } catch (error) {
144
+ if (hooks?.onLoadError) {
145
+ await hooks.onLoadError(error, context);
146
+ }
147
+ }
148
+ }
149
+ };
150
+ }
110
151
  function createTagLoader(tagSlug, config) {
111
152
  return async () => {
112
153
  if (typeof window === "undefined") {
@@ -382,6 +423,7 @@ const blogClientPlugin = (config) => client.defineClientPlugin({
382
423
  newPost: yar.createRoute("/blog/new", () => {
383
424
  return {
384
425
  PageComponent: newPostPage.NewPostPageComponent,
426
+ loader: createNewPostLoader(config),
385
427
  meta: createNewPostMeta(config)
386
428
  };
387
429
  }),
@@ -44,7 +44,14 @@ function createPostsLoader(published, config) {
44
44
  await queryClient.prefetchQuery(tagsQuery);
45
45
  if (hooks?.afterLoadPosts) {
46
46
  const posts = queryClient.getQueryData(listQuery.queryKey) || null;
47
- await hooks.afterLoadPosts(posts, { published }, context);
47
+ const canContinue = await hooks.afterLoadPosts(
48
+ posts,
49
+ { published },
50
+ context
51
+ );
52
+ if (canContinue === false) {
53
+ throw new Error("Load prevented by afterLoadPosts hook");
54
+ }
48
55
  }
49
56
  const queryState = queryClient.getQueryState(listQuery.queryKey);
50
57
  if (queryState?.error) {
@@ -88,7 +95,10 @@ function createPostLoader(slug, config) {
88
95
  await queryClient.prefetchQuery(postQuery);
89
96
  if (hooks?.afterLoadPost) {
90
97
  const post = queryClient.getQueryData(postQuery.queryKey) || null;
91
- await hooks.afterLoadPost(post, slug, context);
98
+ const canContinue = await hooks.afterLoadPost(post, slug, context);
99
+ if (canContinue === false) {
100
+ throw new Error("Load prevented by afterLoadPost hook");
101
+ }
92
102
  }
93
103
  const queryState = queryClient.getQueryState(postQuery.queryKey);
94
104
  if (queryState?.error) {
@@ -105,6 +115,37 @@ function createPostLoader(slug, config) {
105
115
  }
106
116
  };
107
117
  }
118
+ function createNewPostLoader(config) {
119
+ return async () => {
120
+ if (typeof window === "undefined") {
121
+ const { apiBasePath, apiBaseURL, hooks } = config;
122
+ const context = {
123
+ path: "/blog/new",
124
+ isSSR: true,
125
+ apiBaseURL,
126
+ apiBasePath
127
+ };
128
+ try {
129
+ if (hooks?.beforeLoadNewPost) {
130
+ const canLoad = await hooks.beforeLoadNewPost(context);
131
+ if (!canLoad) {
132
+ throw new Error("Load prevented by beforeLoadNewPost hook");
133
+ }
134
+ }
135
+ if (hooks?.afterLoadNewPost) {
136
+ const canContinue = await hooks.afterLoadNewPost(context);
137
+ if (canContinue === false) {
138
+ throw new Error("Load prevented by afterLoadNewPost hook");
139
+ }
140
+ }
141
+ } catch (error) {
142
+ if (hooks?.onLoadError) {
143
+ await hooks.onLoadError(error, context);
144
+ }
145
+ }
146
+ }
147
+ };
148
+ }
108
149
  function createTagLoader(tagSlug, config) {
109
150
  return async () => {
110
151
  if (typeof window === "undefined") {
@@ -380,6 +421,7 @@ const blogClientPlugin = (config) => defineClientPlugin({
380
421
  newPost: createRoute("/blog/new", () => {
381
422
  return {
382
423
  PageComponent: NewPostPageComponent,
424
+ loader: createNewPostLoader(config),
383
425
  meta: createNewPostMeta(config)
384
426
  };
385
427
  }),
@@ -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
 
@@ -57,9 +57,11 @@ interface BlogClientHooks {
57
57
  }, context: LoaderContext) => Promise<boolean> | boolean;
58
58
  afterLoadPosts?: (posts: Post[] | null, filter: {
59
59
  published: boolean;
60
- }, context: LoaderContext) => Promise<void> | void;
60
+ }, context: LoaderContext) => Promise<boolean> | boolean;
61
61
  beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
62
- afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<void> | void;
62
+ afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
63
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
64
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
63
65
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
64
66
  }
65
67
  /**
@@ -130,7 +132,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
130
132
  PageComponent?: react.ComponentType<unknown> | undefined;
131
133
  LoadingComponent?: react.ComponentType<unknown> | undefined;
132
134
  ErrorComponent?: react.ComponentType<unknown> | undefined;
133
- loader?: (() => any) | undefined;
135
+ loader?: (() => Promise<void>) | undefined;
134
136
  meta?: (() => ({
135
137
  title: string;
136
138
  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
 
@@ -57,9 +57,11 @@ interface BlogClientHooks {
57
57
  }, context: LoaderContext) => Promise<boolean> | boolean;
58
58
  afterLoadPosts?: (posts: Post[] | null, filter: {
59
59
  published: boolean;
60
- }, context: LoaderContext) => Promise<void> | void;
60
+ }, context: LoaderContext) => Promise<boolean> | boolean;
61
61
  beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
62
- afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<void> | void;
62
+ afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
63
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
64
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
63
65
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
64
66
  }
65
67
  /**
@@ -130,7 +132,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
130
132
  PageComponent?: react.ComponentType<unknown> | undefined;
131
133
  LoadingComponent?: react.ComponentType<unknown> | undefined;
132
134
  ErrorComponent?: react.ComponentType<unknown> | undefined;
133
- loader?: (() => any) | undefined;
135
+ loader?: (() => Promise<void>) | undefined;
134
136
  meta?: (() => ({
135
137
  title: string;
136
138
  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
 
@@ -57,9 +57,11 @@ interface BlogClientHooks {
57
57
  }, context: LoaderContext) => Promise<boolean> | boolean;
58
58
  afterLoadPosts?: (posts: Post[] | null, filter: {
59
59
  published: boolean;
60
- }, context: LoaderContext) => Promise<void> | void;
60
+ }, context: LoaderContext) => Promise<boolean> | boolean;
61
61
  beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
62
- afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<void> | void;
62
+ afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
63
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
64
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
63
65
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
64
66
  }
65
67
  /**
@@ -130,7 +132,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
130
132
  PageComponent?: react.ComponentType<unknown> | undefined;
131
133
  LoadingComponent?: react.ComponentType<unknown> | undefined;
132
134
  ErrorComponent?: react.ComponentType<unknown> | undefined;
133
- loader?: (() => any) | undefined;
135
+ loader?: (() => Promise<void>) | undefined;
134
136
  meta?: (() => ({
135
137
  title: string;
136
138
  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.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
  }
@@ -68,7 +68,7 @@ export interface BlogClientConfig {
68
68
  * All hooks are optional and allow consumers to customize behavior
69
69
  */
70
70
  export interface BlogClientHooks {
71
- // Loader Hooks - called during data loading (SSR or CSR)
71
+ // Loader Hooks - called during data loading (SSR)
72
72
  beforeLoadPosts?: (
73
73
  filter: { published: boolean },
74
74
  context: LoaderContext,
@@ -77,7 +77,7 @@ export interface BlogClientHooks {
77
77
  posts: Post[] | null,
78
78
  filter: { published: boolean },
79
79
  context: LoaderContext,
80
- ) => Promise<void> | void;
80
+ ) => Promise<boolean> | boolean;
81
81
  beforeLoadPost?: (
82
82
  slug: string,
83
83
  context: LoaderContext,
@@ -86,7 +86,9 @@ export interface BlogClientHooks {
86
86
  post: Post | null,
87
87
  slug: string,
88
88
  context: LoaderContext,
89
- ) => Promise<void> | void;
89
+ ) => Promise<boolean> | boolean;
90
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
91
+ afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
90
92
  onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
91
93
  }
92
94
 
@@ -143,7 +145,14 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
143
145
  if (hooks?.afterLoadPosts) {
144
146
  const posts =
145
147
  queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
146
- 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
+ }
147
156
  }
148
157
 
149
158
  // Check if there was an error after afterLoadPosts hook
@@ -208,7 +217,10 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
208
217
  if (hooks?.afterLoadPost) {
209
218
  const post =
210
219
  queryClient.getQueryData<Post>(postQuery.queryKey) || null;
211
- 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
+ }
212
224
  }
213
225
 
214
226
  // Check if there was an error after afterLoadPost hook
@@ -235,6 +247,46 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
235
247
  };
236
248
  }
237
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
+
238
290
  function createTagLoader(tagSlug: string, config: BlogClientConfig) {
239
291
  return async () => {
240
292
  if (typeof window === "undefined") {
@@ -584,6 +636,7 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
584
636
  newPost: createRoute("/blog/new", () => {
585
637
  return {
586
638
  PageComponent: NewPostPageComponent,
639
+ loader: createNewPostLoader(config),
587
640
  meta: createNewPostMeta(config),
588
641
  };
589
642
  }),
@@ -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>;