@wix/agent-skills 1.20.0 → 1.21.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 (183) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +14 -112
  3. package/skills/wix-app/references/APP_TOOLS.md +2 -1
  4. package/skills/wix-app/references/BACKEND_EVENT.md +3 -3
  5. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +41 -152
  6. package/skills/wix-app/references/DASHBOARD_MODAL.md +4 -4
  7. package/skills/wix-app/references/SDK_MODULE_MAP.md +44 -0
  8. package/skills/wix-app/references/SITE_PLUGIN.md +36 -190
  9. package/skills/wix-app/references/app-tools/TOOLS.md +3 -1
  10. package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +21 -0
  11. package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +17 -0
  12. package/skills/wix-app/references/dashboard-plugin/SLOTS.md +4 -18
  13. package/skills/wix-app/references/site-plugin/EXAMPLES.md +1 -1
  14. package/skills/wix-headless-fast/SKILL.md +302 -51
  15. package/skills/wix-headless-fast/entry/skill.md +11 -2
  16. package/skills/wix-headless-fast/install/AGENTS.md +32 -0
  17. package/skills/wix-headless-fast/install/agents-md.mjs +39 -0
  18. package/skills/wix-headless-fast/install/attach.mjs +360 -0
  19. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  20. package/skills/wix-headless-fast/install/setup.mjs +239 -0
  21. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +226 -60
  22. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  23. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  24. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  25. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  26. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  27. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  28. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  29. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  30. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  31. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  32. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  33. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  34. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  35. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  36. package/skills/wix-headless-fast/references/blog/seed/read-site.mjs +22 -0
  37. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +181 -33
  38. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  39. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  40. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  41. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  42. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  43. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  44. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  45. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  46. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  47. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  48. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  49. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  50. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  51. package/skills/wix-headless-fast/references/bookings/seed/read-site.mjs +28 -0
  52. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +238 -62
  53. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  54. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  55. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  56. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  57. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  58. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  59. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  60. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  61. package/skills/wix-headless-fast/references/cms/seed/read-site.mjs +20 -0
  62. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +241 -83
  63. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  64. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  65. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  66. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  67. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  68. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  69. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  70. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  71. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  72. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  73. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  74. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  75. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  76. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  77. package/skills/wix-headless-fast/references/events/seed/read-site.mjs +26 -0
  78. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +216 -26
  79. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  80. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  81. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  82. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  83. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  84. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  85. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  86. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  87. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  88. package/skills/wix-headless-fast/references/forms/seed/read-site.mjs +20 -0
  89. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  90. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +316 -63
  91. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  92. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  93. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  94. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  95. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  96. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  97. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  98. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  99. package/skills/wix-headless-fast/references/members/seed/read-site.mjs +15 -0
  100. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +240 -59
  101. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  102. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  103. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  104. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  105. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  106. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  107. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  108. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  109. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  110. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  111. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  112. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  113. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  114. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  115. package/skills/wix-headless-fast/references/portfolio/seed/read-site.mjs +20 -0
  116. package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +19 -21
  117. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +214 -51
  118. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  119. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  120. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  121. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  122. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  123. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  124. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  125. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  126. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  127. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  128. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  129. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  130. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  131. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  132. package/skills/wix-headless-fast/references/pricing-plans/seed/read-site.mjs +21 -0
  133. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +242 -47
  134. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  135. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  136. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  137. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  138. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  139. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  140. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  141. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  142. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  143. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  144. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  145. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  146. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  147. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  148. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  149. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  150. package/skills/wix-headless-fast/references/restaurants/seed/read-site.mjs +25 -0
  151. package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +25 -5
  152. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  153. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  154. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  155. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  156. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  157. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  158. package/skills/wix-headless-fast/references/shared/seed/read-site.mjs +99 -0
  159. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +186 -63
  160. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  161. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  162. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  163. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  164. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  165. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  166. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +314 -0
  167. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  168. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  169. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  170. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +2 -0
  171. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  172. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  173. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +31 -9
  174. package/skills/wix-headless-fast/references/storefront/seed/read-site.mjs +38 -0
  175. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +57 -50
  176. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  177. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-coupon.md +10 -19
  178. package/skills/wix-manage/references/events/create-wix-event.md +5 -1
  179. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  180. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  181. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  182. package/skills/wix-headless-fast/install/fast-path.mjs +0 -186
  183. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -3,6 +3,7 @@
3
3
  // build your own on usePost. Chips display .label; the body renders ONLY via RichContent.
4
4
  import type { ComponentType } from "react";
5
5
  import { usePost } from "../../hooks/blog/usePost";
6
+ import { postMetaLine } from "../../wix/blog/posts";
6
7
  import type { BlogCategory, BlogTag, PostDetail } from "../../wix/blog/types";
7
8
  import RichContent from "./RichContent";
8
9
  import type { LinkLikeProps } from "./BlogFeedView";
@@ -81,9 +82,7 @@ export default function PostView({
81
82
  )}
82
83
  <h1 className="mt-2 text-3xl font-semibold tracking-tight">{post.title}</h1>
83
84
  <p className="mt-3 text-sm text-muted-foreground">
84
- {post.dateLabel && <time dateTime={post.dateISO}>{post.dateLabel}</time>}
85
- {post.dateLabel && post.minutesToRead > 0 ? " · " : ""}
86
- {post.minutesToRead > 0 ? `${post.minutesToRead} min read` : ""}
85
+ <time dateTime={post.dateISO}>{postMetaLine(post)}</time>
87
86
  </p>
88
87
  {post.coverUrl && (
89
88
  <div className="mt-6 aspect-[16/9] overflow-hidden rounded-xl bg-secondary">
@@ -1,123 +1,31 @@
1
- // Blog feed + taxonomy filter + cursor paging. SSR-friendly: pass server-fetched data as
2
- // `initial*` (Astro frontmatter / server component) and no client fetch happens; a SPA passes
3
- // nothing. Category/tag filtering fetches live (server-side-filtered); the two filters are
4
- // mutually exclusive — setting one clears the other.
5
- import { useEffect, useState } from "react";
6
- import { fetchPosts } from "../../wix/blog/posts";
7
- import { fetchBlogCategories, fetchBlogTags } from "../../wix/blog/taxonomy";
8
- import type { BlogCategory, BlogTag, PostPage, PostSummary } from "../../wix/blog/types";
1
+ // React binding of the blog feed store (wix/blog/blog-feed-store.ts) — the feed state machine
2
+ // (taxonomy filter, cursor paging) lives there, framework-free; this hook subscribes to one
3
+ // instance per mounted feed and exposes its state and actions under one name. Astro islands and
4
+ // React SPAs use this; a static page, Vue, or Svelte uses the store directly.
5
+ //
6
+ // SSR-friendly: pass server-fetched data as `initial*` (Astro frontmatter / server component) and
7
+ // no client fetch happens; a SPA passes nothing. Category/tag filtering fetches live
8
+ // (server-side-filtered); the two filters are mutually exclusive — setting one clears the other.
9
+ import { useEffect, useRef, useSyncExternalStore } from "react";
10
+ import { createBlogFeedStore, type BlogFeedState, type BlogFeedStore, type BlogFeedStoreOptions } from "../../wix/blog/blog-feed-store";
9
11
 
10
- export interface UseBlogFeedOptions {
11
- initialPage?: PostPage;
12
- initialCategories?: BlogCategory[];
13
- initialTags?: BlogTag[];
14
- pageSize?: number;
15
- }
16
-
17
- export interface UseBlogFeed {
18
- /** null while the first load is in flight — render skeletons, not an empty state. */
19
- posts: PostSummary[] | null;
20
- categories: BlogCategory[];
21
- tags: BlogTag[];
22
- activeCategoryId: string | null;
23
- setActiveCategoryId: (id: string | null) => void;
24
- activeTagId: string | null;
25
- setActiveTagId: (id: string | null) => void;
26
- /** True when another page exists — render a "load more" control on it. */
27
- hasMore: boolean;
28
- loadMore: () => void;
29
- loadingMore: boolean;
30
- error: string | null;
31
- }
12
+ export type UseBlogFeedOptions = BlogFeedStoreOptions;
32
13
 
33
- export function useBlogFeed({
34
- initialPage,
35
- initialCategories,
36
- initialTags,
37
- pageSize = 20,
38
- }: UseBlogFeedOptions = {}): UseBlogFeed {
39
- const [posts, setPosts] = useState<PostSummary[] | null>(initialPage?.posts ?? null);
40
- const [nextCursor, setNextCursor] = useState<string | null>(initialPage?.nextCursor ?? null);
41
- const [categories, setCategories] = useState<BlogCategory[]>(initialCategories ?? []);
42
- const [tags, setTags] = useState<BlogTag[]>(initialTags ?? []);
43
- const [activeCategoryId, setCategoryId] = useState<string | null>(null);
44
- const [activeTagId, setTagId] = useState<string | null>(null);
45
- const [loadingMore, setLoadingMore] = useState(false);
46
- const [error, setError] = useState<string | null>(null);
14
+ export type UseBlogFeed = BlogFeedState & Pick<BlogFeedStore, "setActiveCategoryId" | "setActiveTagId" | "loadMore">;
47
15
 
16
+ export function useBlogFeed(options: UseBlogFeedOptions = {}): UseBlogFeed {
17
+ const ref = useRef<BlogFeedStore | null>(null);
18
+ if (!ref.current) ref.current = createBlogFeedStore(options);
19
+ const store = ref.current;
48
20
  useEffect(() => {
49
- let alive = true;
50
- if (!initialCategories) {
51
- fetchBlogCategories().then((c) => alive && setCategories(c));
52
- }
53
- if (!initialTags) {
54
- fetchBlogTags().then((t) => alive && setTags(t));
55
- }
56
- return () => {
57
- alive = false;
58
- };
59
- // eslint-disable-next-line react-hooks/exhaustive-deps
60
- }, []);
61
-
62
- useEffect(() => {
63
- // The unfiltered first page may have come from the server; only fetch when it didn't,
64
- // or when a filter is active (filters are server-side).
65
- if (activeCategoryId === null && activeTagId === null && initialPage) {
66
- setPosts(initialPage.posts);
67
- setNextCursor(initialPage.nextCursor);
68
- return;
69
- }
70
- let alive = true;
71
- setPosts(null);
72
- setError(null);
73
- fetchPosts({ limit: pageSize, categoryId: activeCategoryId, tagId: activeTagId })
74
- .then((page) => {
75
- if (!alive) return;
76
- setPosts(page.posts);
77
- setNextCursor(page.nextCursor);
78
- })
79
- .catch((e) => {
80
- if (!alive) return;
81
- setPosts([]);
82
- setNextCursor(null);
83
- setError(e instanceof Error ? e.message : String(e));
84
- });
85
- return () => {
86
- alive = false;
87
- };
88
- // eslint-disable-next-line react-hooks/exhaustive-deps
89
- }, [activeCategoryId, activeTagId, pageSize]);
90
-
91
- function loadMore(): void {
92
- if (!nextCursor || loadingMore) return;
93
- setLoadingMore(true);
94
- setError(null);
95
- fetchPosts({ limit: pageSize, cursor: nextCursor })
96
- .then((page) => {
97
- setPosts((prev) => [...(prev ?? []), ...page.posts]);
98
- setNextCursor(page.nextCursor);
99
- })
100
- .catch((e) => setError(e instanceof Error ? e.message : String(e)))
101
- .finally(() => setLoadingMore(false));
102
- }
103
-
21
+ store.start();
22
+ return () => store.stop();
23
+ }, [store]);
24
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
104
25
  return {
105
- posts,
106
- categories,
107
- tags,
108
- activeCategoryId,
109
- setActiveCategoryId: (id) => {
110
- setCategoryId(id);
111
- setTagId(null);
112
- },
113
- activeTagId,
114
- setActiveTagId: (id) => {
115
- setTagId(id);
116
- setCategoryId(null);
117
- },
118
- hasMore: nextCursor !== null,
119
- loadMore,
120
- loadingMore,
121
- error,
26
+ ...state,
27
+ setActiveCategoryId: store.setActiveCategoryId,
28
+ setActiveTagId: store.setActiveTagId,
29
+ loadMore: store.loadMore,
122
30
  };
123
31
  }
@@ -1,70 +1,22 @@
1
- // One post by slug + its resolved category/tag chips. SSR-friendly: pass the server-fetched
2
- // post (and taxonomy) as `initial*` and no client fetch happens; a SPA passes only the slug.
3
- // Chips resolve from the FULL taxonomy lists (blog taxonomies are small, fetched once) —
4
- // never per-id lookups in a render loop.
5
- import { useEffect, useMemo, useState } from "react";
6
- import { fetchPostBySlug } from "../../wix/blog/posts";
7
- import { fetchBlogCategories, fetchBlogTags } from "../../wix/blog/taxonomy";
8
- import type { BlogCategory, BlogTag, PostDetail } from "../../wix/blog/types";
1
+ // React binding of the post store (wix/blog/post-store.ts) — post-by-slug loading and the resolved
2
+ // category/tag chips live there, framework-free; this hook subscribes to one instance per slug.
3
+ //
4
+ // SSR-friendly: pass the server-fetched post (and taxonomy) as `initial*` and no client fetch
5
+ // happens; a SPA passes only the slug. A slug change creates a fresh store and refetches.
6
+ import { useEffect, useRef, useSyncExternalStore } from "react";
7
+ import { createPostStore, type PostState, type PostStore, type PostStoreOptions } from "../../wix/blog/post-store";
9
8
 
10
- export interface UsePostOptions {
11
- slug: string;
12
- initialPost?: PostDetail;
13
- initialCategories?: BlogCategory[];
14
- initialTags?: BlogTag[];
15
- }
9
+ export type UsePostOptions = PostStoreOptions;
16
10
 
17
- export interface UsePost {
18
- /** null while loading (render a skeleton) AND when not found — disambiguate via notFound. */
19
- post: PostDetail | null;
20
- /** True once the slug definitively resolved to nothing — render a not-found state. */
21
- notFound: boolean;
22
- /** This post's categories/tags, resolved to full objects (display .label, route by .slug). */
23
- categories: BlogCategory[];
24
- tags: BlogTag[];
25
- error: string | null;
26
- }
11
+ export type UsePost = PostState;
27
12
 
28
13
  export function usePost({ slug, initialPost, initialCategories, initialTags }: UsePostOptions): UsePost {
29
- const [post, setPost] = useState<PostDetail | null>(initialPost ?? null);
30
- const [notFound, setNotFound] = useState(false);
31
- const [allCategories, setAllCategories] = useState<BlogCategory[]>(initialCategories ?? []);
32
- const [allTags, setAllTags] = useState<BlogTag[]>(initialTags ?? []);
33
- const [error, setError] = useState<string | null>(null);
34
-
14
+ const ref = useRef<{ slug: string; store: PostStore } | null>(null);
15
+ if (!ref.current || ref.current.slug !== slug) ref.current = { slug, store: createPostStore({ slug, initialPost, initialCategories, initialTags }) };
16
+ const store = ref.current.store;
35
17
  useEffect(() => {
36
- let alive = true;
37
- if (!initialPost || initialPost.slug !== slug) {
38
- setPost(null);
39
- setNotFound(false);
40
- fetchPostBySlug(slug)
41
- .then((p) => {
42
- if (!alive) return;
43
- if (p) setPost(p);
44
- else setNotFound(true);
45
- })
46
- .catch((e) => alive && setError(e instanceof Error ? e.message : String(e)));
47
- }
48
- if (!initialCategories) {
49
- fetchBlogCategories().then((c) => alive && setAllCategories(c));
50
- }
51
- if (!initialTags) {
52
- fetchBlogTags().then((t) => alive && setAllTags(t));
53
- }
54
- return () => {
55
- alive = false;
56
- };
57
- // eslint-disable-next-line react-hooks/exhaustive-deps
58
- }, [slug]);
59
-
60
- const categories = useMemo(
61
- () => (post ? allCategories.filter((c) => post.categoryIds.includes(c.id)) : []),
62
- [post, allCategories],
63
- );
64
- const tags = useMemo(
65
- () => (post ? allTags.filter((t) => post.tagIds.includes(t.id)) : []),
66
- [post, allTags],
67
- );
68
-
69
- return { post, notFound, categories, tags, error };
18
+ store.start();
19
+ return () => store.stop();
20
+ }, [store]);
21
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
70
22
  }
@@ -0,0 +1,137 @@
1
+ // The blog feed as a framework-free store — the logic behind useBlogFeed, usable from React
2
+ // (useBlogFeed wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and
3
+ // render), or as the specification for a port. State, actions, subscribe/getState, emit after every
4
+ // change — the same shape as storefront's shop-store.
5
+ //
6
+ // Category and tag filters are applied by Wix (fetchPosts), never to a page already in hand; the
7
+ // two are mutually exclusive — setting one clears the other. A changed filter starts a fresh cursor
8
+ // chain; a late response from a superseded query is dropped. Paging appends by cursor.
9
+ //
10
+ // SSR-friendly: seed with `initialPage` (and `initialCategories`/`initialTags`) and the first,
11
+ // unfiltered page renders at once with no client fetch; a SPA passes nothing and `start()` fetches.
12
+ // One store per mounted feed (a page can hold a feed and a "latest posts" rail): createBlogFeedStore().
13
+ import { fetchPosts } from "./posts";
14
+ import { fetchBlogCategories, fetchBlogTags } from "./taxonomy";
15
+ import type { BlogCategory, BlogTag, PostPage, PostSummary } from "./types";
16
+
17
+ export interface BlogFeedStoreOptions {
18
+ initialPage?: PostPage;
19
+ initialCategories?: BlogCategory[];
20
+ initialTags?: BlogTag[];
21
+ pageSize?: number;
22
+ }
23
+
24
+ /** Everything a feed surface renders from. Read it with getState() or through a subscription. */
25
+ export interface BlogFeedState {
26
+ /** null while the first load is in flight — render skeletons, not an empty state. */
27
+ posts: PostSummary[] | null;
28
+ categories: BlogCategory[];
29
+ tags: BlogTag[];
30
+ activeCategoryId: string | null;
31
+ activeTagId: string | null;
32
+ /** True when another page exists — render a "load more" control on it. */
33
+ hasMore: boolean;
34
+ loadingMore: boolean;
35
+ error: string | null;
36
+ }
37
+
38
+ export interface BlogFeedStore {
39
+ getState(): BlogFeedState;
40
+ subscribe(listener: () => void): () => void;
41
+ /** Fetch the taxonomy (unless seeded) and the first page (unless seeded and unfiltered). Call once when mounted. */
42
+ start(): void;
43
+ /** Stop reacting; drop late responses. */
44
+ stop(): void;
45
+ /** Server-side filter; clears the tag filter. */
46
+ setActiveCategoryId(id: string | null): void;
47
+ /** Server-side filter; clears the category filter. */
48
+ setActiveTagId(id: string | null): void;
49
+ loadMore(): Promise<void>;
50
+ }
51
+
52
+ export function createBlogFeedStore({ initialPage, initialCategories, initialTags, pageSize = 20 }: BlogFeedStoreOptions = {}): BlogFeedStore {
53
+ let posts: PostSummary[] | null = initialPage?.posts ?? null;
54
+ let cursor: string | null = initialPage?.nextCursor ?? null;
55
+ let categories: BlogCategory[] = initialCategories ?? [];
56
+ let tags: BlogTag[] = initialTags ?? [];
57
+ let activeCategoryId: string | null = null;
58
+ let activeTagId: string | null = null;
59
+ let loadingMore = false;
60
+ let error: string | null = null;
61
+ let started = false;
62
+ let generation = 0; // bumped by every filter change and stop(); a response from an older generation is dropped
63
+ const listeners = new Set<() => void>();
64
+ let snapshot: BlogFeedState | null = null;
65
+ const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
66
+
67
+ function getState(): BlogFeedState {
68
+ if (snapshot) return snapshot;
69
+ snapshot = { posts, categories, tags, activeCategoryId, activeTagId, hasMore: cursor !== null, loadingMore, error };
70
+ return snapshot;
71
+ }
72
+
73
+ // The first page for the current filter. The unfiltered page may have come from the server —
74
+ // fetch only when it didn't, or when a filter is active (filters are server-side).
75
+ function query(): void {
76
+ if (!started) return;
77
+ const id = ++generation;
78
+ loadingMore = false;
79
+ if (activeCategoryId === null && activeTagId === null && initialPage) {
80
+ posts = initialPage.posts; cursor = initialPage.nextCursor; error = null;
81
+ emit();
82
+ return;
83
+ }
84
+ posts = null; cursor = null; error = null;
85
+ emit();
86
+ fetchPosts({ limit: pageSize, categoryId: activeCategoryId, tagId: activeTagId })
87
+ .then((page) => {
88
+ if (generation !== id) return; // superseded — drop it
89
+ posts = page.posts; cursor = page.nextCursor;
90
+ emit();
91
+ })
92
+ .catch((e) => {
93
+ if (generation !== id) return;
94
+ posts = []; cursor = null;
95
+ error = e instanceof Error ? e.message : String(e);
96
+ emit();
97
+ });
98
+ }
99
+
100
+ return {
101
+ getState,
102
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
103
+ start() {
104
+ if (started) return;
105
+ started = true;
106
+ if (!initialCategories) fetchBlogCategories().then((c) => { if (started) { categories = c; emit(); } });
107
+ if (!initialTags) fetchBlogTags().then((t) => { if (started) { tags = t; emit(); } });
108
+ query();
109
+ },
110
+ stop() { started = false; generation++; },
111
+ setActiveCategoryId(id) {
112
+ if (id === activeCategoryId && activeTagId === null) return;
113
+ activeCategoryId = id; activeTagId = null;
114
+ query();
115
+ },
116
+ setActiveTagId(id) {
117
+ if (id === activeTagId && activeCategoryId === null) return;
118
+ activeTagId = id; activeCategoryId = null;
119
+ query();
120
+ },
121
+ async loadMore() {
122
+ if (!cursor || loadingMore) return;
123
+ const id = generation;
124
+ loadingMore = true; error = null; emit();
125
+ try {
126
+ const page = await fetchPosts({ limit: pageSize, cursor });
127
+ if (generation !== id) return;
128
+ posts = [...(posts ?? []), ...page.posts]; cursor = page.nextCursor;
129
+ } catch (e) {
130
+ if (generation !== id) return;
131
+ error = e instanceof Error ? e.message : String(e);
132
+ } finally {
133
+ if (generation === id) { loadingMore = false; emit(); }
134
+ }
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,78 @@
1
+ // One post as a framework-free store — the logic behind usePost, usable from React (usePost wraps
2
+ // it), from a static page's post view, from Vue/Svelte, or as the specification for a port. Loads
3
+ // the post by slug (unless seeded) and the FULL taxonomy lists (blog taxonomies are small, fetched
4
+ // once), and resolves the post's category/tag chips from them — never per-id lookups in a render.
5
+ //
6
+ // SSR-friendly: pass the server-fetched PostDetail as `initialPost` (and the taxonomy) and no client
7
+ // fetch happens; a SPA passes only the slug. One store per post surface: createPostStore().
8
+ import { fetchPostBySlug } from "./posts";
9
+ import { fetchBlogCategories, fetchBlogTags } from "./taxonomy";
10
+ import type { BlogCategory, BlogTag, PostDetail } from "./types";
11
+
12
+ export interface PostStoreOptions {
13
+ slug: string;
14
+ initialPost?: PostDetail;
15
+ initialCategories?: BlogCategory[];
16
+ initialTags?: BlogTag[];
17
+ }
18
+
19
+ export interface PostState {
20
+ /** null while loading (render a skeleton) AND when not found — disambiguate via notFound. */
21
+ post: PostDetail | null;
22
+ /** True once the slug definitively resolved to nothing — render a not-found state. */
23
+ notFound: boolean;
24
+ /** This post's categories/tags, resolved to full objects (display .label, route by .slug). */
25
+ categories: BlogCategory[];
26
+ tags: BlogTag[];
27
+ error: string | null;
28
+ }
29
+
30
+ export interface PostStore {
31
+ getState(): PostState;
32
+ subscribe(listener: () => void): () => void;
33
+ /** Fetch the post (unless `initialPost` matches the slug) and the taxonomy (unless seeded). Call once when mounted. */
34
+ start(): void;
35
+ stop(): void;
36
+ }
37
+
38
+ export function createPostStore({ slug, initialPost, initialCategories, initialTags }: PostStoreOptions): PostStore {
39
+ let post: PostDetail | null = initialPost ?? null;
40
+ let notFound = false;
41
+ let allCategories: BlogCategory[] = initialCategories ?? [];
42
+ let allTags: BlogTag[] = initialTags ?? [];
43
+ let error: string | null = null;
44
+ let started = false;
45
+ const listeners = new Set<() => void>();
46
+ let snapshot: PostState | null = null;
47
+ const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
48
+
49
+ function getState(): PostState {
50
+ if (snapshot) return snapshot;
51
+ snapshot = {
52
+ post,
53
+ notFound,
54
+ categories: post ? allCategories.filter((c) => post!.categoryIds.includes(c.id)) : [],
55
+ tags: post ? allTags.filter((t) => post!.tagIds.includes(t.id)) : [],
56
+ error,
57
+ };
58
+ return snapshot;
59
+ }
60
+
61
+ return {
62
+ getState,
63
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
64
+ start() {
65
+ if (started) return;
66
+ started = true;
67
+ if (!initialPost || initialPost.slug !== slug) {
68
+ post = null; notFound = false; emit();
69
+ fetchPostBySlug(slug)
70
+ .then((p) => { if (!started) return; if (p) post = p; else notFound = true; emit(); })
71
+ .catch((e) => { if (started) { error = e instanceof Error ? e.message : String(e); emit(); } });
72
+ }
73
+ if (!initialCategories) fetchBlogCategories().then((c) => { if (started) { allCategories = c; emit(); } });
74
+ if (!initialTags) fetchBlogTags().then((t) => { if (started) { allTags = t; emit(); } });
75
+ },
76
+ stop() { started = false; },
77
+ };
78
+ }
@@ -0,0 +1,117 @@
1
+ // Post rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./posts.ts (the
2
+ // SDK, managed Astro and React) and the REST twin in references/blog/rest/posts.ts (fetch, a static
3
+ // site or a port to another language). Every rule about dates, covers, the feed's sort and filters,
4
+ // and the body fieldsets lives HERE, once. A raw post may come from the SDK (`_id`, media as a
5
+ // `wix:image://` string, dates as Date) or from REST (`id`, media as an { id, url } object, dates
6
+ // as ISO strings); the mappers accept both and produce the same DTO. Imports are type-only so a
7
+ // strip to JS emits no imports.
8
+ import type { PostDetail, PostSummary } from "./types";
9
+
10
+ /** A raw Blog V3 entity as either transport returns it. */
11
+ export type Raw = Record<string, any>;
12
+ /** Media value + size → https URL. Injected: the SDK transport scales through @wix/sdk, REST through the URL form. */
13
+ export type ImgSrc = (value: any, width: number, height: number) => string;
14
+
15
+ /** The Wix Blog app id (Astro item-page routing; comments use it as appId). */
16
+ export const BLOG_APP_ID = "14bcded7-0066-7c35-14d7-466cb3f09103";
17
+
18
+ /** The body fieldsets — without them richContent and contentText come back undefined. */
19
+ export const DETAIL_FIELDSETS = ["RICH_CONTENT", "CONTENT_TEXT"] as const;
20
+
21
+ /** The feed's order: newest first; Wix places pinned posts first on its own. */
22
+ export const FEED_SORT = [{ fieldName: "firstPublishedDate", order: "DESC" }] as const;
23
+
24
+ /** Cover size every card and post header gets — 16:9, one scaled URL per post. */
25
+ export const COVER_WIDTH = 1200;
26
+ export const COVER_HEIGHT = 675;
27
+
28
+ export const rawId = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
29
+
30
+ /**
31
+ * A media value in the form imgSrc scales. The SDK hands over the `wix:image://v1/<file>/<name>#…`
32
+ * string; REST hands over the Image object { id, url, width, height, filename }. Rebuilding the
33
+ * wix:image form from the object makes both transports scale to the SAME URL — passing the object's
34
+ * `url` through would skip scaling and the two paths would disagree on coverUrl.
35
+ */
36
+ export function mediaValue(value: unknown): string {
37
+ if (!value) return "";
38
+ if (typeof value === "string") return value;
39
+ const o = value as Raw;
40
+ if (o.id && !String(o.id).startsWith("http")) {
41
+ const size = o.width && o.height ? `#originWidth=${o.width}&originHeight=${o.height}` : "";
42
+ return `wix:image://v1/${o.id}/${o.filename ?? ""}${size}`;
43
+ }
44
+ return o.url ?? o.image ?? "";
45
+ }
46
+
47
+ /** Display date + ISO date from either a Date (SDK) or an ISO string (REST); both "" when missing or invalid. */
48
+ export function dateParts(value: unknown): { dateLabel: string; dateISO: string } {
49
+ if (!value) return { dateLabel: "", dateISO: "" };
50
+ const d = value instanceof Date ? value : new Date(String(value));
51
+ if (Number.isNaN(d.getTime())) return { dateLabel: "", dateISO: "" };
52
+ return {
53
+ dateLabel: d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }),
54
+ dateISO: d.toISOString(),
55
+ };
56
+ }
57
+
58
+ /** "Aug 27, 2026 · 4 min read" — the card's and header's meta line; either half alone, "" when neither is known. */
59
+ export function postMetaLine(post: Pick<PostSummary, "dateLabel" | "minutesToRead">): string {
60
+ return [post.dateLabel, post.minutesToRead > 0 ? `${post.minutesToRead} min read` : ""].filter(Boolean).join(" · ");
61
+ }
62
+
63
+ export function toSummary(raw: Raw, imgSrc: ImgSrc): PostSummary {
64
+ return {
65
+ id: rawId(raw),
66
+ slug: raw.slug ?? "",
67
+ title: raw.title ?? "",
68
+ excerpt: raw.excerpt ?? "",
69
+ ...dateParts(raw.firstPublishedDate),
70
+ minutesToRead: raw.minutesToRead ?? 0,
71
+ featured: raw.featured === true,
72
+ pinned: raw.pinned === true,
73
+ // The cover lives at media.wixMedia.image — a media string (SDK) or an Image object (REST).
74
+ coverUrl: imgSrc(mediaValue(raw.media?.wixMedia?.image), COVER_WIDTH, COVER_HEIGHT),
75
+ categoryIds: raw.categoryIds ?? [],
76
+ tagIds: raw.tagIds ?? [],
77
+ };
78
+ }
79
+
80
+ export function toDetail(raw: Raw, imgSrc: ImgSrc): PostDetail {
81
+ return {
82
+ ...toSummary(raw, imgSrc),
83
+ richContent: raw.richContent ?? null,
84
+ // contentText is plain text — split on newlines for the fallback body.
85
+ paragraphs: String(raw.contentText ?? "")
86
+ .split("\n")
87
+ .map((s: string) => s.trim())
88
+ .filter(Boolean),
89
+ };
90
+ }
91
+
92
+ export interface FetchPostsOptions {
93
+ limit?: number;
94
+ /** `nextCursor` from a previous page. */
95
+ cursor?: string | null;
96
+ /** Server-side filters (first page only — the cursor carries them on later pages). */
97
+ categoryId?: string | null;
98
+ tagId?: string | null;
99
+ }
100
+
101
+ /**
102
+ * The feed query as one object — the REST body's `query`, and the rule the SDK builder in ./posts.ts
103
+ * spells with .descending()/.hasSome()/.skipTo(). A cursor encodes the original filter+sort, so a
104
+ * cursor request carries ONLY cursorPaging; the first page carries the sort and the filters.
105
+ */
106
+ export function feedQuery({ limit = 20, cursor, categoryId, tagId }: FetchPostsOptions = {}): Raw {
107
+ if (cursor) return { cursorPaging: { limit, cursor } };
108
+ const filter: Raw = {};
109
+ if (categoryId) filter.categoryIds = { $hasSome: [categoryId] };
110
+ if (tagId) filter.tagIds = { $hasSome: [tagId] };
111
+ return { ...(Object.keys(filter).length ? { filter } : {}), sort: FEED_SORT, cursorPaging: { limit } };
112
+ }
113
+
114
+ /** The by-slug query (REST body's `query`): exact slug, one row. */
115
+ export function slugQuery(slug: string): Raw {
116
+ return { filter: { slug: { $eq: slug } }, cursorPaging: { limit: 1 } };
117
+ }