@btst/stack 2.2.0 → 2.4.0

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 (237) hide show
  1. package/dist/packages/stack/src/client/components/compose.cjs +1 -2
  2. package/dist/packages/stack/src/client/components/compose.mjs +1 -2
  3. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
  4. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +54 -7
  6. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +54 -7
  7. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
  8. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
  9. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
  10. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
  11. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
  12. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
  13. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
  14. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
  15. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
  16. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
  17. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +52 -1
  18. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +52 -1
  19. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
  20. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
  21. package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
  22. package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
  23. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
  24. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
  25. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
  26. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
  27. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
  28. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
  29. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
  30. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
  31. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
  32. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
  33. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +15 -0
  34. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +16 -1
  35. package/dist/packages/stack/src/plugins/cms/api/getters.cjs +10 -0
  36. package/dist/packages/stack/src/plugins/cms/api/getters.mjs +10 -1
  37. package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
  38. package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
  39. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +75 -0
  40. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +76 -1
  41. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
  42. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
  43. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +15 -0
  44. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
  45. package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +9 -0
  46. package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +9 -1
  47. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +62 -1
  48. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +63 -2
  49. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
  50. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
  51. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +15 -0
  52. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -1
  53. package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
  54. package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
  55. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +34 -1
  56. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +34 -1
  57. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
  58. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
  59. package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
  60. package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
  61. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
  62. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
  63. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +10 -0
  64. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +11 -1
  65. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
  66. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
  67. package/dist/packages/stack/src/plugins/utils.cjs +6 -0
  68. package/dist/packages/stack/src/plugins/utils.mjs +6 -1
  69. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  70. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  71. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  72. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  73. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  74. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  75. package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
  76. package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
  77. package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
  78. package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
  79. package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
  80. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  81. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  82. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  83. package/dist/plugins/ai-chat/client/index.d.cts +2 -2
  84. package/dist/plugins/ai-chat/client/index.d.mts +2 -2
  85. package/dist/plugins/ai-chat/client/index.d.ts +2 -2
  86. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  87. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  88. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  89. package/dist/plugins/blog/api/index.cjs +5 -0
  90. package/dist/plugins/blog/api/index.d.cts +19 -4
  91. package/dist/plugins/blog/api/index.d.mts +19 -4
  92. package/dist/plugins/blog/api/index.d.ts +19 -4
  93. package/dist/plugins/blog/api/index.mjs +2 -0
  94. package/dist/plugins/blog/client/hooks/index.d.cts +3 -3
  95. package/dist/plugins/blog/client/hooks/index.d.mts +3 -3
  96. package/dist/plugins/blog/client/hooks/index.d.ts +3 -3
  97. package/dist/plugins/blog/client/index.d.cts +1 -1
  98. package/dist/plugins/blog/client/index.d.mts +1 -1
  99. package/dist/plugins/blog/client/index.d.ts +1 -1
  100. package/dist/plugins/blog/query-keys.cjs +6 -5
  101. package/dist/plugins/blog/query-keys.d.cts +8 -387
  102. package/dist/plugins/blog/query-keys.d.mts +8 -387
  103. package/dist/plugins/blog/query-keys.d.ts +8 -387
  104. package/dist/plugins/blog/query-keys.mjs +6 -5
  105. package/dist/plugins/client/index.cjs +1 -0
  106. package/dist/plugins/client/index.d.cts +8 -1
  107. package/dist/plugins/client/index.d.mts +8 -1
  108. package/dist/plugins/client/index.d.ts +8 -1
  109. package/dist/plugins/client/index.mjs +1 -1
  110. package/dist/plugins/cms/api/index.cjs +8 -0
  111. package/dist/plugins/cms/api/index.d.cts +7 -219
  112. package/dist/plugins/cms/api/index.d.mts +7 -219
  113. package/dist/plugins/cms/api/index.d.ts +7 -219
  114. package/dist/plugins/cms/api/index.mjs +3 -1
  115. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  116. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  117. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  118. package/dist/plugins/cms/query-keys.cjs +2 -1
  119. package/dist/plugins/cms/query-keys.d.cts +5 -9
  120. package/dist/plugins/cms/query-keys.d.mts +5 -9
  121. package/dist/plugins/cms/query-keys.d.ts +5 -9
  122. package/dist/plugins/cms/query-keys.mjs +2 -1
  123. package/dist/plugins/form-builder/api/index.cjs +6 -0
  124. package/dist/plugins/form-builder/api/index.d.cts +7 -211
  125. package/dist/plugins/form-builder/api/index.d.mts +7 -211
  126. package/dist/plugins/form-builder/api/index.d.ts +7 -211
  127. package/dist/plugins/form-builder/api/index.mjs +2 -1
  128. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  129. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  130. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  131. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  132. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  133. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  134. package/dist/plugins/form-builder/query-keys.cjs +3 -2
  135. package/dist/plugins/form-builder/query-keys.d.cts +6 -6
  136. package/dist/plugins/form-builder/query-keys.d.mts +6 -6
  137. package/dist/plugins/form-builder/query-keys.d.ts +6 -6
  138. package/dist/plugins/form-builder/query-keys.mjs +3 -2
  139. package/dist/plugins/kanban/api/index.cjs +10 -0
  140. package/dist/plugins/kanban/api/index.d.cts +17 -392
  141. package/dist/plugins/kanban/api/index.d.mts +17 -392
  142. package/dist/plugins/kanban/api/index.d.ts +17 -392
  143. package/dist/plugins/kanban/api/index.mjs +3 -0
  144. package/dist/plugins/kanban/client/components/index.d.cts +1 -1
  145. package/dist/plugins/kanban/client/components/index.d.mts +1 -1
  146. package/dist/plugins/kanban/client/components/index.d.ts +1 -1
  147. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  148. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  149. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  150. package/dist/plugins/kanban/client/index.d.cts +1 -1
  151. package/dist/plugins/kanban/client/index.d.mts +1 -1
  152. package/dist/plugins/kanban/client/index.d.ts +1 -1
  153. package/dist/plugins/kanban/query-keys.cjs +2 -9
  154. package/dist/plugins/kanban/query-keys.d.cts +4 -16
  155. package/dist/plugins/kanban/query-keys.d.mts +4 -16
  156. package/dist/plugins/kanban/query-keys.d.ts +4 -16
  157. package/dist/plugins/kanban/query-keys.mjs +2 -9
  158. package/dist/plugins/ui-builder/index.d.cts +1 -1
  159. package/dist/plugins/ui-builder/index.d.mts +1 -1
  160. package/dist/plugins/ui-builder/index.d.ts +1 -1
  161. package/dist/shared/stack.B7ONvlD_.d.mts +293 -0
  162. package/dist/shared/{stack.BeSm90va.d.ts → stack.BEn34wW6.d.ts} +60 -2
  163. package/dist/shared/stack.BUkC2EsZ.d.cts +327 -0
  164. package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
  165. package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
  166. package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
  167. package/dist/shared/stack.BepFXT3w.d.mts +500 -0
  168. package/dist/shared/stack.CL8ts1Mu.d.ts +419 -0
  169. package/dist/shared/{stack.CXjzTMsb.d.cts → stack.CVDTkMoO.d.cts} +7 -1
  170. package/dist/shared/{stack.CXjzTMsb.d.mts → stack.CVDTkMoO.d.mts} +7 -1
  171. package/dist/shared/{stack.CXjzTMsb.d.ts → stack.CVDTkMoO.d.ts} +7 -1
  172. package/dist/shared/stack.CczspVn2.d.mts +327 -0
  173. package/dist/shared/stack.CgWzG5jH.d.ts +500 -0
  174. package/dist/shared/stack.D3GB6wKv.d.cts +500 -0
  175. package/dist/shared/stack.DASmUVjX.d.ts +327 -0
  176. package/dist/shared/{stack.QD1y_7NY.d.cts → stack.DJaKVY7v.d.cts} +1 -1
  177. package/dist/shared/{stack.QD1y_7NY.d.mts → stack.DJaKVY7v.d.mts} +1 -1
  178. package/dist/shared/{stack.QD1y_7NY.d.ts → stack.DJaKVY7v.d.ts} +1 -1
  179. package/dist/shared/{stack.Dg09R0oB.d.mts → stack.DTDxgFj8.d.mts} +60 -2
  180. package/dist/shared/{stack.CMh_EdxW.d.cts → stack.DWoCZff7.d.cts} +60 -2
  181. package/dist/shared/{stack.CIrIsc-A.d.cts → stack.DdI5W6MB.d.cts} +7 -1
  182. package/dist/shared/{stack.CIrIsc-A.d.mts → stack.DdI5W6MB.d.mts} +7 -1
  183. package/dist/shared/{stack.CIrIsc-A.d.ts → stack.DdI5W6MB.d.ts} +7 -1
  184. package/dist/shared/stack.Dk5r4W1F.d.mts +419 -0
  185. package/dist/shared/stack.Kq2-QzOC.d.ts +293 -0
  186. package/dist/shared/stack.heOA9gzA.d.cts +419 -0
  187. package/dist/shared/stack.kcdnD4gA.d.cts +293 -0
  188. package/package.json +16 -3
  189. package/src/client/components/compose.tsx +7 -4
  190. package/src/plugins/ai-chat/api/page-tools.ts +111 -0
  191. package/src/plugins/ai-chat/api/plugin.ts +180 -9
  192. package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
  193. package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
  194. package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
  195. package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
  196. package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
  197. package/src/plugins/ai-chat/schemas.ts +16 -0
  198. package/src/plugins/blog/api/index.ts +2 -0
  199. package/src/plugins/blog/api/plugin.ts +85 -0
  200. package/src/plugins/blog/api/query-key-defs.ts +46 -0
  201. package/src/plugins/blog/api/serializers.ts +27 -0
  202. package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
  203. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
  204. package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
  205. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
  206. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
  207. package/src/plugins/blog/client/plugin.tsx +19 -0
  208. package/src/plugins/blog/query-keys.ts +5 -7
  209. package/src/plugins/client/index.ts +1 -1
  210. package/src/plugins/cms/api/getters.ts +24 -0
  211. package/src/plugins/cms/api/index.ts +14 -1
  212. package/src/plugins/cms/api/mutations.ts +84 -0
  213. package/src/plugins/cms/api/plugin.ts +114 -0
  214. package/src/plugins/cms/api/query-key-defs.ts +53 -0
  215. package/src/plugins/cms/api/serializers.ts +12 -0
  216. package/src/plugins/cms/client/plugin.tsx +19 -0
  217. package/src/plugins/cms/query-keys.ts +2 -1
  218. package/src/plugins/form-builder/api/getters.ts +23 -0
  219. package/src/plugins/form-builder/api/index.ts +15 -2
  220. package/src/plugins/form-builder/api/plugin.ts +91 -0
  221. package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
  222. package/src/plugins/form-builder/api/serializers.ts +12 -0
  223. package/src/plugins/form-builder/client/plugin.tsx +19 -0
  224. package/src/plugins/form-builder/query-keys.ts +6 -2
  225. package/src/plugins/kanban/api/index.ts +9 -0
  226. package/src/plugins/kanban/api/mutations.ts +169 -0
  227. package/src/plugins/kanban/api/plugin.ts +61 -0
  228. package/src/plugins/kanban/api/query-key-defs.ts +54 -0
  229. package/src/plugins/kanban/api/serializers.ts +49 -0
  230. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
  231. package/src/plugins/kanban/client/plugin.tsx +13 -0
  232. package/src/plugins/kanban/query-keys.ts +2 -9
  233. package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
  234. package/src/plugins/utils.ts +19 -0
  235. package/dist/shared/{stack.BkYlUT_8.d.cts → stack.BQmuNl5p.d.cts} +6 -6
  236. package/dist/shared/{stack.BkYlUT_8.d.mts → stack.BQmuNl5p.d.mts} +6 -6
  237. package/dist/shared/{stack.BkYlUT_8.d.ts → stack.BQmuNl5p.d.ts} +6 -6
@@ -0,0 +1,27 @@
1
+ import type { Post, Tag, SerializedPost, SerializedTag } from "../types";
2
+
3
+ /**
4
+ * Serialize a Tag for SSR/SSG use (convert dates to strings).
5
+ * Pure function — no DB access, no hooks.
6
+ */
7
+ export function serializeTag(tag: Tag): SerializedTag {
8
+ return {
9
+ ...tag,
10
+ createdAt: tag.createdAt.toISOString(),
11
+ updatedAt: tag.updatedAt.toISOString(),
12
+ };
13
+ }
14
+
15
+ /**
16
+ * Serialize a Post (with tags) for SSR/SSG use (convert dates to strings).
17
+ * Pure function — no DB access, no hooks.
18
+ */
19
+ export function serializePost(post: Post & { tags: Tag[] }): SerializedPost {
20
+ return {
21
+ ...post,
22
+ createdAt: post.createdAt.toISOString(),
23
+ updatedAt: post.updatedAt.toISOString(),
24
+ publishedAt: post.publishedAt?.toISOString(),
25
+ tags: post.tags.map(serializeTag),
26
+ };
27
+ }
@@ -40,7 +40,7 @@ import {
40
40
 
41
41
  import { zodResolver } from "@hookform/resolvers/zod";
42
42
  import { Loader2 } from "lucide-react";
43
- import { lazy, memo, Suspense, useMemo, useState } from "react";
43
+ import { lazy, memo, Suspense, useEffect, useMemo, useState } from "react";
44
44
  import {
45
45
  type FieldPath,
46
46
  type SubmitHandler,
@@ -325,6 +325,10 @@ const CustomPostUpdateSchema = PostUpdateSchema.omit({
325
325
  type AddPostFormProps = {
326
326
  onClose: () => void;
327
327
  onSuccess: (post: { published: boolean }) => void;
328
+ /** Called once with the form instance so parent components can access form state */
329
+ onFormReady?: (
330
+ form: UseFormReturn<z.input<typeof CustomPostCreateSchema>>,
331
+ ) => void;
328
332
  };
329
333
 
330
334
  const addPostFormPropsAreEqual = (
@@ -333,10 +337,15 @@ const addPostFormPropsAreEqual = (
333
337
  ): boolean => {
334
338
  if (prevProps.onClose !== nextProps.onClose) return false;
335
339
  if (prevProps.onSuccess !== nextProps.onSuccess) return false;
340
+ if (prevProps.onFormReady !== nextProps.onFormReady) return false;
336
341
  return true;
337
342
  };
338
343
 
339
- const AddPostFormComponent = ({ onClose, onSuccess }: AddPostFormProps) => {
344
+ const AddPostFormComponent = ({
345
+ onClose,
346
+ onSuccess,
347
+ onFormReady,
348
+ }: AddPostFormProps) => {
340
349
  const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
341
350
  const { localization } = usePluginOverrides<
342
351
  BlogPluginOverrides,
@@ -393,6 +402,12 @@ const AddPostFormComponent = ({ onClose, onSuccess }: AddPostFormProps) => {
393
402
  },
394
403
  });
395
404
 
405
+ // Expose form instance to parent for AI context integration
406
+ useEffect(() => {
407
+ onFormReady?.(form);
408
+ // eslint-disable-next-line react-hooks/exhaustive-deps
409
+ }, []);
410
+
396
411
  return (
397
412
  <PostFormBody
398
413
  form={form}
@@ -417,6 +432,10 @@ type EditPostFormProps = {
417
432
  onClose: () => void;
418
433
  onSuccess: (post: { slug: string; published: boolean }) => void;
419
434
  onDelete?: () => void;
435
+ /** Called once with the form instance so parent components can access form state */
436
+ onFormReady?: (
437
+ form: UseFormReturn<z.input<typeof CustomPostUpdateSchema>>,
438
+ ) => void;
420
439
  };
421
440
 
422
441
  const editPostFormPropsAreEqual = (
@@ -427,6 +446,7 @@ const editPostFormPropsAreEqual = (
427
446
  if (prevProps.onClose !== nextProps.onClose) return false;
428
447
  if (prevProps.onSuccess !== nextProps.onSuccess) return false;
429
448
  if (prevProps.onDelete !== nextProps.onDelete) return false;
449
+ if (prevProps.onFormReady !== nextProps.onFormReady) return false;
430
450
  return true;
431
451
  };
432
452
 
@@ -435,6 +455,7 @@ const EditPostFormComponent = ({
435
455
  onClose,
436
456
  onSuccess,
437
457
  onDelete,
458
+ onFormReady,
438
459
  }: EditPostFormProps) => {
439
460
  const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
440
461
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -537,6 +558,12 @@ const EditPostFormComponent = ({
537
558
  values: initialData as z.input<typeof schema>,
538
559
  });
539
560
 
561
+ // Expose form instance to parent for AI context integration
562
+ useEffect(() => {
563
+ onFormReady?.(form);
564
+ // eslint-disable-next-line react-hooks/exhaustive-deps
565
+ }, []);
566
+
540
567
  if (!post) {
541
568
  return <EmptyList message={localization.BLOG_PAGE_NOT_FOUND_DESCRIPTION} />;
542
569
  }
@@ -7,6 +7,10 @@ import { PageWrapper } from "../shared/page-wrapper";
7
7
  import { BLOG_LOCALIZATION } from "../../localization";
8
8
  import type { BlogPluginOverrides } from "../../overrides";
9
9
  import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
10
+ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
11
+ import { useRef, useCallback } from "react";
12
+ import type { UseFormReturn } from "react-hook-form";
13
+ import { createFillBlogFormHandler } from "./fill-blog-form-handler";
10
14
 
11
15
  // Internal component with actual page content
12
16
  export function EditPostPage({ slug }: { slug: string }) {
@@ -36,6 +40,29 @@ export function EditPostPage({ slug }: { slug: string }) {
36
40
  },
37
41
  });
38
42
 
43
+ // Ref to capture the form instance from EditPostForm via onFormReady callback
44
+ const formRef = useRef<UseFormReturn<any> | null>(null);
45
+ const handleFormReady = useCallback((form: UseFormReturn<any>) => {
46
+ formRef.current = form;
47
+ }, []);
48
+
49
+ // Register AI context so the chat can fill in the edit form
50
+ useRegisterPageAIContext({
51
+ routeName: "blog-edit-post",
52
+ pageDescription: `User is editing a blog post (slug: "${slug}") in the admin editor.`,
53
+ suggestions: [
54
+ "Improve this post's title",
55
+ "Rewrite the intro paragraph",
56
+ "Suggest better tags",
57
+ ],
58
+ clientTools: {
59
+ fillBlogForm: createFillBlogFormHandler(
60
+ formRef,
61
+ "Form updated successfully",
62
+ ),
63
+ },
64
+ });
65
+
39
66
  const handleClose = () => {
40
67
  navigate(`${basePath}/blog`);
41
68
  };
@@ -61,6 +88,7 @@ export function EditPostPage({ slug }: { slug: string }) {
61
88
  onClose={handleClose}
62
89
  onSuccess={handleSuccess}
63
90
  onDelete={handleDelete}
91
+ onFormReady={handleFormReady}
64
92
  />
65
93
  </PageWrapper>
66
94
  );
@@ -0,0 +1,38 @@
1
+ import type { RefObject } from "react";
2
+ import type { UseFormReturn } from "react-hook-form";
3
+
4
+ /**
5
+ * Returns a `fillBlogForm` client tool handler bound to a form ref.
6
+ * Used by both the new-post and edit-post pages so the field-mapping
7
+ * logic stays in one place when the form schema changes.
8
+ */
9
+ export function createFillBlogFormHandler(
10
+ formRef: RefObject<UseFormReturn<any> | null>,
11
+ successMessage: string,
12
+ ) {
13
+ return async ({
14
+ title,
15
+ content,
16
+ excerpt,
17
+ tags,
18
+ }: {
19
+ title?: string;
20
+ content?: string;
21
+ excerpt?: string;
22
+ tags?: string[];
23
+ }) => {
24
+ const form = formRef.current;
25
+ if (!form) return { success: false, message: "Form not ready" };
26
+ if (title !== undefined)
27
+ form.setValue("title", title, { shouldValidate: true });
28
+ if (content !== undefined)
29
+ form.setValue("content", content, { shouldValidate: true });
30
+ if (excerpt !== undefined) form.setValue("excerpt", excerpt);
31
+ if (tags !== undefined)
32
+ form.setValue(
33
+ "tags",
34
+ tags.map((name: string) => ({ name })),
35
+ );
36
+ return { success: true, message: successMessage };
37
+ };
38
+ }
@@ -7,6 +7,10 @@ import { PageWrapper } from "../shared/page-wrapper";
7
7
  import type { BlogPluginOverrides } from "../../overrides";
8
8
  import { BLOG_LOCALIZATION } from "../../localization";
9
9
  import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
10
+ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
11
+ import { useRef, useCallback } from "react";
12
+ import type { UseFormReturn } from "react-hook-form";
13
+ import { createFillBlogFormHandler } from "./fill-blog-form-handler";
10
14
 
11
15
  // Internal component with actual page content
12
16
  export function NewPostPage() {
@@ -35,6 +39,30 @@ export function NewPostPage() {
35
39
  },
36
40
  });
37
41
 
42
+ // Ref to capture the form instance from AddPostForm via onFormReady callback
43
+ const formRef = useRef<UseFormReturn<any> | null>(null);
44
+ const handleFormReady = useCallback((form: UseFormReturn<any>) => {
45
+ formRef.current = form;
46
+ }, []);
47
+
48
+ // Register AI context so the chat can fill in the new post form
49
+ useRegisterPageAIContext({
50
+ routeName: "blog-new-post",
51
+ pageDescription:
52
+ "User is creating a new blog post in the admin editor. IMPORTANT: When asked to write, draft, or create a blog post, you MUST call the fillBlogForm tool to populate the form fields directly — do NOT just output the text in your response.",
53
+ suggestions: [
54
+ "Write a post about AI trends",
55
+ "Draft an intro paragraph",
56
+ "Suggest 5 tags for this post",
57
+ ],
58
+ clientTools: {
59
+ fillBlogForm: createFillBlogFormHandler(
60
+ formRef,
61
+ "Form filled successfully",
62
+ ),
63
+ },
64
+ });
65
+
38
66
  const handleClose = () => {
39
67
  navigate(`${basePath}/blog`);
40
68
  };
@@ -54,7 +82,11 @@ export function NewPostPage() {
54
82
  title={localization.BLOG_POST_ADD_TITLE}
55
83
  description={localization.BLOG_POST_ADD_DESCRIPTION}
56
84
  />
57
- <AddPostForm onClose={handleClose} onSuccess={handleSuccess} />
85
+ <AddPostForm
86
+ onClose={handleClose}
87
+ onSuccess={handleSuccess}
88
+ onFormReady={handleFormReady}
89
+ />
58
90
  </PageWrapper>
59
91
  );
60
92
  }
@@ -20,6 +20,7 @@ import { Badge } from "@workspace/ui/components/badge";
20
20
  import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
21
21
  import { OnThisPage, OnThisPageSelect } from "../shared/on-this-page";
22
22
  import type { SerializedPost } from "../../../types";
23
+ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
23
24
 
24
25
  // Internal component with actual page content
25
26
  export function PostPage({ slug }: { slug: string }) {
@@ -64,6 +65,25 @@ export function PostPage({ slug }: { slug: string }) {
64
65
  enabled: !!post,
65
66
  });
66
67
 
68
+ // Register page AI context so the chat can summarize and discuss this post
69
+ useRegisterPageAIContext(
70
+ post
71
+ ? {
72
+ routeName: "blog-post",
73
+ pageDescription:
74
+ `Blog post: "${post.title}"\nAuthor: ${post.authorId ?? "Unknown"}\n\n${post.content ?? ""}`.slice(
75
+ 0,
76
+ 16000,
77
+ ),
78
+ suggestions: [
79
+ "Summarize this post",
80
+ "What are the key takeaways?",
81
+ "Explain this in simpler terms",
82
+ ],
83
+ }
84
+ : null,
85
+ );
86
+
67
87
  if (!slug || !post) {
68
88
  return (
69
89
  <PageWrapper>
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  defineClientPlugin,
3
3
  createApiClient,
4
+ isConnectionError,
4
5
  } from "@btst/stack/plugins/client";
5
6
  import { createRoute } from "@btst/yar";
6
7
  import type { QueryClient } from "@tanstack/react-query";
@@ -226,6 +227,12 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
226
227
  } catch (error) {
227
228
  // Error hook - log the error but don't throw during SSR
228
229
  // Let Error Boundaries handle errors when components render
230
+ if (isConnectionError(error)) {
231
+ console.warn(
232
+ "[btst/blog] route.loader() failed — no server running at build time. " +
233
+ "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
234
+ );
235
+ }
229
236
  if (hooks?.onLoadError) {
230
237
  await hooks.onLoadError(error as Error, context);
231
238
  }
@@ -299,6 +306,12 @@ function createPostLoader(
299
306
  } catch (error) {
300
307
  // Error hook - log the error but don't throw during SSR
301
308
  // Let Error Boundaries handle errors when components render
309
+ if (isConnectionError(error)) {
310
+ console.warn(
311
+ "[btst/blog] route.loader() failed — no server running at build time. " +
312
+ "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
313
+ );
314
+ }
302
315
  if (hooks?.onLoadError) {
303
316
  await hooks.onLoadError(error as Error, context);
304
317
  }
@@ -398,6 +411,12 @@ function createTagLoader(tagSlug: string, config: BlogClientConfig) {
398
411
  await hooks.onLoadError(error, context);
399
412
  }
400
413
  } catch (error) {
414
+ if (isConnectionError(error)) {
415
+ console.warn(
416
+ "[btst/blog] route.loader() failed — no server running at build time. " +
417
+ "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
418
+ );
419
+ }
401
420
  if (hooks?.onLoadError) {
402
421
  await hooks.onLoadError(error as Error, context);
403
422
  }
@@ -5,6 +5,7 @@ import {
5
5
  import type { BlogApiRouter } from "./api";
6
6
  import { createApiClient } from "@btst/stack/plugins/client";
7
7
  import type { SerializedPost, SerializedTag } from "./types";
8
+ import { postsListDiscriminator } from "./api/query-key-defs";
8
9
 
9
10
  interface PostsListParams {
10
11
  query?: string;
@@ -71,15 +72,12 @@ function createPostsQueries(
71
72
  return createQueryKeys("posts", {
72
73
  list: (params?: PostsListParams) => ({
73
74
  queryKey: [
74
- {
75
- query:
76
- params?.query !== undefined && params?.query?.trim() === ""
77
- ? undefined
78
- : params?.query,
79
- limit: params?.limit ?? 10,
75
+ postsListDiscriminator({
80
76
  published: params?.published ?? true,
77
+ limit: params?.limit ?? 10,
81
78
  tagSlug: params?.tagSlug,
82
- },
79
+ query: params?.query,
80
+ }),
83
81
  ],
84
82
  queryFn: async ({ pageParam }: { pageParam?: number }) => {
85
83
  try {
@@ -18,7 +18,7 @@ export type {
18
18
  PluginOverrides,
19
19
  } from "../../types";
20
20
 
21
- export { createApiClient } from "../utils";
21
+ export { createApiClient, isConnectionError } from "../utils";
22
22
 
23
23
  // Re-export Yar types needed for plugins
24
24
  export type { Route } from "@btst/yar";
@@ -191,6 +191,30 @@ export async function getAllContentItems(
191
191
  };
192
192
  }
193
193
 
194
+ /**
195
+ * Retrieve a single content item by its ID.
196
+ * Returns null if the item is not found.
197
+ * Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
198
+ *
199
+ * @remarks **Security:** Authorization hooks are NOT called. The caller is
200
+ * responsible for any access-control checks before invoking this function.
201
+ *
202
+ * @param adapter - The database adapter
203
+ * @param id - The content item ID (UUID)
204
+ */
205
+ export async function getContentItemById(
206
+ adapter: Adapter,
207
+ id: string,
208
+ ): Promise<SerializedContentItemWithType | null> {
209
+ const item = await adapter.findOne<ContentItemWithType>({
210
+ model: "contentItem",
211
+ where: [{ field: "id", value: id, operator: "eq" as const }],
212
+ join: { contentType: true },
213
+ });
214
+ if (!item) return null;
215
+ return serializeContentItemWithType(item);
216
+ }
217
+
194
218
  /**
195
219
  * Retrieve a single content item by its slug within a content type.
196
220
  * Returns null if the content type or item is not found.
@@ -1,6 +1,19 @@
1
- export { cmsBackendPlugin, type CMSApiRouter } from "./plugin";
1
+ export {
2
+ cmsBackendPlugin,
3
+ type CMSApiRouter,
4
+ type CMSRouteKey,
5
+ } from "./plugin";
2
6
  export {
3
7
  getAllContentTypes,
4
8
  getAllContentItems,
5
9
  getContentItemBySlug,
10
+ getContentItemById,
11
+ serializeContentType,
12
+ serializeContentItem,
13
+ serializeContentItemWithType,
6
14
  } from "./getters";
15
+ export {
16
+ createCMSContentItem,
17
+ type CreateCMSContentItemInput,
18
+ } from "./mutations";
19
+ export { CMS_QUERY_KEYS } from "./query-key-defs";
@@ -0,0 +1,84 @@
1
+ import type { Adapter } from "@btst/db";
2
+ import type { ContentType, ContentItem } from "../types";
3
+ import { serializeContentItem } from "./getters";
4
+ import type { SerializedContentItem } from "../types";
5
+
6
+ /**
7
+ * Input for creating a new CMS content item.
8
+ */
9
+ export interface CreateCMSContentItemInput {
10
+ /** URL-safe slug for the item */
11
+ slug: string;
12
+ /** Arbitrary data payload — should match the content type schema */
13
+ data: Record<string, unknown>;
14
+ }
15
+
16
+ /**
17
+ * Create a new content item for a content type (looked up by slug).
18
+ *
19
+ * Bypasses Zod schema validation and relation processing — the caller is
20
+ * responsible for providing valid, relation-free data. For relation fields or
21
+ * schema validation, use the HTTP endpoint instead.
22
+ *
23
+ * Throws if the content type is not found or the slug is already taken within
24
+ * that content type.
25
+ *
26
+ * @remarks **Security:** No authorization hooks (`onBeforeCreate`, `onAfterCreate`)
27
+ * are called. The caller is responsible for any access-control checks before
28
+ * invoking this function.
29
+ *
30
+ * @param adapter - The database adapter
31
+ * @param contentTypeSlug - Slug of the target content type
32
+ * @param input - Item slug and data payload
33
+ */
34
+ export async function createCMSContentItem(
35
+ adapter: Adapter,
36
+ contentTypeSlug: string,
37
+ input: CreateCMSContentItemInput,
38
+ ): Promise<SerializedContentItem> {
39
+ const contentType = await adapter.findOne<ContentType>({
40
+ model: "contentType",
41
+ where: [
42
+ {
43
+ field: "slug",
44
+ value: contentTypeSlug,
45
+ operator: "eq" as const,
46
+ },
47
+ ],
48
+ });
49
+
50
+ if (!contentType) {
51
+ throw new Error(`Content type "${contentTypeSlug}" not found`);
52
+ }
53
+
54
+ const existing = await adapter.findOne<ContentItem>({
55
+ model: "contentItem",
56
+ where: [
57
+ {
58
+ field: "contentTypeId",
59
+ value: contentType.id,
60
+ operator: "eq" as const,
61
+ },
62
+ { field: "slug", value: input.slug, operator: "eq" as const },
63
+ ],
64
+ });
65
+
66
+ if (existing) {
67
+ throw new Error(
68
+ `Content item with slug "${input.slug}" already exists in type "${contentTypeSlug}"`,
69
+ );
70
+ }
71
+
72
+ const item = await adapter.create<ContentItem>({
73
+ model: "contentItem",
74
+ data: {
75
+ contentTypeId: contentType.id,
76
+ slug: input.slug,
77
+ data: JSON.stringify(input.data),
78
+ createdAt: new Date(),
79
+ updatedAt: new Date(),
80
+ },
81
+ });
82
+
83
+ return serializeContentItem(item);
84
+ }
@@ -25,10 +25,38 @@ import {
25
25
  getAllContentTypes,
26
26
  getAllContentItems,
27
27
  getContentItemBySlug,
28
+ getContentItemById,
28
29
  serializeContentType,
29
30
  serializeContentItem,
30
31
  serializeContentItemWithType,
31
32
  } from "./getters";
33
+ import { createCMSContentItem } from "./mutations";
34
+ import type { QueryClient } from "@tanstack/react-query";
35
+ import { CMS_QUERY_KEYS } from "./query-key-defs";
36
+
37
+ /**
38
+ * Route keys for the CMS plugin — matches the keys returned by
39
+ * `stackClient.router.getRoute(path).routeKey`.
40
+ */
41
+ export type CMSRouteKey =
42
+ | "dashboard"
43
+ | "contentList"
44
+ | "newContent"
45
+ | "editContent";
46
+
47
+ interface CMSPrefetchForRoute {
48
+ (key: "dashboard" | "newContent", qc: QueryClient): Promise<void>;
49
+ (
50
+ key: "contentList",
51
+ qc: QueryClient,
52
+ params: { typeSlug: string },
53
+ ): Promise<void>;
54
+ (
55
+ key: "editContent",
56
+ qc: QueryClient,
57
+ params: { typeSlug: string; id: string },
58
+ ): Promise<void>;
59
+ }
32
60
 
33
61
  /**
34
62
  * Sync content types from config to database
@@ -443,6 +471,79 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
443
471
  return syncPromise;
444
472
  };
445
473
 
474
+ const getContentTypesWithCounts = async (adapter: Adapter) => {
475
+ const contentTypes = await getAllContentTypes(adapter);
476
+ return Promise.all(
477
+ contentTypes.map(async (ct) => {
478
+ const count: number = await adapter.count({
479
+ model: "contentItem",
480
+ where: [
481
+ { field: "contentTypeId", value: ct.id, operator: "eq" as const },
482
+ ],
483
+ });
484
+ return { ...ct, itemCount: count };
485
+ }),
486
+ );
487
+ };
488
+
489
+ const createCMSPrefetchForRoute = (adapter: Adapter): CMSPrefetchForRoute => {
490
+ return async function prefetchForRoute(
491
+ key: CMSRouteKey,
492
+ qc: QueryClient,
493
+ params?: Record<string, string>,
494
+ ): Promise<void> {
495
+ // Sync content types once at the top — idempotent for concurrent SSG calls
496
+ await ensureSynced(adapter);
497
+
498
+ switch (key) {
499
+ case "dashboard":
500
+ case "newContent": {
501
+ const typesWithCounts = await getContentTypesWithCounts(adapter);
502
+ qc.setQueryData(CMS_QUERY_KEYS.typesList(), typesWithCounts);
503
+ break;
504
+ }
505
+ case "contentList": {
506
+ const typeSlug = params?.typeSlug ?? "";
507
+ const [contentTypes, contentItems] = await Promise.all([
508
+ getContentTypesWithCounts(adapter),
509
+ getAllContentItems(adapter, typeSlug, { limit: 20, offset: 0 }),
510
+ ]);
511
+ qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
512
+ qc.setQueryData(
513
+ CMS_QUERY_KEYS.contentList({ typeSlug, limit: 20, offset: 0 }),
514
+ {
515
+ pages: [
516
+ {
517
+ items: contentItems.items,
518
+ total: contentItems.total,
519
+ limit: contentItems.limit ?? 20,
520
+ offset: contentItems.offset ?? 0,
521
+ },
522
+ ],
523
+ pageParams: [0],
524
+ },
525
+ );
526
+ break;
527
+ }
528
+ case "editContent": {
529
+ const typeSlug = params?.typeSlug ?? "";
530
+ const id = params?.id ?? "";
531
+ const [contentTypes, item] = await Promise.all([
532
+ getContentTypesWithCounts(adapter),
533
+ id ? getContentItemById(adapter, id) : Promise.resolve(null),
534
+ ]);
535
+ qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
536
+ if (id) {
537
+ qc.setQueryData(CMS_QUERY_KEYS.contentDetail(typeSlug, id), item);
538
+ }
539
+ break;
540
+ }
541
+ default:
542
+ break;
543
+ }
544
+ } as CMSPrefetchForRoute;
545
+ };
546
+
446
547
  return defineBackendPlugin({
447
548
  name: "cms",
448
549
 
@@ -464,6 +565,19 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
464
565
  await ensureSynced(adapter);
465
566
  return getContentItemBySlug(adapter, contentTypeSlug, slug);
466
567
  },
568
+ getContentItemById: async (id: string) => {
569
+ await ensureSynced(adapter);
570
+ return getContentItemById(adapter, id);
571
+ },
572
+ prefetchForRoute: createCMSPrefetchForRoute(adapter),
573
+ // Mutations
574
+ createContentItem: async (
575
+ typeSlug: string,
576
+ input: Parameters<typeof createCMSContentItem>[2],
577
+ ) => {
578
+ await ensureSynced(adapter);
579
+ return createCMSContentItem(adapter, typeSlug, input);
580
+ },
467
581
  }),
468
582
 
469
583
  routes: (adapter: Adapter) => {