@wix/agent-skills 1.20.0 → 1.20.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.
Files changed (152) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-headless-fast/SKILL.md +200 -34
  3. package/skills/wix-headless-fast/entry/skill.md +10 -2
  4. package/skills/wix-headless-fast/install/attach.mjs +253 -0
  5. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  6. package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
  7. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
  8. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  9. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  10. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  11. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  12. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  13. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  14. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  15. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  16. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  17. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  18. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  19. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  20. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  21. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  22. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
  23. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  24. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  25. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  26. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  27. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  28. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  29. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  30. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  31. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  32. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  33. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  34. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  35. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  36. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
  37. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  38. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  39. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  40. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  41. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  42. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  43. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  44. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  45. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
  46. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  47. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  48. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  49. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  50. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  51. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  52. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  53. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  54. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  55. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  56. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  57. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  58. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  59. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  60. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
  61. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  62. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  63. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  64. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  65. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  66. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  67. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  68. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  69. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  70. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  71. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
  72. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  73. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  74. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  75. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  76. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  77. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  78. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  79. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  80. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
  81. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  82. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  83. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  84. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  85. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  86. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  87. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  88. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  89. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  90. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  91. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  92. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  93. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  94. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  95. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
  96. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  97. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  98. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  99. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  100. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  101. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  102. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  103. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  104. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  105. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  106. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  107. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  108. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  109. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  110. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
  111. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  112. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  113. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  114. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  115. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  116. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  117. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  118. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  119. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  120. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  121. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  122. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  123. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  124. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  125. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  126. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  127. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  128. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  129. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  130. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  131. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  132. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  133. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
  134. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  135. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  136. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  137. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  138. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  139. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  140. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
  141. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  142. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  143. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  144. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  145. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  146. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
  147. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
  148. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  149. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  150. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  151. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  152. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -1,29 +1,37 @@
1
1
  # Blog — playbook
2
2
 
3
3
  The blog machinery ships as files — post reads (feed paging, slug lookup with the body
4
- fieldsets), category/tag taxonomy, and the Ricos body renderer, typed end-to-end. **The
5
- presentation is yours**: you design and implement the post card, the grid, the blog index
6
- surface, and the post page surface on the shipped hooks/DTOs, plus the home page and the
7
- brand. You never write blog data logic; you never skip designing. The vertical is
8
- **read-only** — visitors read posts; authoring stays in the dashboard.
4
+ fieldsets), category/tag taxonomy, the feed and post state machines, and the Ricos body renderer,
5
+ typed end-to-end. **The presentation doesn't ship — you build it** on the shipped hooks/DTOs: the
6
+ post card, the grid, the blog index surface, the post page surface, the home page, and the brand.
7
+ You never write blog data logic; you never skip designing. The vertical is **read-only** —
8
+ visitors read posts; authoring stays in the dashboard.
9
9
 
10
10
  ## The file map (deployed into `src/`)
11
11
 
12
- **Don't read the shipped files** — this table and the contracts below are everything you
13
- need. Open a shipped file's source only on a real fallback (runtime error / uncovered field),
14
- or to read a reference component's pattern.
12
+ **On Astro and React the shipped files are tested and work as they are** — this table and the
13
+ contracts below are everything you need to use them, so don't spend the run reading their source;
14
+ wire them and build your surfaces. Reading them is the right move when something is off (a runtime
15
+ error, a field this playbook doesn't cover) or when the brief wants a behaviour they don't offer —
16
+ then read the file that owns it and change or extend it. On `lib`, `static`, and a port the
17
+ components don't deploy at all, and each wiring section below opens with the files to read before
18
+ writing their equivalents. Files you edit: `SiteLayout.astro`, `styles/global.css`, and the two
19
+ shipped pages' templates. Files you **create**: your feed island (skeleton below), your post
20
+ surface, and your home page.
15
21
 
16
22
  | file | what it is |
17
23
  |---|---|
18
- | `wix/config.ts` · `wix/sdk.ts` · `wix/media.ts` · `wix/money.ts` | shared auth seam + helpers (deploy configures; nothing to set) |
19
- | `wix/blog/types.ts` | the DTOs (`PostSummary`, `PostDetail`, `BlogCategory`, `BlogTag`, `PostPage`) — contracts below |
20
- | `wix/blog/posts.ts` | `fetchPosts` (paged, filterable), `fetchPostBySlug` (full body) |
21
- | `wix/blog/taxonomy.ts` | `fetchBlogCategories`, `fetchBlogTags`, `fetchCategoryBySlug`, `fetchTagBySlug` |
22
- | `hooks/blog/useBlogFeed.ts` | feed + taxonomy filter + load-more — contract below |
23
- | `hooks/blog/usePost.ts` | post by slug + resolved chips — contract below |
24
- | `components/blog/RichContent.tsx` | the post-body renderer — **wire AS-IS** (machinery, not a reference) |
25
- | `components/blog/BlogFeedView.tsx` (+ `PostCard`) · `PostView.tsx` | **REFERENCE implementations** — correct, plain; build your own instead of shipping them |
26
- | `styles/global.css` | the design system: Tailwind v4 + the `@theme` token block (shared across verticals) |
24
+ | `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
25
+ | `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes, ratio)` — every `<img>` attribute for a DTO image (`src`, `srcSet`, `sizes`, lazy): `<img {...imgAttrs(post.coverUrl, "(min-width: 1024px) 33vw, 100vw", 9 / 16)} alt={post.title} />`; `imgSrc()` / `imgSrcSet()` underneath |
26
+ | `wix/blog/types.ts` | the DTOs (`PostSummary`, `PostDetail`, `BlogCategory`, `BlogTag`, `PostPage`) — contracts inlined below |
27
+ | `wix/blog/posts.ts` | `fetchPosts` (cursor-paged, category/tag filtered on Wix), `fetchPostBySlug` (full body), `postMetaLine(post)` ("Aug 27, 2026 · 4 min read") — the transport; the rules and DTO mappers are in `posts-core.ts` beside it (shared with the REST layer) |
28
+ | `wix/blog/taxonomy.ts` | `fetchBlogCategories`, `fetchBlogTags`, `fetchCategoryBySlug`, `fetchTagBySlug` — the transport; mappers in `taxonomy-core.ts` |
29
+ | `wix/blog/blog-feed-store.ts` · `post-store.ts` | the feed and post state machines, framework-free (`createBlogFeedStore()`, `createPostStore()` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
30
+ | `hooks/blog/useBlogFeed.ts` | React binding of `blog-feed-store.ts`: feed + taxonomy filter + load-more — contract below |
31
+ | `hooks/blog/usePost.ts` | React binding of `post-store.ts`: post by slug + resolved chips — contract below |
32
+ | `components/blog/RichContent.tsx` | the post-body renderer — **wire as-is** (machinery, not a reference) |
33
+ | `components/blog/BlogFeedView.tsx` (+ `PostCard`) · `PostView.tsx` | **reference implementations** — correct, plain; build your own instead of shipping them |
34
+ | `styles/global.css` | **the design system**: Tailwind v4 + the `@theme` token block (shared across verticals). Everything, shipped and yours, styles from these tokens |
27
35
 
28
36
  Astro stack additionally gets:
29
37
 
@@ -31,43 +39,62 @@ Astro stack additionally gets:
31
39
  |---|---|
32
40
  | `layouts/SiteLayout.astro` | site chrome — **yours to brand** (keep the `seo-tags` slot + global.css import). If another vertical is also deployed, its layout won — add a Blog nav link there |
33
41
  | `pages/blog.astro` | SSR feed — **keep the frontmatter**, swap the island import to YOUR component |
34
- | `pages/blog/[...slug].astro` | SSR post page with owner-editable SEO — **keep the frontmatter, the `[...slug]` rest param, and the SEO pieces** (`wixMetadata`, `loadSEOTagsServiceConfig`, `<SEO.Tags>`) exactly; restyle the template. The body island stays `client:only="react"` (the ricos viewer breaks under SSR) |
42
+ | `pages/blog/[...slug].astro` | SSR post page with owner-editable SEO — **keep the frontmatter, the `[...slug]` rest param, and the SEO pieces** (`wixMetadata`, `loadSEOTagsServiceConfig`, `<SEO.Tags>`) exactly; restyle the template. The body island stays `client:only="react"` (the ricos viewer breaks under SSR). A slug that resolves to nothing returns a real 404 |
35
43
 
36
44
  ## What you build — the design job
37
45
 
38
- 1. **The post card + grid** — your tile (cover, title, excerpt, date · min-read presentation)
39
- and rhythm, with skeletons while loading and an honest empty state.
40
- 2. **The blog index surface** — the feed on `useBlogFeed`: category filter (only when >1
46
+ 1. **The post card + grid** — your tile (cover, title, excerpt, `postMetaLine(post)`) and rhythm,
47
+ with skeletons while `posts === null` and an honest empty state for `[]`.
48
+ 2. **The blog index surface** — the feed island on `useBlogFeed`: category filter (only when >1
41
49
  non-empty category), the grid, and a load-more control gated on `hasMore`.
42
- 3. **The post page surface** — header (categories eyebrow, title, date, cover), the body via
43
- the shipped `RichContent`, and the tag chips — restyle the `[...slug].astro` template
44
- around it.
45
- 4. **The home page** — hero, latest posts (fetch in frontmatter → your components), brand
46
- story.
50
+ 3. **The post page surface** — header (categories eyebrow, title, meta line, cover), the body via
51
+ the shipped `RichContent`, and the tag chips — restyle the `[...slug].astro` template around it.
52
+ 4. **The home page** — hero, latest posts (fetch in frontmatter → your components), brand story.
47
53
 
48
54
  Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
49
55
  Style everything with Tailwind utilities on the tokens. Dark theme: the ricos CSS hardcodes
50
56
  near-black text — add a global override scoping `.ricos-content` to the foreground token
51
57
  (in Astro use `<style is:global>`; React islands don't inherit scoped Astro styles).
52
58
 
59
+ ### What a complete blog shows (recommended defaults)
60
+
61
+ Defaults for a blog whose brief says nothing about them; the prompt wins where it differs. Look at
62
+ the seeded content before designing (how many posts, categories, covers, body structure) and design
63
+ for this blog, not for a stereotype of one.
64
+
65
+ - **Home:** what the blog is about and the latest posts (real ones, under a truthful heading) in the
66
+ first screen; a category link goes to the feed filtered to it, never to an anchor.
67
+ - **Index:** a real card — cover, title, meta line — in the first screen at 390px too; the category
68
+ row only when more than one category has posts; loading, empty, and error states that look
69
+ different; load-more, never a raised page size.
70
+ - **Post page:** eyebrow, title, meta line, cover as a bounded band on phones (`max-h-[45vh]`), the
71
+ body, then the tags; a slug that resolves to nothing shows only the not-found state.
72
+ - **Copy:** nothing the author didn't write — no invented bylines, comment counts, likes, or
73
+ "trending" labels; no Wix IDs or technical words in visible text.
74
+
53
75
  ### The contracts your components consume
54
76
 
77
+ Tested and working as they are; read the source when something is off or the brief wants more.
78
+
55
79
  ```ts
56
80
  // PostSummary (tiles) — display-ready:
57
81
  // { id, slug, title, excerpt, dateLabel /* "Aug 26, 2026" | "" */, dateISO,
58
- // minutesToRead /* 0 = unknown */, featured, pinned, coverUrl /* "" = no cover */,
82
+ // minutesToRead /* 0 = unknown */, featured, pinned, coverUrl /* "" = no cover, 16:9 */,
59
83
  // categoryIds, tagIds }
84
+ // postMetaLine(post) → "Aug 26, 2026 · 4 min read" (either half alone; "" when neither is known) —
85
+ // render it inside <time dateTime={post.dateISO}>; don't hand-assemble the separator.
60
86
  // PostDetail adds: richContent (Ricos JSON | null — render ONLY via RichContent),
61
87
  // paragraphs (plain-text fallback body).
62
88
  // BlogCategory: { id, slug, label, description, postCount, coverUrl } // display .label
63
- // BlogTag: { id, slug, label, postCount }
89
+ // BlogTag: { id, slug, label, postCount /* published posts */ }
64
90
 
65
- // useBlogFeed({ initialPage?, initialCategories?, initialTags?, pageSize? }) →
91
+ // useBlogFeed({ initialPage?, initialCategories?, initialTags?, pageSize? /* 20 */ }) →
66
92
  // { posts: PostSummary[]|null /* null = loading → skeletons */,
67
93
  // categories, tags,
68
94
  // activeCategoryId, setActiveCategoryId(id|null), // server-side filter
69
95
  // activeTagId, setActiveTagId(id|null), // mutually exclusive with category
70
96
  // hasMore, loadMore(), loadingMore, error }
97
+ // Filters and paging run on Wix (a changed filter restarts the list; a late response is dropped).
71
98
 
72
99
  // usePost({ slug, initialPost?, initialCategories?, initialTags? }) →
73
100
  // { post: PostDetail|null, notFound /* true = render a 404 state, never invent a post */,
@@ -77,61 +104,198 @@ near-black text — add a global override scoping `.ricos-content` to the foregr
77
104
  // — the ONLY body render path. In Astro: client:only="react".
78
105
  ```
79
106
 
107
+ ### The island you create — skeleton
108
+
109
+ The two Astro pages ship; their frontmatter is machinery (SSR fetch → DTO props → island; the
110
+ post page's SEO block) and stays as shipped. What you create is the feed island `blog.astro` mounts
111
+ (and, in a SPA, the post surface — `PostView.tsx` shows its shape). Hooks first, branches after (an
112
+ early return above a hook changes hook order and React throws). Islands render on the server too,
113
+ after the 200 is sent — nothing in a render path may throw.
114
+
115
+ ```tsx
116
+ // src/components/blog/<YourFeed>.tsx — YOU build it; blog.astro mounts it (swap the import there).
117
+ import { useBlogFeed } from "../../hooks/blog/useBlogFeed";
118
+ import { postMetaLine } from "../../wix/blog/posts";
119
+ import { imgAttrs } from "../../wix/media";
120
+ import type { BlogCategory, BlogTag, PostPage } from "../../wix/blog/types";
121
+
122
+ export default function YourFeed(props: {
123
+ initialPage?: PostPage; // SSR props from blog.astro — pass straight to useBlogFeed;
124
+ initialCategories?: BlogCategory[]; // omitted in a SPA (client fetch)
125
+ initialTags?: BlogTag[];
126
+ }) {
127
+ const feed = useBlogFeed(props);
128
+ const { posts, categories, activeCategoryId, setActiveCategoryId, hasMore, loadMore, loadingMore, error } = feed;
129
+ // …you implement the render:
130
+ // • a category row when more than one category has posts (categories.filter(c => c.postCount > 0)):
131
+ // "All" (null) first, the active one marked by activeCategoryId, each calling setActiveCategoryId
132
+ // • error → a short inline message
133
+ // • posts === null → skeleton tiles; [] → your honest empty state
134
+ // • else YOUR grid of YOUR tiles: cover via
135
+ // <img {...imgAttrs(p.coverUrl, "(min-width: 1024px) 33vw, 100vw", 9 / 16)} alt={p.title} /> — {} when
136
+ // there is no cover, render your placeholder then; title WRAPS (no truncate); excerpt clamped;
137
+ // <time dateTime={p.dateISO}>{postMetaLine(p)}</time>; the tile links to `/blog/${p.slug}`
138
+ // • hasMore → your "load more" control calling loadMore() (disabled while loadingMore)
139
+ }
140
+ ```
141
+
142
+ ### The reference files for stacks where the components don't deploy
143
+
144
+ On `lib`, `static`, and a port, nothing under `components/` or `hooks/` arrives. The state
145
+ machines behind the hooks do arrive — `wix/blog/blog-feed-store.ts`, `post-store.ts` — so you never
146
+ rewrite them: create a store per surface, `subscribe`, render from `getState()`, call its actions.
147
+ Their `*State` interfaces are the render contract; read those. What you write is the rendering —
148
+ card, grid, filter row, post header — and for that read these first; they are tested code for
149
+ exactly that behaviour:
150
+
151
+ 1. `components/blog/BlogFeedView.tsx` — the filter row (only categories with posts, "All" first,
152
+ one active pill), the skeleton grid while `posts === null`, the empty state, load-more disabled
153
+ while loading; `PostCard` inside it is the tile: 16:9 cover, wrapping title, clamped excerpt,
154
+ the meta line in a `<time>`.
155
+ 2. `components/blog/PostView.tsx` — the states in order (not found → error → loading → article),
156
+ the categories eyebrow by `.label`, the tag chips, the body slot.
157
+ 3. `components/blog/RichContent.tsx` — what the body renderer is: the `@wix/ricos` viewer, a React
158
+ component that needs a bundler. On these stacks it doesn't run; the body renders from
159
+ `post.paragraphs` (one `<p>` per entry), and headings, lists, quotes, and inline images flatten
160
+ to text — say so in the closing message. A blog whose posts carry structure stays on Astro.
161
+
162
+ All under `references/blog/app/`.
163
+
80
164
  ### Wiring — Astro (default)
81
165
 
82
166
  1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass — merge into the
83
167
  other vertical's layout instead if both are deployed).
84
- 2. Write your components under `src/components/blog/` (new names — don't overwrite the
168
+ 2. Write your feed island under `src/components/blog/` (a new name — don't overwrite the
85
169
  references), swap the island import in `pages/blog.astro`, and restyle the
86
170
  `pages/blog/[...slug].astro` template (keep its frontmatter + SEO pieces + the
87
171
  `client:only="react"` RichContent island). **Author your surfaces in as few messages as
88
172
  possible** — batch multiple Writes per message.
89
173
  3. Write `pages/index.astro` (home) — it exists from the scaffold; Read it before overwriting.
90
174
 
175
+ ### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
176
+
177
+ Read the reference files listed above before writing any surface.
178
+
179
+ `deploy.mjs blog --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts` (the
180
+ visitor client, configured with the public client id), `media.ts`, `money.ts`, and `wix/blog/` —
181
+ `posts.ts`, `taxonomy.ts`, `types.ts`, the `*-core.ts` rules, and the two stores
182
+ `blog-feed-store.ts`, `post-store.ts`. None of it is React. The hooks and components don't ship on
183
+ this stack; the stores replace the hooks, and you write the components in your framework to the
184
+ contracts on this page:
185
+
186
+ - bind the stores with your framework's external-store primitive (Vue: `shallowRef` updated in
187
+ `subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
188
+ Solid: a signal set in `subscribe`). `createBlogFeedStore(options)` per feed (`start()` when
189
+ mounted, `stop()` when unmounted), `createPostStore({ slug, initialPost? })` per post surface.
190
+ State in, actions out — exactly the hooks' contracts above;
191
+ - the body: `@wix/ricos` is a React component — without React it doesn't render; use
192
+ `post.paragraphs` and say so in the closing message.
193
+
194
+ Routes `/blog`, `/blog/:slug` (null → your 404 view); dev server on 4321; a static build goes
195
+ through `npx @wix/cli@latest release` with `site.outputDirectory` pointing at the build folder,
196
+ an SSR build is hosted by you. Post-page tags from `post.title` and `post.excerpt`.
197
+
198
+ ### Wiring — static site (`--stack static`, no bundler)
199
+
200
+ Read the reference files listed above before writing any surface.
201
+
202
+ `deploy.mjs blog --stack static --out site` put the REST layer in `site/js/wix/` (browser ESM,
203
+ the `.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` —
204
+ pages, styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project
205
+ root (config, plan, seed output) is never the upload. Same function names and DTOs as the table
206
+ above, so the contracts on this page hold unchanged: `fetchPosts`, `fetchPostBySlug`,
207
+ `postMetaLine` from `./js/wix/posts.js`; `fetchBlogCategories`, `fetchBlogTags`,
208
+ `fetchCategoryBySlug`, `fetchTagBySlug` from `./js/wix/taxonomy.js`; `imgAttrs` from
209
+ `./js/wix/media.js`. The state machines ship too: `createBlogFeedStore` from
210
+ `./js/wix/blog-feed-store.js` (the feed — filters, cursor paging; `start()` once the page is up)
211
+ and `createPostStore` from `./js/wix/post-store.js` (the post and its resolved chips). No
212
+ components ship — you write the rendering in plain JS: one render function per surface that reads
213
+ `getState()`, called from `subscribe`, with the surface's controls calling the store's actions.
214
+ Pages are `blog.html` and `post.html?slug=…` (Wix static hosting serves files, not directories —
215
+ name the file and link to it). The body renders from `post.paragraphs` (no Ricos viewer without a
216
+ bundler — see the reference files note). Set `document.title` and the meta description from
217
+ `post.title` and `post.excerpt` once the post loads, as the Astro page does. The visitor token
218
+ persists in `localStorage` on its own; never mint per page. `npx @wix/cli@latest release`
219
+ uploads `site/`.
220
+
221
+ ### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
222
+
223
+ Read the reference files listed above before writing any surface.
224
+
225
+ Run `deploy.mjs blog --stack static` in the project folder anyway: `js/wix/` is both the
226
+ browser-side code and the readable spec. Reads render on the server: port `js/wix/posts.ts`,
227
+ `taxonomy.ts` and their `*-core.ts` to your language — the same six functions returning the same
228
+ DTO shapes as dicts, one anonymous visitor token per process for these public reads (mint and
229
+ refresh per `client.ts`) — and render the feed and the post page in your templates to the
230
+ contracts above, so titles and excerpts are in the HTML; post-page tags from `post.title` and
231
+ `post.excerpt`. The body is `paragraphs` server-side too (the Ricos viewer is React). Nothing in
232
+ this vertical runs on the visitor's behalf, so the browser side is optional: the feed's category
233
+ filter and load-more can run client-side on `./js/wix/blog-feed-store.js` exactly as the static
234
+ wiring above, or as plain server routes (`/blog?category=<slug>` via `fetchCategoryBySlug`,
235
+ paging by cursor). Routes stay `/blog`, `/blog/<slug>`.
236
+
237
+ **Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for
238
+ the reads, run at build time with one anonymous token; the generator must emit a page for every
239
+ post — walk `fetchPosts` by `nextCursor` until it is null, never only the first page. Run
240
+ `deploy.mjs blog --stack static --out <build dir>` so `js/wix/` is inside the output the pages
241
+ import from, point `site.outputDirectory` at that folder, `wix release`. Pages sit at different
242
+ depths (`/`, `/blog/…`): give the templates one base path to `js/wix/` (a template variable, or
243
+ root-relative `/js/wix/…`), never a relative `./js/wix/`. The frozen grid is the first paint; the
244
+ category filter and load-more still run client-side on it through `createBlogFeedStore()` from
245
+ `./js/wix/blog-feed-store.js`. Close with the live URL, the rebuild + release command, and one
246
+ line for the owner: posts published in the dashboard reach the site when that command runs.
247
+
91
248
  ### Wiring — React SPA (Vite etc.)
92
249
 
93
250
  Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
94
251
  plugins — deploy added the dep). Routes: `/blog` → your feed on `useBlogFeed`;
95
- `/blog/:slug` → your post surface on `usePost` (the `PostView` reference shows the shape).
252
+ `/blog/:slug` → your post surface on `usePost({ slug })` (the `PostView` reference shows the
253
+ shape; `notFound` → your 404 view). Deploy wrote the public client id into `wix/config.ts`;
254
+ nothing else to configure.
96
255
 
97
256
  ## Hard rules
98
257
 
99
- - **The body renders only through `RichContent`** — a post body is a Ricos document, not
100
- HTML and not text: never `set:html`/innerHTML it, never stringify its nodes, never write
101
- your own node walker. `paragraphs` is the honest fallback it already handles.
102
- - **Route by `slug` through the shipped functions** — never hand-build a post/category/tag
103
- URL from ids; display taxonomy by `.label` (the API has no `.name`).
104
- - **`notFound` means not found** — render your 404 state; never invent a post. Only
105
- published posts come back, so a "missing" post is usually an unseeded/unpublished one.
106
- - **Never hand-build a wixstatic image URL** — `coverUrl` is already resolved; anything else
107
- goes through `wix/media.ts`.
108
- - Theme via the `@theme` tokens; no parallel theme files, no hardcoded palettes.
109
- - Live data or an honest empty state — never mock posts, authors, dates, or read times; no
110
- stock placeholder covers. No author bylines or comment/like UI — the DTOs don't carry
111
- them; don't fabricate engagement.
258
+ - **The body renders only through `RichContent`** where it deploys (Astro, React) — a post body
259
+ is a Ricos document, not HTML and not text: never `set:html`/innerHTML it, never stringify its
260
+ nodes, never write your own node walker. Where it doesn't deploy, `paragraphs` is the honest
261
+ body, and the closing message says so.
262
+ - **Data logic only through the shipped exports** — never rewrite their internals or re-derive a
263
+ request shape; extend by adding a function in `wix/blog/` (API contracts: the `wix-docs` skill).
264
+ - **Route by `slug` through the shipped functions** — never hand-build a post/category/tag URL
265
+ from ids; display taxonomy by `.label` (the API has no `.name`).
266
+ - **`notFound` means not found** — render your 404 state; never invent a post. Only published
267
+ posts come back, so a "missing" post is usually an unseeded/unpublished one.
268
+ - **Filter and page at the source** — `useBlogFeed`'s filters and `fetchPosts` run on Wix before
269
+ cursor paging; never filter a loaded page client-side, never raise the page limit instead of
270
+ paging (`hasMore`/`loadMore`).
271
+ - **Never hand-build a wixstatic image URL** — `coverUrl` is already resolved; anything else goes
272
+ through `wix/media.ts`.
273
+ - Where the shipped components deploy: theme via the `@theme` tokens, your markup in Tailwind
274
+ utilities on the same tokens; no parallel theme files, no hardcoded palettes. Where they don't
275
+ (`lib`, `static`, a port): style with what your stack does well, on one token set of your own.
276
+ - Live data or an honest empty state — never mock posts, authors, dates, or read times; no stock
277
+ placeholder covers. No author bylines or comment/like UI — the DTOs don't carry them.
112
278
  - Keep the post page's SEO pieces and `[...slug]` rest param exactly as shipped.
279
+ - **Call every hook before any conditional return.** Hooks first, branches after.
113
280
 
114
281
  ## Point the user to their dashboard
115
282
 
116
- Give the owner the dashboard link plus the Blog pages — the deploy step's JSON printed
117
- `dashboardUrl`; append `/blog/posts` for writing/editing posts and `/blog/categories` for
118
- the category menu. Only published posts appear on the site.
283
+ Hand the owner these links — `{siteId}` is `siteId` in `wix.config.json` (the deploy JSON prints it as
284
+ `dashboardUrl`).
285
+
286
+ | page | `https://manage.wix.com/dashboard/{siteId}/` + |
287
+ |---|---|
288
+ | Posts (published and drafts) | `blog/posts` |
289
+ | Categories | `blog/categories` |
290
+ | Tags | `blog/tags` |
291
+ | Writers | `blog/writers` |
292
+ | Comments | `blog/comments` |
293
+ | Blog settings | `blog/settings` |
294
+
295
+ Only published posts appear on the site.
119
296
 
120
297
  ## Seeding
121
298
 
122
299
  Per `seed/SEED.md` — plain-data `plan.json` into `seed-blog.mjs` from the project root.
123
300
  Seed posts that exercise the UI (~3 posts, 2 categories when the brief has sections, varied
124
301
  content blocks, a cover image per post).
125
-
126
- ## Verify (before declaring done)
127
-
128
- - [ ] `/blog` renders live posts SSR (view-source shows titles) through YOUR components;
129
- empty blog shows your honest empty state.
130
- - [ ] A post page renders the full body via `RichContent` (headings/quotes/lists show, not
131
- raw text), with your header design; a bad slug returns 404/not-found.
132
- - [ ] Category filter narrows the feed (server-side — check the network tab, not a hidden
133
- client filter); load-more appends the next page.
134
- - [ ] Post page view-source carries the SEO tags (Astro).
135
- - [ ] Card/grid/index/post surfaces/home are YOUR designs on the tokens; data-layer/hook
136
- files unedited; `RichContent` wired as-is.
137
- - [ ] Dashboard links handed to the owner.
@@ -2,6 +2,7 @@
2
2
  // Correct and complete; per the skill's model you design and build your own on useBlogFeed.
3
3
  import type { ComponentType, ReactNode } from "react";
4
4
  import { useBlogFeed } from "../../hooks/blog/useBlogFeed";
5
+ import { postMetaLine } from "../../wix/blog/posts";
5
6
  import type { BlogCategory, BlogTag, PostPage, PostSummary } from "../../wix/blog/types";
6
7
 
7
8
  export interface LinkLikeProps {
@@ -38,9 +39,7 @@ export function PostCard({ post, postHref = (slug) => `/blog/${slug}`, LinkCompo
38
39
  <h3 className="mt-3 text-base font-semibold leading-snug text-foreground">{post.title}</h3>
39
40
  {post.excerpt && <p className="mt-1 line-clamp-2 text-sm text-muted-foreground">{post.excerpt}</p>}
40
41
  <p className="mt-2 text-xs text-muted-foreground">
41
- {post.dateLabel && <time dateTime={post.dateISO}>{post.dateLabel}</time>}
42
- {post.dateLabel && post.minutesToRead > 0 ? " · " : ""}
43
- {post.minutesToRead > 0 ? `${post.minutesToRead} min read` : ""}
42
+ <time dateTime={post.dateISO}>{postMetaLine(post)}</time>
44
43
  </p>
45
44
  </LinkComponent>
46
45
  );
@@ -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
  }