@btst/stack 1.0.1 → 1.1.1
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/README.md +156 -709
- package/dist/api/index.cjs +2 -1
- package/dist/api/index.d.cts +4 -3
- package/dist/api/index.d.mts +4 -3
- package/dist/api/index.d.ts +4 -3
- package/dist/api/index.mjs +1 -1
- package/dist/client/components/compose.cjs +68 -0
- package/dist/client/components/compose.mjs +65 -0
- package/dist/client/components/error-boundary.cjs +24 -0
- package/dist/client/components/error-boundary.mjs +22 -0
- package/dist/client/components/index.cjs +10 -0
- package/dist/client/components/index.d.cts +52 -0
- package/dist/client/components/index.d.mts +52 -0
- package/dist/client/components/index.d.ts +52 -0
- package/dist/client/components/index.mjs +2 -0
- package/dist/client/index.cjs +24 -5
- package/dist/client/index.d.cts +125 -8
- package/dist/client/index.d.mts +125 -8
- package/dist/client/index.d.ts +125 -8
- package/dist/client/index.mjs +21 -4
- package/dist/client/meta-utils.cjs +162 -0
- package/dist/client/meta-utils.mjs +160 -0
- package/dist/client/path-utils.cjs +15 -0
- package/dist/client/path-utils.mjs +13 -0
- package/dist/client/sitemap-utils.cjs +14 -0
- package/dist/client/sitemap-utils.mjs +12 -0
- package/dist/context/index.cjs +6 -63
- package/dist/context/index.d.cts +21 -24
- package/dist/context/index.d.mts +21 -24
- package/dist/context/index.d.ts +21 -24
- package/dist/context/index.mjs +1 -61
- package/dist/context/provider.cjs +51 -0
- package/dist/context/provider.mjs +46 -0
- package/dist/index.cjs +2 -3
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +1 -2
- package/dist/plugins/api/index.cjs +13 -0
- package/dist/plugins/api/index.d.cts +40 -0
- package/dist/plugins/api/index.d.mts +40 -0
- package/dist/plugins/api/index.d.ts +40 -0
- package/dist/plugins/api/index.mjs +8 -0
- package/dist/plugins/blog/api/index.cjs +11 -0
- package/dist/plugins/blog/api/index.d.cts +7 -0
- package/dist/plugins/blog/api/index.d.mts +7 -0
- package/dist/plugins/blog/api/index.d.ts +7 -0
- package/dist/plugins/blog/api/index.mjs +2 -0
- package/dist/plugins/blog/api/plugin.cjs +569 -0
- package/dist/plugins/blog/api/plugin.mjs +565 -0
- package/dist/plugins/blog/client/components/forms/image-field.cjs +133 -0
- package/dist/plugins/blog/client/components/forms/image-field.mjs +131 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor.cjs +106 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor.mjs +104 -0
- package/dist/plugins/blog/client/components/forms/post-forms.cjs +401 -0
- package/dist/plugins/blog/client/components/forms/post-forms.mjs +398 -0
- package/dist/plugins/blog/client/components/forms/tags-multiselect.cjs +71 -0
- package/dist/plugins/blog/client/components/forms/tags-multiselect.mjs +65 -0
- package/dist/plugins/blog/client/components/index.cjs +17 -0
- package/dist/plugins/blog/client/components/index.d.cts +22 -0
- package/dist/plugins/blog/client/components/index.d.mts +22 -0
- package/dist/plugins/blog/client/components/index.d.ts +22 -0
- package/dist/plugins/blog/client/components/index.mjs +12 -0
- package/dist/plugins/blog/client/components/loading/form-page-skeleton.cjs +62 -0
- package/dist/plugins/blog/client/components/loading/form-page-skeleton.mjs +60 -0
- package/dist/plugins/blog/client/components/loading/index.cjs +20 -0
- package/dist/plugins/blog/client/components/loading/index.mjs +16 -0
- package/dist/plugins/blog/client/components/loading/list-page-skeleton.cjs +26 -0
- package/dist/plugins/blog/client/components/loading/list-page-skeleton.mjs +24 -0
- package/dist/plugins/blog/client/components/loading/page-header-skeleton.cjs +13 -0
- package/dist/plugins/blog/client/components/loading/page-header-skeleton.mjs +11 -0
- package/dist/plugins/blog/client/components/loading/post-card-skeleton.cjs +22 -0
- package/dist/plugins/blog/client/components/loading/post-card-skeleton.mjs +20 -0
- package/dist/plugins/blog/client/components/loading/post-page-skeleton.cjs +56 -0
- package/dist/plugins/blog/client/components/loading/post-page-skeleton.mjs +54 -0
- package/dist/plugins/blog/client/components/pages/404-page.cjs +19 -0
- package/dist/plugins/blog/client/components/pages/404-page.mjs +17 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.cjs +41 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.internal.cjs +57 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.internal.mjs +55 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.mjs +39 -0
- package/dist/plugins/blog/client/components/pages/home-page.cjs +41 -0
- package/dist/plugins/blog/client/components/pages/home-page.internal.cjs +61 -0
- package/dist/plugins/blog/client/components/pages/home-page.internal.mjs +59 -0
- package/dist/plugins/blog/client/components/pages/home-page.mjs +39 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.cjs +37 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.internal.cjs +53 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.internal.mjs +51 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.mjs +35 -0
- package/dist/plugins/blog/client/components/pages/post-page.cjs +39 -0
- package/dist/plugins/blog/client/components/pages/post-page.internal.cjs +101 -0
- package/dist/plugins/blog/client/components/pages/post-page.internal.mjs +99 -0
- package/dist/plugins/blog/client/components/pages/post-page.mjs +37 -0
- package/dist/plugins/blog/client/components/pages/tag-page.cjs +39 -0
- package/dist/plugins/blog/client/components/pages/tag-page.internal.cjs +61 -0
- package/dist/plugins/blog/client/components/pages/tag-page.internal.mjs +59 -0
- package/dist/plugins/blog/client/components/pages/tag-page.mjs +37 -0
- package/dist/plugins/blog/client/components/shared/better-blog-attribution.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/better-blog-attribution.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/default-error.cjs +18 -0
- package/dist/plugins/blog/client/components/shared/default-error.mjs +16 -0
- package/dist/plugins/blog/client/components/shared/defaults.cjs +13 -0
- package/dist/plugins/blog/client/components/shared/defaults.mjs +10 -0
- package/dist/plugins/blog/client/components/shared/empty-list.cjs +21 -0
- package/dist/plugins/blog/client/components/shared/empty-list.mjs +19 -0
- package/dist/plugins/blog/client/components/shared/error-placeholder.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/error-placeholder.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/highlight-text.cjs +53 -0
- package/dist/plugins/blog/client/components/shared/highlight-text.mjs +51 -0
- package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
- package/dist/plugins/blog/client/components/shared/markdown-content.cjs +324 -0
- package/dist/plugins/blog/client/components/shared/markdown-content.mjs +315 -0
- package/dist/plugins/blog/client/components/shared/on-this-page.cjs +161 -0
- package/dist/plugins/blog/client/components/shared/on-this-page.mjs +158 -0
- package/dist/plugins/blog/client/components/shared/page-header.cjs +40 -0
- package/dist/plugins/blog/client/components/shared/page-header.mjs +38 -0
- package/dist/plugins/blog/client/components/shared/page-layout.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/page-layout.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/page-wrapper.cjs +23 -0
- package/dist/plugins/blog/client/components/shared/page-wrapper.mjs +21 -0
- package/dist/plugins/blog/client/components/shared/post-card.cjs +279 -0
- package/dist/plugins/blog/client/components/shared/post-card.mjs +277 -0
- package/dist/plugins/blog/client/components/shared/post-navigation.cjs +74 -0
- package/dist/plugins/blog/client/components/shared/post-navigation.mjs +72 -0
- package/dist/plugins/blog/client/components/shared/posts-list.cjs +48 -0
- package/dist/plugins/blog/client/components/shared/posts-list.mjs +46 -0
- package/dist/plugins/blog/client/components/shared/recent-posts-carousel.cjs +59 -0
- package/dist/plugins/blog/client/components/shared/recent-posts-carousel.mjs +57 -0
- package/dist/plugins/blog/client/components/shared/search-input.cjs +136 -0
- package/dist/plugins/blog/client/components/shared/search-input.mjs +117 -0
- package/dist/plugins/blog/client/components/shared/search-modal.cjs +135 -0
- package/dist/plugins/blog/client/components/shared/search-modal.mjs +116 -0
- package/dist/plugins/blog/client/components/shared/tags-list.cjs +22 -0
- package/dist/plugins/blog/client/components/shared/tags-list.mjs +20 -0
- package/dist/plugins/blog/client/components/shared/use-route-lifecycle.cjs +50 -0
- package/dist/plugins/blog/client/components/shared/use-route-lifecycle.mjs +48 -0
- package/dist/plugins/blog/client/hooks/blog-hooks.cjs +380 -0
- package/dist/plugins/blog/client/hooks/blog-hooks.mjs +368 -0
- package/dist/plugins/blog/client/hooks/index.cjs +17 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +150 -0
- package/dist/plugins/blog/client/hooks/index.d.mts +150 -0
- package/dist/plugins/blog/client/hooks/index.d.ts +150 -0
- package/dist/plugins/blog/client/hooks/index.mjs +1 -0
- package/dist/plugins/blog/client/hooks/use-debounce.cjs +16 -0
- package/dist/plugins/blog/client/hooks/use-debounce.mjs +14 -0
- package/dist/plugins/blog/client/index.cjs +7 -0
- package/dist/plugins/blog/client/index.d.cts +414 -0
- package/dist/plugins/blog/client/index.d.mts +414 -0
- package/dist/plugins/blog/client/index.d.ts +414 -0
- package/dist/plugins/blog/client/index.mjs +1 -0
- package/dist/plugins/blog/client/localization/blog-card.cjs +7 -0
- package/dist/plugins/blog/client/localization/blog-card.mjs +5 -0
- package/dist/plugins/blog/client/localization/blog-common.cjs +10 -0
- package/dist/plugins/blog/client/localization/blog-common.mjs +8 -0
- package/dist/plugins/blog/client/localization/blog-forms.cjs +40 -0
- package/dist/plugins/blog/client/localization/blog-forms.mjs +38 -0
- package/dist/plugins/blog/client/localization/blog-list.cjs +18 -0
- package/dist/plugins/blog/client/localization/blog-list.mjs +16 -0
- package/dist/plugins/blog/client/localization/blog-post.cjs +13 -0
- package/dist/plugins/blog/client/localization/blog-post.mjs +11 -0
- package/dist/plugins/blog/client/localization/index.cjs +17 -0
- package/dist/plugins/blog/client/localization/index.mjs +15 -0
- package/dist/plugins/blog/client/plugin.cjs +462 -0
- package/dist/plugins/blog/client/plugin.mjs +460 -0
- package/dist/plugins/blog/client.css +3 -0
- package/dist/plugins/blog/db.cjs +90 -0
- package/dist/plugins/blog/db.mjs +88 -0
- package/dist/plugins/blog/query-keys.cjs +181 -0
- package/dist/plugins/blog/query-keys.d.cts +530 -0
- package/dist/plugins/blog/query-keys.d.mts +530 -0
- package/dist/plugins/blog/query-keys.d.ts +530 -0
- package/dist/plugins/blog/query-keys.mjs +179 -0
- package/dist/plugins/blog/schemas.cjs +39 -0
- package/dist/plugins/blog/schemas.mjs +35 -0
- package/dist/plugins/blog/style.css +22 -0
- package/dist/plugins/blog/utils.cjs +97 -0
- package/dist/plugins/blog/utils.mjs +87 -0
- package/dist/plugins/client/index.cjs +15 -0
- package/dist/plugins/client/index.d.cts +57 -0
- package/dist/plugins/client/index.d.mts +57 -0
- package/dist/plugins/client/index.d.ts +57 -0
- package/dist/plugins/client/index.mjs +9 -0
- package/dist/{shared/stack.3OUyGp_E.mjs → plugins/utils.mjs} +1 -1
- package/dist/shared/{stack.DORw_1ps.d.cts → stack.ByOugz9d.d.cts} +17 -1
- package/dist/shared/{stack.DORw_1ps.d.mts → stack.ByOugz9d.d.mts} +17 -1
- package/dist/shared/{stack.DORw_1ps.d.ts → stack.ByOugz9d.d.ts} +17 -1
- package/dist/shared/stack.CoPoHVfV.d.cts +76 -0
- package/dist/shared/stack.CoPoHVfV.d.mts +76 -0
- package/dist/shared/stack.CoPoHVfV.d.ts +76 -0
- package/package.json +102 -14
- package/src/__tests__/plugins.test.tsx +539 -0
- package/src/__tests__/sitemap.test.ts +60 -0
- package/src/api/index.ts +75 -0
- package/src/client/components/compose.tsx +116 -0
- package/src/client/components/error-boundary.tsx +30 -0
- package/src/client/components/index.tsx +2 -0
- package/src/client/index.ts +109 -0
- package/src/client/meta-utils.ts +228 -0
- package/src/client/path-utils.ts +38 -0
- package/src/client/sitemap-utils.ts +46 -0
- package/src/context/index.ts +1 -0
- package/src/context/provider.tsx +157 -0
- package/src/index.ts +1 -0
- package/src/plugins/api/index.ts +50 -0
- package/src/plugins/blog/api/index.ts +2 -0
- package/src/plugins/blog/api/plugin.ts +759 -0
- package/src/plugins/blog/client/components/forms/image-field.tsx +165 -0
- package/src/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
- package/src/plugins/blog/client/components/forms/markdown-editor.tsx +136 -0
- package/src/plugins/blog/client/components/forms/post-forms.tsx +531 -0
- package/src/plugins/blog/client/components/forms/tags-multiselect.tsx +79 -0
- package/src/plugins/blog/client/components/index.tsx +11 -0
- package/src/plugins/blog/client/components/loading/form-page-skeleton.tsx +75 -0
- package/src/plugins/blog/client/components/loading/index.tsx +27 -0
- package/src/plugins/blog/client/components/loading/list-page-skeleton.tsx +38 -0
- package/src/plugins/blog/client/components/loading/page-header-skeleton.tsx +10 -0
- package/src/plugins/blog/client/components/loading/post-card-skeleton.tsx +30 -0
- package/src/plugins/blog/client/components/loading/post-page-skeleton.tsx +75 -0
- package/src/plugins/blog/client/components/pages/404-page.tsx +23 -0
- package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +60 -0
- package/src/plugins/blog/client/components/pages/edit-post-page.tsx +40 -0
- package/src/plugins/blog/client/components/pages/home-page.internal.tsx +71 -0
- package/src/plugins/blog/client/components/pages/home-page.tsx +42 -0
- package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +59 -0
- package/src/plugins/blog/client/components/pages/new-post-page.tsx +36 -0
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +142 -0
- package/src/plugins/blog/client/components/pages/post-page.tsx +38 -0
- package/src/plugins/blog/client/components/pages/tag-page.internal.tsx +74 -0
- package/src/plugins/blog/client/components/pages/tag-page.tsx +38 -0
- package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +19 -0
- package/src/plugins/blog/client/components/shared/default-error.tsx +20 -0
- package/src/plugins/blog/client/components/shared/defaults.tsx +9 -0
- package/src/plugins/blog/client/components/shared/empty-list.tsx +25 -0
- package/src/plugins/blog/client/components/shared/error-placeholder.tsx +20 -0
- package/src/plugins/blog/client/components/shared/highlight-text.tsx +80 -0
- package/src/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
- package/src/plugins/blog/client/components/shared/markdown-content.tsx +448 -0
- package/src/plugins/blog/client/components/shared/on-this-page.tsx +234 -0
- package/src/plugins/blog/client/components/shared/page-header.tsx +35 -0
- package/src/plugins/blog/client/components/shared/page-layout.tsx +23 -0
- package/src/plugins/blog/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/blog/client/components/shared/post-card.tsx +308 -0
- package/src/plugins/blog/client/components/shared/post-navigation.tsx +98 -0
- package/src/plugins/blog/client/components/shared/posts-list.tsx +67 -0
- package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +79 -0
- package/src/plugins/blog/client/components/shared/search-input.tsx +146 -0
- package/src/plugins/blog/client/components/shared/search-modal.tsx +162 -0
- package/src/plugins/blog/client/components/shared/tags-list.tsx +34 -0
- package/src/plugins/blog/client/components/shared/use-route-lifecycle.tsx +68 -0
- package/src/plugins/blog/client/hooks/blog-hooks.tsx +623 -0
- package/src/plugins/blog/client/hooks/index.tsx +1 -0
- package/src/plugins/blog/client/hooks/use-debounce.ts +43 -0
- package/src/plugins/blog/client/index.ts +9 -0
- package/src/plugins/blog/client/localization/blog-card.ts +3 -0
- package/src/plugins/blog/client/localization/blog-common.ts +7 -0
- package/src/plugins/blog/client/localization/blog-forms.ts +45 -0
- package/src/plugins/blog/client/localization/blog-list.ts +14 -0
- package/src/plugins/blog/client/localization/blog-post.ts +9 -0
- package/src/plugins/blog/client/localization/index.ts +15 -0
- package/src/plugins/blog/client/overrides.ts +123 -0
- package/src/plugins/blog/client/plugin.tsx +672 -0
- package/src/plugins/blog/client.css +3 -0
- package/src/plugins/blog/db.ts +90 -0
- package/src/plugins/blog/query-keys.ts +267 -0
- package/src/plugins/blog/schemas.ts +39 -0
- package/src/plugins/blog/style.css +22 -0
- package/src/plugins/blog/types.ts +37 -0
- package/src/plugins/blog/utils.ts +144 -0
- package/src/plugins/client/index.ts +53 -0
- package/src/plugins/index.ts +0 -0
- package/src/plugins/utils.ts +35 -0
- package/src/types.ts +209 -0
- package/dist/plugins/index.cjs +0 -15
- package/dist/plugins/index.d.cts +0 -64
- package/dist/plugins/index.d.mts +0 -64
- package/dist/plugins/index.d.ts +0 -64
- package/dist/plugins/index.mjs +0 -11
- package/dist/shared/stack.DrUAVfIH.d.cts +0 -17
- package/dist/shared/stack.DrUAVfIH.d.mts +0 -17
- package/dist/shared/stack.DrUAVfIH.d.ts +0 -17
- /package/dist/{shared/stack.CktCg4PJ.cjs → plugins/utils.cjs} +0 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineClientPlugin,
|
|
3
|
+
createApiClient,
|
|
4
|
+
} from "@btst/stack/plugins/client";
|
|
5
|
+
import { createRoute } from "@btst/yar";
|
|
6
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
7
|
+
import type { BlogApiRouter } from "../api";
|
|
8
|
+
import { createBlogQueryKeys } from "../query-keys";
|
|
9
|
+
import type { Post, SerializedPost, SerializedTag } from "../types";
|
|
10
|
+
import { HomePageComponent } from "./components/pages/home-page";
|
|
11
|
+
import { NewPostPageComponent } from "./components/pages/new-post-page";
|
|
12
|
+
import { EditPostPageComponent } from "./components/pages/edit-post-page";
|
|
13
|
+
import { TagPageComponent } from "./components/pages/tag-page";
|
|
14
|
+
import { PostPageComponent } from "./components/pages/post-page";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Context passed to route hooks
|
|
18
|
+
*/
|
|
19
|
+
export interface RouteContext {
|
|
20
|
+
path: string;
|
|
21
|
+
params?: Record<string, string>;
|
|
22
|
+
isSSR: boolean;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Context passed to loader hooks
|
|
28
|
+
*/
|
|
29
|
+
export interface LoaderContext {
|
|
30
|
+
path: string;
|
|
31
|
+
params?: Record<string, string>;
|
|
32
|
+
isSSR: boolean;
|
|
33
|
+
apiBaseURL: string;
|
|
34
|
+
apiBasePath: string;
|
|
35
|
+
[key: string]: any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for blog client plugin
|
|
40
|
+
* Note: queryClient is passed at runtime to both loader and meta (for SSR isolation)
|
|
41
|
+
*/
|
|
42
|
+
export interface BlogClientConfig {
|
|
43
|
+
// Required configuration
|
|
44
|
+
apiBaseURL: string;
|
|
45
|
+
apiBasePath: string;
|
|
46
|
+
siteBaseURL: string;
|
|
47
|
+
siteBasePath: string;
|
|
48
|
+
queryClient: QueryClient;
|
|
49
|
+
|
|
50
|
+
// Optional SEO/meta configuration
|
|
51
|
+
seo?: {
|
|
52
|
+
siteName?: string;
|
|
53
|
+
author?: string;
|
|
54
|
+
twitterHandle?: string;
|
|
55
|
+
locale?: string;
|
|
56
|
+
defaultImage?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Optional hooks
|
|
60
|
+
hooks?: BlogClientHooks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hooks for blog client plugin
|
|
65
|
+
* All hooks are optional and allow consumers to customize behavior
|
|
66
|
+
*/
|
|
67
|
+
export interface BlogClientHooks {
|
|
68
|
+
// Loader Hooks - called during data loading (SSR or CSR)
|
|
69
|
+
beforeLoadPosts?: (
|
|
70
|
+
filter: { published: boolean },
|
|
71
|
+
context: LoaderContext,
|
|
72
|
+
) => Promise<boolean> | boolean;
|
|
73
|
+
afterLoadPosts?: (
|
|
74
|
+
posts: Post[] | null,
|
|
75
|
+
filter: { published: boolean },
|
|
76
|
+
context: LoaderContext,
|
|
77
|
+
) => Promise<void> | void;
|
|
78
|
+
beforeLoadPost?: (
|
|
79
|
+
slug: string,
|
|
80
|
+
context: LoaderContext,
|
|
81
|
+
) => Promise<boolean> | boolean;
|
|
82
|
+
afterLoadPost?: (
|
|
83
|
+
post: Post | null,
|
|
84
|
+
slug: string,
|
|
85
|
+
context: LoaderContext,
|
|
86
|
+
) => Promise<void> | void;
|
|
87
|
+
onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Loader for SSR prefetching with hooks - configured once
|
|
91
|
+
function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
92
|
+
return async () => {
|
|
93
|
+
if (typeof window === "undefined") {
|
|
94
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
|
|
95
|
+
|
|
96
|
+
const context: LoaderContext = {
|
|
97
|
+
path: published ? "/blog" : "/blog/drafts",
|
|
98
|
+
isSSR: true,
|
|
99
|
+
apiBaseURL,
|
|
100
|
+
apiBasePath,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Before hook
|
|
105
|
+
if (hooks?.beforeLoadPosts) {
|
|
106
|
+
const canLoad = await hooks.beforeLoadPosts({ published }, context);
|
|
107
|
+
if (!canLoad) {
|
|
108
|
+
throw new Error("Load prevented by beforeLoadPosts hook");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const limit = 10;
|
|
113
|
+
const client = createApiClient<BlogApiRouter>({
|
|
114
|
+
baseURL: apiBaseURL,
|
|
115
|
+
basePath: apiBasePath,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// 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);
|
|
120
|
+
const listQuery = queries.posts.list({
|
|
121
|
+
query: undefined,
|
|
122
|
+
limit,
|
|
123
|
+
published: published,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await queryClient.prefetchInfiniteQuery({
|
|
127
|
+
...listQuery,
|
|
128
|
+
initialPageParam: 0,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Prefetch tags
|
|
132
|
+
const tagsQuery = queries.tags.list();
|
|
133
|
+
await queryClient.prefetchQuery(tagsQuery);
|
|
134
|
+
|
|
135
|
+
// Don't throw errors during SSR - let Error Boundaries catch them when components render
|
|
136
|
+
// React Query stores errors in query state, and Suspense/Error Boundaries handle them
|
|
137
|
+
// Note: We still call hooks so consumers can log/track errors
|
|
138
|
+
|
|
139
|
+
// After hook - get data from queryClient if needed
|
|
140
|
+
if (hooks?.afterLoadPosts) {
|
|
141
|
+
const posts =
|
|
142
|
+
queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
|
|
143
|
+
await hooks.afterLoadPosts(posts, { published }, context);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if there was an error after afterLoadPosts hook
|
|
147
|
+
const queryState = queryClient.getQueryState(listQuery.queryKey);
|
|
148
|
+
if (queryState?.error) {
|
|
149
|
+
// Call error hook but don't throw - let Error Boundary handle it during render
|
|
150
|
+
if (hooks?.onLoadError) {
|
|
151
|
+
const error =
|
|
152
|
+
queryState.error instanceof Error
|
|
153
|
+
? queryState.error
|
|
154
|
+
: new Error(String(queryState.error));
|
|
155
|
+
await hooks.onLoadError(error, context);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Error hook - log the error but don't throw during SSR
|
|
160
|
+
// Let Error Boundaries handle errors when components render
|
|
161
|
+
if (hooks?.onLoadError) {
|
|
162
|
+
await hooks.onLoadError(error as Error, context);
|
|
163
|
+
}
|
|
164
|
+
// Don't re-throw - let Error Boundary catch it during render
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function createPostLoader(slug: string, config: BlogClientConfig) {
|
|
171
|
+
return async () => {
|
|
172
|
+
if (typeof window === "undefined") {
|
|
173
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
|
|
174
|
+
|
|
175
|
+
const context: LoaderContext = {
|
|
176
|
+
path: `/blog/${slug}`,
|
|
177
|
+
params: { slug },
|
|
178
|
+
isSSR: true,
|
|
179
|
+
apiBaseURL,
|
|
180
|
+
apiBasePath,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// Before hook
|
|
185
|
+
if (hooks?.beforeLoadPost) {
|
|
186
|
+
const canLoad = await hooks.beforeLoadPost(slug, context);
|
|
187
|
+
if (!canLoad) {
|
|
188
|
+
throw new Error("Load prevented by beforeLoadPost hook");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const client = createApiClient<BlogApiRouter>({
|
|
193
|
+
baseURL: apiBaseURL,
|
|
194
|
+
basePath: apiBasePath,
|
|
195
|
+
});
|
|
196
|
+
const queries = createBlogQueryKeys(client);
|
|
197
|
+
const postQuery = queries.posts.detail(slug);
|
|
198
|
+
await queryClient.prefetchQuery(postQuery);
|
|
199
|
+
|
|
200
|
+
// Don't throw errors during SSR - let Error Boundaries catch them when components render
|
|
201
|
+
// React Query stores errors in query state, and Suspense/Error Boundaries handle them
|
|
202
|
+
// Note: We still call hooks so consumers can log/track errors
|
|
203
|
+
|
|
204
|
+
// After hook
|
|
205
|
+
if (hooks?.afterLoadPost) {
|
|
206
|
+
const post =
|
|
207
|
+
queryClient.getQueryData<Post>(postQuery.queryKey) || null;
|
|
208
|
+
await hooks.afterLoadPost(post, slug, context);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if there was an error after afterLoadPost hook
|
|
212
|
+
const queryState = queryClient.getQueryState(postQuery.queryKey);
|
|
213
|
+
if (queryState?.error) {
|
|
214
|
+
// Call error hook but don't throw - let Error Boundary handle it during render
|
|
215
|
+
if (hooks?.onLoadError) {
|
|
216
|
+
const error =
|
|
217
|
+
queryState.error instanceof Error
|
|
218
|
+
? queryState.error
|
|
219
|
+
: new Error(String(queryState.error));
|
|
220
|
+
await hooks.onLoadError(error, context);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// Error hook - log the error but don't throw during SSR
|
|
225
|
+
// Let Error Boundaries handle errors when components render
|
|
226
|
+
if (hooks?.onLoadError) {
|
|
227
|
+
await hooks.onLoadError(error as Error, context);
|
|
228
|
+
}
|
|
229
|
+
// Don't re-throw - let Error Boundary catch it during render
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function createTagLoader(tagSlug: string, config: BlogClientConfig) {
|
|
236
|
+
return async () => {
|
|
237
|
+
if (typeof window === "undefined") {
|
|
238
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks } = config;
|
|
239
|
+
|
|
240
|
+
const context: LoaderContext = {
|
|
241
|
+
path: `/blog/tag/${tagSlug}`,
|
|
242
|
+
params: { tagSlug },
|
|
243
|
+
isSSR: true,
|
|
244
|
+
apiBaseURL,
|
|
245
|
+
apiBasePath,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const limit = 10;
|
|
250
|
+
const client = createApiClient<BlogApiRouter>({
|
|
251
|
+
baseURL: apiBaseURL,
|
|
252
|
+
basePath: apiBasePath,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const queries = createBlogQueryKeys(client);
|
|
256
|
+
const listQuery = queries.posts.list({
|
|
257
|
+
query: undefined,
|
|
258
|
+
limit,
|
|
259
|
+
published: true,
|
|
260
|
+
tagSlug: tagSlug,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await queryClient.prefetchInfiniteQuery({
|
|
264
|
+
...listQuery,
|
|
265
|
+
initialPageParam: 0,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const tagsQuery = queries.tags.list();
|
|
269
|
+
await queryClient.prefetchQuery(tagsQuery);
|
|
270
|
+
|
|
271
|
+
if (hooks?.onLoadError) {
|
|
272
|
+
const queryState = queryClient.getQueryState(listQuery.queryKey);
|
|
273
|
+
if (queryState?.error) {
|
|
274
|
+
const error =
|
|
275
|
+
queryState.error instanceof Error
|
|
276
|
+
? queryState.error
|
|
277
|
+
: new Error(String(queryState.error));
|
|
278
|
+
await hooks.onLoadError(error, context);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (hooks?.onLoadError) {
|
|
283
|
+
await hooks.onLoadError(error as Error, context);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Meta generators with SEO optimization
|
|
291
|
+
function createPostsListMeta(published: boolean, config: BlogClientConfig) {
|
|
292
|
+
return () => {
|
|
293
|
+
const { siteBaseURL, siteBasePath, seo } = config;
|
|
294
|
+
const path = published ? "/blog" : "/blog/drafts";
|
|
295
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}${path}`;
|
|
296
|
+
const title = published ? "Blog" : "Draft Posts";
|
|
297
|
+
const description = published
|
|
298
|
+
? "Read our latest articles, insights, and updates on web development, technology, and more."
|
|
299
|
+
: "View and manage your draft blog posts.";
|
|
300
|
+
|
|
301
|
+
return [
|
|
302
|
+
// Primary meta tags
|
|
303
|
+
{ title },
|
|
304
|
+
{ name: "title", content: title },
|
|
305
|
+
{ name: "description", content: description },
|
|
306
|
+
{
|
|
307
|
+
name: "keywords",
|
|
308
|
+
content: "blog, articles, technology, web development, insights",
|
|
309
|
+
},
|
|
310
|
+
...(seo?.author ? [{ name: "author", content: seo.author }] : []),
|
|
311
|
+
{
|
|
312
|
+
name: "robots",
|
|
313
|
+
content: published ? "index, follow" : "noindex, nofollow",
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
// Open Graph / Facebook
|
|
317
|
+
{ property: "og:type", content: "website" },
|
|
318
|
+
{ property: "og:title", content: title },
|
|
319
|
+
{ property: "og:description", content: description },
|
|
320
|
+
{ property: "og:url", content: fullUrl },
|
|
321
|
+
...(seo?.siteName
|
|
322
|
+
? [{ property: "og:site_name", content: seo.siteName }]
|
|
323
|
+
: []),
|
|
324
|
+
...(seo?.locale ? [{ property: "og:locale", content: seo.locale }] : []),
|
|
325
|
+
...(seo?.defaultImage
|
|
326
|
+
? [{ property: "og:image", content: seo.defaultImage }]
|
|
327
|
+
: []),
|
|
328
|
+
|
|
329
|
+
// Twitter Card
|
|
330
|
+
{ name: "twitter:card", content: "summary_large_image" },
|
|
331
|
+
{ name: "twitter:title", content: title },
|
|
332
|
+
{ name: "twitter:description", content: description },
|
|
333
|
+
...(seo?.twitterHandle
|
|
334
|
+
? [{ name: "twitter:site", content: seo.twitterHandle }]
|
|
335
|
+
: []),
|
|
336
|
+
];
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function createPostMeta(slug: string, config: BlogClientConfig) {
|
|
341
|
+
return () => {
|
|
342
|
+
// Use queryClient passed at runtime (same as loader!)
|
|
343
|
+
const { queryClient } = config;
|
|
344
|
+
const { apiBaseURL, apiBasePath, siteBaseURL, siteBasePath, seo } = config;
|
|
345
|
+
const queries = createBlogQueryKeys(
|
|
346
|
+
createApiClient<BlogApiRouter>({
|
|
347
|
+
baseURL: apiBaseURL,
|
|
348
|
+
basePath: apiBasePath,
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
const post = queryClient.getQueryData<Post>(
|
|
352
|
+
queries.posts.detail(slug).queryKey,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (!post) {
|
|
356
|
+
// Fallback if post not loaded
|
|
357
|
+
return [
|
|
358
|
+
{ title: "Unknown route" },
|
|
359
|
+
{ name: "title", content: "Unknown route" },
|
|
360
|
+
{ name: "robots", content: "noindex" },
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/blog/${post.slug}`;
|
|
365
|
+
const title = post.title;
|
|
366
|
+
const description = post.excerpt || post.content.substring(0, 160);
|
|
367
|
+
const publishedTime = post.publishedAt
|
|
368
|
+
? new Date(post.publishedAt).toISOString()
|
|
369
|
+
: new Date(post.createdAt).toISOString();
|
|
370
|
+
const modifiedTime = new Date(post.updatedAt).toISOString();
|
|
371
|
+
const image = post.image || seo?.defaultImage;
|
|
372
|
+
|
|
373
|
+
return [
|
|
374
|
+
// Primary meta tags
|
|
375
|
+
{ title },
|
|
376
|
+
{ name: "title", content: title },
|
|
377
|
+
{ name: "description", content: description },
|
|
378
|
+
...(post.authorId || seo?.author
|
|
379
|
+
? [{ name: "author", content: post.authorId || seo?.author }]
|
|
380
|
+
: []),
|
|
381
|
+
{
|
|
382
|
+
name: "robots",
|
|
383
|
+
content: post.published ? "index, follow" : "noindex, nofollow",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "keywords",
|
|
387
|
+
content: `blog, article, ${post.slug.replace(/-/g, ", ")}`,
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
// Open Graph / Facebook
|
|
391
|
+
{ property: "og:type", content: "article" },
|
|
392
|
+
{ property: "og:title", content: title },
|
|
393
|
+
{ property: "og:description", content: description },
|
|
394
|
+
{ property: "og:url", content: fullUrl },
|
|
395
|
+
...(seo?.siteName
|
|
396
|
+
? [{ property: "og:site_name", content: seo.siteName }]
|
|
397
|
+
: []),
|
|
398
|
+
...(seo?.locale ? [{ property: "og:locale", content: seo.locale }] : []),
|
|
399
|
+
...(image ? [{ property: "og:image", content: image }] : []),
|
|
400
|
+
...(image
|
|
401
|
+
? [
|
|
402
|
+
{ property: "og:image:width", content: "1200" },
|
|
403
|
+
{ property: "og:image:height", content: "630" },
|
|
404
|
+
{ property: "og:image:alt", content: title },
|
|
405
|
+
]
|
|
406
|
+
: []),
|
|
407
|
+
|
|
408
|
+
// Article-specific Open Graph tags
|
|
409
|
+
{ property: "article:published_time", content: publishedTime },
|
|
410
|
+
{ property: "article:modified_time", content: modifiedTime },
|
|
411
|
+
...(post.authorId
|
|
412
|
+
? [{ property: "article:author", content: post.authorId }]
|
|
413
|
+
: []),
|
|
414
|
+
|
|
415
|
+
// Twitter Card
|
|
416
|
+
{
|
|
417
|
+
name: "twitter:card",
|
|
418
|
+
content: image ? "summary_large_image" : "summary",
|
|
419
|
+
},
|
|
420
|
+
{ name: "twitter:title", content: title },
|
|
421
|
+
{ name: "twitter:description", content: description },
|
|
422
|
+
...(seo?.twitterHandle
|
|
423
|
+
? [{ name: "twitter:site", content: seo.twitterHandle }]
|
|
424
|
+
: []),
|
|
425
|
+
...(post.authorId || seo?.twitterHandle
|
|
426
|
+
? [
|
|
427
|
+
{
|
|
428
|
+
name: "twitter:creator",
|
|
429
|
+
content: post.authorId || seo?.twitterHandle,
|
|
430
|
+
},
|
|
431
|
+
]
|
|
432
|
+
: []),
|
|
433
|
+
...(image ? [{ name: "twitter:image", content: image }] : []),
|
|
434
|
+
...(image ? [{ name: "twitter:image:alt", content: title }] : []),
|
|
435
|
+
|
|
436
|
+
// Additional SEO tags
|
|
437
|
+
{ name: "publish_date", content: publishedTime },
|
|
438
|
+
];
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function createTagMeta(tagSlug: string, config: BlogClientConfig) {
|
|
443
|
+
return () => {
|
|
444
|
+
const { queryClient } = config;
|
|
445
|
+
const { apiBaseURL, apiBasePath, siteBaseURL, siteBasePath, seo } = config;
|
|
446
|
+
const queries = createBlogQueryKeys(
|
|
447
|
+
createApiClient<BlogApiRouter>({
|
|
448
|
+
baseURL: apiBaseURL,
|
|
449
|
+
basePath: apiBasePath,
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
const tags = queryClient.getQueryData<SerializedTag[]>(
|
|
453
|
+
queries.tags.list().queryKey,
|
|
454
|
+
);
|
|
455
|
+
const tag = tags?.find((t) => t.slug === tagSlug);
|
|
456
|
+
|
|
457
|
+
if (!tag) {
|
|
458
|
+
return [
|
|
459
|
+
{ title: "Unknown route" },
|
|
460
|
+
{ name: "title", content: "Unknown route" },
|
|
461
|
+
{ name: "robots", content: "noindex" },
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/blog/tag/${tag.slug}`;
|
|
466
|
+
const title = `${tag.name} Posts`;
|
|
467
|
+
const description = `Browse all ${tag.name} posts`;
|
|
468
|
+
|
|
469
|
+
return [
|
|
470
|
+
{ title },
|
|
471
|
+
{ name: "title", content: title },
|
|
472
|
+
{ name: "description", content: description },
|
|
473
|
+
{ name: "robots", content: "index, follow" },
|
|
474
|
+
{ name: "keywords", content: `blog, ${tag.name}, articles` },
|
|
475
|
+
{ property: "og:type", content: "website" },
|
|
476
|
+
{ property: "og:title", content: title },
|
|
477
|
+
{ property: "og:description", content: description },
|
|
478
|
+
{ property: "og:url", content: fullUrl },
|
|
479
|
+
...(seo?.siteName
|
|
480
|
+
? [{ property: "og:site_name", content: seo.siteName }]
|
|
481
|
+
: []),
|
|
482
|
+
...(seo?.defaultImage
|
|
483
|
+
? [{ property: "og:image", content: seo.defaultImage }]
|
|
484
|
+
: []),
|
|
485
|
+
{ name: "twitter:card", content: "summary" },
|
|
486
|
+
{ name: "twitter:title", content: title },
|
|
487
|
+
];
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function createNewPostMeta(config: BlogClientConfig) {
|
|
492
|
+
return () => {
|
|
493
|
+
const { siteBaseURL, siteBasePath } = config;
|
|
494
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/blog/new`;
|
|
495
|
+
|
|
496
|
+
const title = "Create New Post";
|
|
497
|
+
|
|
498
|
+
return [
|
|
499
|
+
{ title },
|
|
500
|
+
{ name: "title", content: title },
|
|
501
|
+
{ name: "description", content: "Write and publish a new blog post." },
|
|
502
|
+
{ name: "robots", content: "noindex, nofollow" },
|
|
503
|
+
|
|
504
|
+
// Open Graph
|
|
505
|
+
{ property: "og:type", content: "website" },
|
|
506
|
+
{ property: "og:title", content: title },
|
|
507
|
+
{
|
|
508
|
+
property: "og:description",
|
|
509
|
+
content: "Write and publish a new blog post.",
|
|
510
|
+
},
|
|
511
|
+
{ property: "og:url", content: fullUrl },
|
|
512
|
+
|
|
513
|
+
// Twitter
|
|
514
|
+
{ name: "twitter:card", content: "summary" },
|
|
515
|
+
{ name: "twitter:title", content: title },
|
|
516
|
+
];
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function createEditPostMeta(slug: string, config: BlogClientConfig) {
|
|
521
|
+
return () => {
|
|
522
|
+
// Use queryClient passed at runtime (same as loader!)
|
|
523
|
+
const { queryClient } = config;
|
|
524
|
+
const { apiBaseURL, apiBasePath, siteBaseURL, siteBasePath } = config;
|
|
525
|
+
const queries = createBlogQueryKeys(
|
|
526
|
+
createApiClient<BlogApiRouter>({
|
|
527
|
+
baseURL: apiBaseURL,
|
|
528
|
+
basePath: apiBasePath,
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
const post = queryClient.getQueryData<Post>(
|
|
532
|
+
queries.posts.detail(slug).queryKey,
|
|
533
|
+
);
|
|
534
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/blog/${slug}/edit`;
|
|
535
|
+
|
|
536
|
+
const title = post ? `Edit: ${post.title}` : "Unknown route";
|
|
537
|
+
|
|
538
|
+
return [
|
|
539
|
+
{ title },
|
|
540
|
+
{ name: "title", content: title },
|
|
541
|
+
{ name: "description", content: "Edit your blog post." },
|
|
542
|
+
{ name: "robots", content: "noindex, nofollow" },
|
|
543
|
+
|
|
544
|
+
// Open Graph
|
|
545
|
+
{ property: "og:type", content: "website" },
|
|
546
|
+
{ property: "og:title", content: title },
|
|
547
|
+
{ property: "og:url", content: fullUrl },
|
|
548
|
+
|
|
549
|
+
// Twitter
|
|
550
|
+
{ name: "twitter:card", content: "summary" },
|
|
551
|
+
{ name: "twitter:title", content: title },
|
|
552
|
+
];
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Blog client plugin
|
|
558
|
+
* Provides routes, components, and React Query hooks for blog posts
|
|
559
|
+
*
|
|
560
|
+
* @param config - Configuration including queryClient, baseURL, and optional hooks
|
|
561
|
+
*/
|
|
562
|
+
export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
563
|
+
defineClientPlugin({
|
|
564
|
+
name: "blog",
|
|
565
|
+
|
|
566
|
+
routes: () => ({
|
|
567
|
+
posts: createRoute("/blog", () => {
|
|
568
|
+
return {
|
|
569
|
+
PageComponent: () => <HomePageComponent published={true} />,
|
|
570
|
+
loader: createPostsLoader(true, config),
|
|
571
|
+
meta: createPostsListMeta(true, config),
|
|
572
|
+
};
|
|
573
|
+
}),
|
|
574
|
+
drafts: createRoute("/blog/drafts", () => {
|
|
575
|
+
return {
|
|
576
|
+
PageComponent: () => <HomePageComponent published={false} />,
|
|
577
|
+
loader: createPostsLoader(false, config),
|
|
578
|
+
meta: createPostsListMeta(false, config),
|
|
579
|
+
};
|
|
580
|
+
}),
|
|
581
|
+
newPost: createRoute("/blog/new", () => {
|
|
582
|
+
return {
|
|
583
|
+
PageComponent: NewPostPageComponent,
|
|
584
|
+
meta: createNewPostMeta(config),
|
|
585
|
+
};
|
|
586
|
+
}),
|
|
587
|
+
editPost: createRoute("/blog/:slug/edit", ({ params: { slug } }) => {
|
|
588
|
+
return {
|
|
589
|
+
PageComponent: () => <EditPostPageComponent slug={slug} />,
|
|
590
|
+
loader: createPostLoader(slug, config),
|
|
591
|
+
meta: createEditPostMeta(slug, config),
|
|
592
|
+
};
|
|
593
|
+
}),
|
|
594
|
+
tag: createRoute("/blog/tag/:tagSlug", ({ params: { tagSlug } }) => {
|
|
595
|
+
return {
|
|
596
|
+
PageComponent: () => <TagPageComponent tagSlug={tagSlug} />,
|
|
597
|
+
loader: createTagLoader(tagSlug, config),
|
|
598
|
+
meta: createTagMeta(tagSlug, config),
|
|
599
|
+
};
|
|
600
|
+
}),
|
|
601
|
+
post: createRoute("/blog/:slug", ({ params: { slug } }) => {
|
|
602
|
+
return {
|
|
603
|
+
PageComponent: () => <PostPageComponent slug={slug} />,
|
|
604
|
+
loader: createPostLoader(slug, config),
|
|
605
|
+
meta: createPostMeta(slug, config),
|
|
606
|
+
};
|
|
607
|
+
}),
|
|
608
|
+
}),
|
|
609
|
+
|
|
610
|
+
sitemap: async () => {
|
|
611
|
+
const origin = `${config.siteBaseURL}${config.siteBasePath}`;
|
|
612
|
+
const indexUrl = `${origin}/blog`;
|
|
613
|
+
|
|
614
|
+
// Fetch all published posts via API, with pagination
|
|
615
|
+
const client = createApiClient<BlogApiRouter>({
|
|
616
|
+
baseURL: config.apiBaseURL,
|
|
617
|
+
basePath: config.apiBasePath,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const limit = 100;
|
|
621
|
+
let offset = 0;
|
|
622
|
+
const posts: SerializedPost[] = [];
|
|
623
|
+
// eslint-disable-next-line no-constant-condition
|
|
624
|
+
while (true) {
|
|
625
|
+
const res = await client("/posts", {
|
|
626
|
+
method: "GET",
|
|
627
|
+
query: {
|
|
628
|
+
offset,
|
|
629
|
+
limit,
|
|
630
|
+
published: "true",
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
const page = (res.data ?? []) as unknown as SerializedPost[];
|
|
634
|
+
posts.push(...page);
|
|
635
|
+
if (page.length < limit) break;
|
|
636
|
+
offset += limit;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const getLastModified = (p: SerializedPost): Date | undefined => {
|
|
640
|
+
const dates = [p.updatedAt, p.publishedAt, p.createdAt].filter(
|
|
641
|
+
Boolean,
|
|
642
|
+
) as string[];
|
|
643
|
+
if (dates.length === 0) return undefined;
|
|
644
|
+
const times = dates
|
|
645
|
+
.map((d) => new Date(d).getTime())
|
|
646
|
+
.filter((t) => !Number.isNaN(t));
|
|
647
|
+
if (times.length === 0) return undefined;
|
|
648
|
+
return new Date(Math.max(...times));
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const latestTime = posts
|
|
652
|
+
.map((p) => getLastModified(p)?.getTime() ?? 0)
|
|
653
|
+
.reduce((a, b) => Math.max(a, b), 0);
|
|
654
|
+
|
|
655
|
+
const entries = [
|
|
656
|
+
{
|
|
657
|
+
url: indexUrl,
|
|
658
|
+
lastModified: latestTime ? new Date(latestTime) : undefined,
|
|
659
|
+
changeFrequency: "daily" as const,
|
|
660
|
+
priority: 0.7,
|
|
661
|
+
},
|
|
662
|
+
...posts.map((p) => ({
|
|
663
|
+
url: `${origin}/blog/${p.slug}`,
|
|
664
|
+
lastModified: getLastModified(p),
|
|
665
|
+
changeFrequency: "monthly" as const,
|
|
666
|
+
priority: 0.6,
|
|
667
|
+
})),
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
return entries;
|
|
671
|
+
},
|
|
672
|
+
});
|