@btst/stack 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/better-stack/src/plugins/blog/api/plugin.cjs +29 -34
- package/dist/packages/better-stack/src/plugins/blog/api/plugin.mjs +29 -34
- package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +3 -3
- package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +4 -4
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/hooks/blog-hooks.cjs +16 -16
- package/dist/packages/better-stack/src/plugins/blog/client/hooks/blog-hooks.mjs +16 -16
- package/dist/packages/better-stack/src/plugins/blog/client/plugin.cjs +50 -8
- package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +50 -8
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.cts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.mts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.ts +3 -3
- package/dist/plugins/blog/client/index.d.cts +11 -4
- package/dist/plugins/blog/client/index.d.mts +11 -4
- package/dist/plugins/blog/client/index.d.ts +11 -4
- package/dist/plugins/blog/query-keys.cjs +19 -13
- package/dist/plugins/blog/query-keys.d.cts +6 -6
- package/dist/plugins/blog/query-keys.d.mts +6 -6
- package/dist/plugins/blog/query-keys.d.ts +6 -6
- package/dist/plugins/blog/query-keys.mjs +19 -13
- package/dist/plugins/blog/style.css +2 -5
- package/package.json +3 -3
- package/src/plugins/blog/api/plugin.ts +56 -36
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +4 -4
- package/src/plugins/blog/client/components/shared/default-error.tsx +4 -1
- package/src/plugins/blog/client/hooks/blog-hooks.tsx +16 -16
- package/src/plugins/blog/client/overrides.ts +4 -0
- package/src/plugins/blog/client/plugin.tsx +67 -11
- package/src/plugins/blog/query-keys.ts +13 -3
- package/src/plugins/blog/style.css +2 -5
- package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CoPoHVfV.d.cts} +2 -2
- package/dist/shared/{stack.CbuN2zVV.d.mts → stack.CoPoHVfV.d.mts} +2 -2
- package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CoPoHVfV.d.ts} +2 -2
|
@@ -16,13 +16,13 @@ function toError(error) {
|
|
|
16
16
|
}
|
|
17
17
|
return new Error(String(error));
|
|
18
18
|
}
|
|
19
|
-
function createBlogQueryKeys(client) {
|
|
20
|
-
const posts = createPostsQueries(client);
|
|
21
|
-
const drafts = createDraftsQueries(client);
|
|
22
|
-
const tags = createTagsQueries(client);
|
|
19
|
+
function createBlogQueryKeys(client, headers) {
|
|
20
|
+
const posts = createPostsQueries(client, headers);
|
|
21
|
+
const drafts = createDraftsQueries(client, headers);
|
|
22
|
+
const tags = createTagsQueries(client, headers);
|
|
23
23
|
return mergeQueryKeys(posts, drafts, tags);
|
|
24
24
|
}
|
|
25
|
-
function createPostsQueries(client) {
|
|
25
|
+
function createPostsQueries(client, headers) {
|
|
26
26
|
return createQueryKeys("posts", {
|
|
27
27
|
list: (params) => ({
|
|
28
28
|
queryKey: [
|
|
@@ -43,7 +43,8 @@ function createPostsQueries(client) {
|
|
|
43
43
|
limit: params?.limit ?? 10,
|
|
44
44
|
published: params?.published !== void 0 ? params.published ? "true" : "false" : void 0,
|
|
45
45
|
tagSlug: params?.tagSlug
|
|
46
|
-
}
|
|
46
|
+
},
|
|
47
|
+
headers
|
|
47
48
|
});
|
|
48
49
|
if (isErrorResponse(response)) {
|
|
49
50
|
const errorResponse = response;
|
|
@@ -63,7 +64,8 @@ function createPostsQueries(client) {
|
|
|
63
64
|
try {
|
|
64
65
|
const response = await client("/posts", {
|
|
65
66
|
method: "GET",
|
|
66
|
-
query: { slug, limit: 1 }
|
|
67
|
+
query: { slug, limit: 1 },
|
|
68
|
+
headers
|
|
67
69
|
});
|
|
68
70
|
if (isErrorResponse(response)) {
|
|
69
71
|
const errorResponse = response;
|
|
@@ -85,7 +87,8 @@ function createPostsQueries(client) {
|
|
|
85
87
|
method: "GET",
|
|
86
88
|
query: {
|
|
87
89
|
date: dateValue.toISOString()
|
|
88
|
-
}
|
|
90
|
+
},
|
|
91
|
+
headers
|
|
89
92
|
});
|
|
90
93
|
if (isErrorResponse(response)) {
|
|
91
94
|
const errorResponse = response;
|
|
@@ -105,7 +108,8 @@ function createPostsQueries(client) {
|
|
|
105
108
|
query: {
|
|
106
109
|
limit: params?.limit ?? 5,
|
|
107
110
|
published: "true"
|
|
108
|
-
}
|
|
111
|
+
},
|
|
112
|
+
headers
|
|
109
113
|
});
|
|
110
114
|
if (isErrorResponse(response)) {
|
|
111
115
|
const errorResponse = response;
|
|
@@ -123,7 +127,7 @@ function createPostsQueries(client) {
|
|
|
123
127
|
})
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
|
-
function createDraftsQueries(client) {
|
|
130
|
+
function createDraftsQueries(client, headers) {
|
|
127
131
|
return createQueryKeys("drafts", {
|
|
128
132
|
list: (params) => ({
|
|
129
133
|
queryKey: [
|
|
@@ -140,7 +144,8 @@ function createDraftsQueries(client) {
|
|
|
140
144
|
offset: pageParam ?? 0,
|
|
141
145
|
limit: params?.limit ?? 10,
|
|
142
146
|
published: "false"
|
|
143
|
-
}
|
|
147
|
+
},
|
|
148
|
+
headers
|
|
144
149
|
});
|
|
145
150
|
if (isErrorResponse(response)) {
|
|
146
151
|
const errorResponse = response;
|
|
@@ -154,14 +159,15 @@ function createDraftsQueries(client) {
|
|
|
154
159
|
})
|
|
155
160
|
});
|
|
156
161
|
}
|
|
157
|
-
function createTagsQueries(client) {
|
|
162
|
+
function createTagsQueries(client, headers) {
|
|
158
163
|
return createQueryKeys("tags", {
|
|
159
164
|
list: () => ({
|
|
160
165
|
queryKey: ["tags"],
|
|
161
166
|
queryFn: async () => {
|
|
162
167
|
try {
|
|
163
168
|
const response = await client("/tags", {
|
|
164
|
-
method: "GET"
|
|
169
|
+
method: "GET",
|
|
170
|
+
headers
|
|
165
171
|
});
|
|
166
172
|
if (isErrorResponse(response)) {
|
|
167
173
|
const errorResponse = response;
|
|
@@ -11,11 +11,8 @@
|
|
|
11
11
|
/* Scan this package's source files for Tailwind classes */
|
|
12
12
|
@source "../../../src/**/*.{ts,tsx}";
|
|
13
13
|
|
|
14
|
-
/* Scan UI package components (
|
|
15
|
-
@source "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
666
|
-
|
|
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:
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
|
710
|
-
...(nextPost
|
|
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
|
|
739
|
+
previous: previousPost
|
|
720
740
|
? {
|
|
721
|
-
...previousPost
|
|
722
|
-
tags: postTagsMap.get(previousPost
|
|
741
|
+
...previousPost,
|
|
742
|
+
tags: postTagsMap.get(previousPost.id) || [],
|
|
723
743
|
}
|
|
724
744
|
: null,
|
|
725
|
-
next: nextPost
|
|
745
|
+
next: nextPost
|
|
726
746
|
? {
|
|
727
|
-
...nextPost
|
|
728
|
-
tags: postTagsMap.get(nextPost
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
18
|
+
const message =
|
|
19
|
+
process.env.NODE_ENV === "production"
|
|
20
|
+
? localization.BLOG_GENERIC_ERROR_MESSAGE
|
|
21
|
+
: (error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE);
|
|
19
22
|
return <ErrorPlaceholder title={title} message={message} />;
|
|
20
23
|
}
|
|
@@ -86,7 +86,7 @@ export type PostUpdateInput = z.infer<typeof updatePostSchema>;
|
|
|
86
86
|
* Hook for fetching paginated posts with load more functionality
|
|
87
87
|
*/
|
|
88
88
|
export function usePosts(options: UsePostsOptions = {}): UsePostsResult {
|
|
89
|
-
const { apiBaseURL, apiBasePath } =
|
|
89
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
90
90
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
91
91
|
const client = createApiClient<BlogApiRouter>({
|
|
92
92
|
baseURL: apiBaseURL,
|
|
@@ -100,7 +100,7 @@ export function usePosts(options: UsePostsOptions = {}): UsePostsResult {
|
|
|
100
100
|
query,
|
|
101
101
|
published,
|
|
102
102
|
} = options;
|
|
103
|
-
const queries = createBlogQueryKeys(client);
|
|
103
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
104
104
|
|
|
105
105
|
const queryParams = {
|
|
106
106
|
tag,
|
|
@@ -155,7 +155,7 @@ export function useSuspensePosts(options: UsePostsOptions = {}): {
|
|
|
155
155
|
isLoadingMore: boolean;
|
|
156
156
|
refetch: () => Promise<unknown>;
|
|
157
157
|
} {
|
|
158
|
-
const { apiBaseURL, apiBasePath } =
|
|
158
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
159
159
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
160
160
|
const client = createApiClient<BlogApiRouter>({
|
|
161
161
|
baseURL: apiBaseURL,
|
|
@@ -169,7 +169,7 @@ export function useSuspensePosts(options: UsePostsOptions = {}): {
|
|
|
169
169
|
query,
|
|
170
170
|
published,
|
|
171
171
|
} = options;
|
|
172
|
-
const queries = createBlogQueryKeys(client);
|
|
172
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
173
173
|
|
|
174
174
|
const queryParams = { tag, tagSlug, limit, query, published };
|
|
175
175
|
const basePosts = queries.posts.list(queryParams);
|
|
@@ -214,13 +214,13 @@ export function useSuspensePosts(options: UsePostsOptions = {}): {
|
|
|
214
214
|
* Hook for fetching a single post by slug
|
|
215
215
|
*/
|
|
216
216
|
export function usePost(slug?: string): UsePostResult {
|
|
217
|
-
const { apiBaseURL, apiBasePath } =
|
|
217
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
218
218
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
219
219
|
const client = createApiClient<BlogApiRouter>({
|
|
220
220
|
baseURL: apiBaseURL,
|
|
221
221
|
basePath: apiBasePath,
|
|
222
222
|
});
|
|
223
|
-
const queries = createBlogQueryKeys(client);
|
|
223
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
224
224
|
|
|
225
225
|
const basePost = queries.posts.detail(slug ?? "");
|
|
226
226
|
const { data, isLoading, error, refetch } = useQuery<
|
|
@@ -247,13 +247,13 @@ export function useSuspensePost(slug: string): {
|
|
|
247
247
|
post: SerializedPost | null;
|
|
248
248
|
refetch: () => Promise<unknown>;
|
|
249
249
|
} {
|
|
250
|
-
const { apiBaseURL, apiBasePath } =
|
|
250
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
251
251
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
252
252
|
const client = createApiClient<BlogApiRouter>({
|
|
253
253
|
baseURL: apiBaseURL,
|
|
254
254
|
basePath: apiBasePath,
|
|
255
255
|
});
|
|
256
|
-
const queries = createBlogQueryKeys(client);
|
|
256
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
257
257
|
const basePost = queries.posts.detail(slug);
|
|
258
258
|
const { data, refetch, error, isFetching } = useSuspenseQuery<
|
|
259
259
|
SerializedPost | null,
|
|
@@ -283,13 +283,13 @@ export function useTags(): {
|
|
|
283
283
|
error: Error | null;
|
|
284
284
|
refetch: () => void;
|
|
285
285
|
} {
|
|
286
|
-
const { apiBaseURL, apiBasePath } =
|
|
286
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
287
287
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
288
288
|
const client = createApiClient<BlogApiRouter>({
|
|
289
289
|
baseURL: apiBaseURL,
|
|
290
290
|
basePath: apiBasePath,
|
|
291
291
|
});
|
|
292
|
-
const queries = createBlogQueryKeys(client);
|
|
292
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
293
293
|
const baseTags = queries.tags.list();
|
|
294
294
|
const { data, isLoading, error, refetch } = useQuery<
|
|
295
295
|
SerializedTag[] | null,
|
|
@@ -315,13 +315,13 @@ export function useSuspenseTags(): {
|
|
|
315
315
|
tags: SerializedTag[];
|
|
316
316
|
refetch: () => Promise<unknown>;
|
|
317
317
|
} {
|
|
318
|
-
const { apiBaseURL, apiBasePath } =
|
|
318
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
319
319
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
320
320
|
const client = createApiClient<BlogApiRouter>({
|
|
321
321
|
baseURL: apiBaseURL,
|
|
322
322
|
basePath: apiBasePath,
|
|
323
323
|
});
|
|
324
|
-
const queries = createBlogQueryKeys(client);
|
|
324
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
325
325
|
const baseTags = queries.tags.list();
|
|
326
326
|
const { data, refetch, error, isFetching } = useSuspenseQuery<
|
|
327
327
|
SerializedTag[] | null,
|
|
@@ -555,13 +555,13 @@ export function useNextPreviousPosts(
|
|
|
555
555
|
ref: (node: Element | null) => void;
|
|
556
556
|
inView: boolean;
|
|
557
557
|
} {
|
|
558
|
-
const { apiBaseURL, apiBasePath } =
|
|
558
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
559
559
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
560
560
|
const client = createApiClient<BlogApiRouter>({
|
|
561
561
|
baseURL: apiBaseURL,
|
|
562
562
|
basePath: apiBasePath,
|
|
563
563
|
});
|
|
564
|
-
const queries = createBlogQueryKeys(client);
|
|
564
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
565
565
|
|
|
566
566
|
const { ref, inView } = useInView({
|
|
567
567
|
// start a little early so the data is ready as it scrolls in
|
|
@@ -619,13 +619,13 @@ export function useRecentPosts(
|
|
|
619
619
|
ref: (node: Element | null) => void;
|
|
620
620
|
inView: boolean;
|
|
621
621
|
} {
|
|
622
|
-
const { apiBaseURL, apiBasePath } =
|
|
622
|
+
const { apiBaseURL, apiBasePath, headers } =
|
|
623
623
|
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
624
624
|
const client = createApiClient<BlogApiRouter>({
|
|
625
625
|
baseURL: apiBaseURL,
|
|
626
626
|
basePath: apiBasePath,
|
|
627
627
|
});
|
|
628
|
-
const queries = createBlogQueryKeys(client);
|
|
628
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
629
629
|
|
|
630
630
|
const { ref, inView } = useInView({
|
|
631
631
|
// start a little early so the data is ready as it scrolls in
|
|
@@ -57,6 +57,10 @@ export interface BlogPluginOverrides {
|
|
|
57
57
|
* Whether to show the attribution
|
|
58
58
|
*/
|
|
59
59
|
showAttribution?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Optional headers to pass with API requests (e.g., for SSR auth)
|
|
62
|
+
*/
|
|
63
|
+
headers?: HeadersInit;
|
|
60
64
|
|
|
61
65
|
// Lifecycle Hooks (optional)
|
|
62
66
|
/**
|
|
@@ -58,6 +58,9 @@ export interface BlogClientConfig {
|
|
|
58
58
|
|
|
59
59
|
// Optional hooks
|
|
60
60
|
hooks?: BlogClientHooks;
|
|
61
|
+
|
|
62
|
+
// Optional headers for SSR (e.g., forwarding cookies)
|
|
63
|
+
headers?: HeadersInit;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
/**
|
|
@@ -65,7 +68,7 @@ export interface BlogClientConfig {
|
|
|
65
68
|
* All hooks are optional and allow consumers to customize behavior
|
|
66
69
|
*/
|
|
67
70
|
export interface BlogClientHooks {
|
|
68
|
-
// Loader Hooks - called during data loading (SSR
|
|
71
|
+
// Loader Hooks - called during data loading (SSR)
|
|
69
72
|
beforeLoadPosts?: (
|
|
70
73
|
filter: { published: boolean },
|
|
71
74
|
context: LoaderContext,
|
|
@@ -74,7 +77,7 @@ export interface BlogClientHooks {
|
|
|
74
77
|
posts: Post[] | null,
|
|
75
78
|
filter: { published: boolean },
|
|
76
79
|
context: LoaderContext,
|
|
77
|
-
) => Promise<
|
|
80
|
+
) => Promise<boolean> | boolean;
|
|
78
81
|
beforeLoadPost?: (
|
|
79
82
|
slug: string,
|
|
80
83
|
context: LoaderContext,
|
|
@@ -83,7 +86,9 @@ export interface BlogClientHooks {
|
|
|
83
86
|
post: Post | null,
|
|
84
87
|
slug: string,
|
|
85
88
|
context: LoaderContext,
|
|
86
|
-
) => Promise<
|
|
89
|
+
) => Promise<boolean> | boolean;
|
|
90
|
+
beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
91
|
+
afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
87
92
|
onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -91,7 +96,7 @@ export interface BlogClientHooks {
|
|
|
91
96
|
function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
92
97
|
return async () => {
|
|
93
98
|
if (typeof window === "undefined") {
|
|
94
|
-
const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
|
|
99
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
95
100
|
|
|
96
101
|
const context: LoaderContext = {
|
|
97
102
|
path: published ? "/blog" : "/blog/drafts",
|
|
@@ -116,7 +121,7 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
|
116
121
|
});
|
|
117
122
|
|
|
118
123
|
// note: for a module not to be bundled with client, and to be shared by client and server we need to add it to build.config.ts as an entry
|
|
119
|
-
const queries = createBlogQueryKeys(client);
|
|
124
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
120
125
|
const listQuery = queries.posts.list({
|
|
121
126
|
query: undefined,
|
|
122
127
|
limit,
|
|
@@ -140,7 +145,14 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
|
140
145
|
if (hooks?.afterLoadPosts) {
|
|
141
146
|
const posts =
|
|
142
147
|
queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
|
|
143
|
-
await hooks.afterLoadPosts(
|
|
148
|
+
const canContinue = await hooks.afterLoadPosts(
|
|
149
|
+
posts,
|
|
150
|
+
{ published },
|
|
151
|
+
context,
|
|
152
|
+
);
|
|
153
|
+
if (canContinue === false) {
|
|
154
|
+
throw new Error("Load prevented by afterLoadPosts hook");
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
// Check if there was an error after afterLoadPosts hook
|
|
@@ -170,7 +182,7 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
|
170
182
|
function createPostLoader(slug: string, config: BlogClientConfig) {
|
|
171
183
|
return async () => {
|
|
172
184
|
if (typeof window === "undefined") {
|
|
173
|
-
const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
|
|
185
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
174
186
|
|
|
175
187
|
const context: LoaderContext = {
|
|
176
188
|
path: `/blog/${slug}`,
|
|
@@ -193,7 +205,7 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
|
|
|
193
205
|
baseURL: apiBaseURL,
|
|
194
206
|
basePath: apiBasePath,
|
|
195
207
|
});
|
|
196
|
-
const queries = createBlogQueryKeys(client);
|
|
208
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
197
209
|
const postQuery = queries.posts.detail(slug);
|
|
198
210
|
await queryClient.prefetchQuery(postQuery);
|
|
199
211
|
|
|
@@ -205,7 +217,10 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
|
|
|
205
217
|
if (hooks?.afterLoadPost) {
|
|
206
218
|
const post =
|
|
207
219
|
queryClient.getQueryData<Post>(postQuery.queryKey) || null;
|
|
208
|
-
await hooks.afterLoadPost(post, slug, context);
|
|
220
|
+
const canContinue = await hooks.afterLoadPost(post, slug, context);
|
|
221
|
+
if (canContinue === false) {
|
|
222
|
+
throw new Error("Load prevented by afterLoadPost hook");
|
|
223
|
+
}
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
// Check if there was an error after afterLoadPost hook
|
|
@@ -232,10 +247,50 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
|
|
|
232
247
|
};
|
|
233
248
|
}
|
|
234
249
|
|
|
250
|
+
function createNewPostLoader(config: BlogClientConfig) {
|
|
251
|
+
return async () => {
|
|
252
|
+
if (typeof window === "undefined") {
|
|
253
|
+
const { apiBasePath, apiBaseURL, hooks } = config;
|
|
254
|
+
|
|
255
|
+
const context: LoaderContext = {
|
|
256
|
+
path: "/blog/new",
|
|
257
|
+
isSSR: true,
|
|
258
|
+
apiBaseURL,
|
|
259
|
+
apiBasePath,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Before hook
|
|
264
|
+
if (hooks?.beforeLoadNewPost) {
|
|
265
|
+
const canLoad = await hooks.beforeLoadNewPost(context);
|
|
266
|
+
if (!canLoad) {
|
|
267
|
+
throw new Error("Load prevented by beforeLoadNewPost hook");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// After hook
|
|
272
|
+
if (hooks?.afterLoadNewPost) {
|
|
273
|
+
const canContinue = await hooks.afterLoadNewPost(context);
|
|
274
|
+
if (canContinue === false) {
|
|
275
|
+
throw new Error("Load prevented by afterLoadNewPost hook");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Error hook - log the error but don't throw during SSR
|
|
280
|
+
// Let Error Boundaries handle errors when components render
|
|
281
|
+
if (hooks?.onLoadError) {
|
|
282
|
+
await hooks.onLoadError(error as Error, context);
|
|
283
|
+
}
|
|
284
|
+
// Don't re-throw - let Error Boundary catch it during render
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
235
290
|
function createTagLoader(tagSlug: string, config: BlogClientConfig) {
|
|
236
291
|
return async () => {
|
|
237
292
|
if (typeof window === "undefined") {
|
|
238
|
-
const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
|
|
293
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
239
294
|
|
|
240
295
|
const context: LoaderContext = {
|
|
241
296
|
path: `/blog/tag/${tagSlug}`,
|
|
@@ -252,7 +307,7 @@ function createTagLoader(tagSlug: string, config: BlogClientConfig) {
|
|
|
252
307
|
basePath: apiBasePath,
|
|
253
308
|
});
|
|
254
309
|
|
|
255
|
-
const queries = createBlogQueryKeys(client);
|
|
310
|
+
const queries = createBlogQueryKeys(client, headers);
|
|
256
311
|
const listQuery = queries.posts.list({
|
|
257
312
|
query: undefined,
|
|
258
313
|
limit,
|
|
@@ -581,6 +636,7 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
|
581
636
|
newPost: createRoute("/blog/new", () => {
|
|
582
637
|
return {
|
|
583
638
|
PageComponent: NewPostPageComponent,
|
|
639
|
+
loader: createNewPostLoader(config),
|
|
584
640
|
meta: createNewPostMeta(config),
|
|
585
641
|
};
|
|
586
642
|
}),
|