@wix/agent-skills 1.19.2 → 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.
- package/package.json +1 -1
- package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
- package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
- package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
- package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
- package/skills/wix-headless/references/CAPABILITIES.md +2 -2
- package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
- package/skills/wix-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +28 -31
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +55 -1
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +371 -81
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +136 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +53 -136
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +73 -230
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -5
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
- package/skills/wix-manage/SKILL.md +4 -1
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-manage/references/events/create-wix-event.md +46 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
- package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
- package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +19 -4
- package/skills/wix-vibe-headless/platforms/generic.md +6 -0
- package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
- package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
- package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
|
@@ -1,70 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
|
11
|
-
slug: string;
|
|
12
|
-
initialPost?: PostDetail;
|
|
13
|
-
initialCategories?: BlogCategory[];
|
|
14
|
-
initialTags?: BlogTag[];
|
|
15
|
-
}
|
|
9
|
+
export type UsePostOptions = PostStoreOptions;
|
|
16
10
|
|
|
17
|
-
export
|
|
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
|
|
30
|
-
|
|
31
|
-
const
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
@@ -1,71 +1,24 @@
|
|
|
1
|
-
// Post reads (Wix Blog V3) — the only file that touches raw post entities
|
|
2
|
-
// returns is a plain DTO from ./types.
|
|
3
|
-
//
|
|
1
|
+
// Post reads (Wix Blog V3) over the SDK — the only file that touches raw post entities on this
|
|
2
|
+
// transport. Everything it returns is a plain DTO from ./types. The rules and mappers live in
|
|
3
|
+
// ./posts-core (shared with the REST twin in references/blog/rest/); this file is the transport
|
|
4
|
+
// only. Copy as-is; extend by adding functions, not by editing these. Blog posts are NOT CMS
|
|
5
|
+
// collections — always @wix/blog, never @wix/data.
|
|
4
6
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/posts-stats/query-posts.md
|
|
5
7
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/posts-stats/get-post-by-slug.md
|
|
6
8
|
import { posts as postsModule } from "@wix/blog";
|
|
7
9
|
import { wixModule } from "../sdk";
|
|
8
10
|
import { imgSrc } from "../media";
|
|
9
|
-
import
|
|
11
|
+
import { BLOG_APP_ID, DETAIL_FIELDSETS, postMetaLine, toDetail, toSummary, type FetchPostsOptions, type Raw } from "./posts-core";
|
|
12
|
+
import type { PostDetail, PostPage } from "./types";
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/** The Wix Blog app id (Astro item-page routing; comments use it as appId). */
|
|
14
|
-
export const BLOG_APP_ID = "14bcded7-0066-7c35-14d7-466cb3f09103";
|
|
15
|
-
|
|
16
|
-
type Raw = Record<string, any>;
|
|
14
|
+
export { BLOG_APP_ID, postMetaLine, type FetchPostsOptions };
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
if (!value) return { dateLabel: "", dateISO: "" };
|
|
20
|
-
const d = value instanceof Date ? value : new Date(String(value));
|
|
21
|
-
if (Number.isNaN(d.getTime())) return { dateLabel: "", dateISO: "" };
|
|
22
|
-
return {
|
|
23
|
-
dateLabel: d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }),
|
|
24
|
-
dateISO: d.toISOString(),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function toSummary(raw: Raw): PostSummary {
|
|
29
|
-
return {
|
|
30
|
-
id: raw._id ?? "",
|
|
31
|
-
slug: raw.slug ?? "",
|
|
32
|
-
title: raw.title ?? "",
|
|
33
|
-
excerpt: raw.excerpt ?? "",
|
|
34
|
-
...dateParts(raw.firstPublishedDate),
|
|
35
|
-
minutesToRead: raw.minutesToRead ?? 0,
|
|
36
|
-
featured: raw.featured === true,
|
|
37
|
-
pinned: raw.pinned === true,
|
|
38
|
-
// The cover is a media STRING (wix:image:// or https) at media.wixMedia.image.
|
|
39
|
-
coverUrl: imgSrc(raw.media?.wixMedia?.image, 1200, 675),
|
|
40
|
-
categoryIds: raw.categoryIds ?? [],
|
|
41
|
-
tagIds: raw.tagIds ?? [],
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function toDetail(raw: Raw): PostDetail {
|
|
46
|
-
return {
|
|
47
|
-
...toSummary(raw),
|
|
48
|
-
richContent: raw.richContent ?? null,
|
|
49
|
-
// contentText is plain text — split on newlines for the fallback body.
|
|
50
|
-
paragraphs: String(raw.contentText ?? "")
|
|
51
|
-
.split("\n")
|
|
52
|
-
.map((s: string) => s.trim())
|
|
53
|
-
.filter(Boolean),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface FetchPostsOptions {
|
|
58
|
-
limit?: number;
|
|
59
|
-
/** `nextCursor` from a previous page. */
|
|
60
|
-
cursor?: string | null;
|
|
61
|
-
/** Server-side filters (first page only — the cursor carries them on later pages). */
|
|
62
|
-
categoryId?: string | null;
|
|
63
|
-
tagId?: string | null;
|
|
64
|
-
}
|
|
16
|
+
const posts = wixModule(postsModule);
|
|
65
17
|
|
|
66
18
|
/**
|
|
67
19
|
* One page of published posts, newest first (pinned posts lead). Only published posts come
|
|
68
20
|
* back to a visitor token — a "missing" post usually wasn't published, not a query bug.
|
|
21
|
+
* The builder below spells posts-core's feedQuery: sort + filters on the first page, the cursor alone after.
|
|
69
22
|
*/
|
|
70
23
|
export async function fetchPosts({ limit = 20, cursor, categoryId, tagId }: FetchPostsOptions = {}): Promise<PostPage> {
|
|
71
24
|
let q = posts.queryPosts().limit(limit);
|
|
@@ -79,7 +32,7 @@ export async function fetchPosts({ limit = 20, cursor, categoryId, tagId }: Fetc
|
|
|
79
32
|
}
|
|
80
33
|
const res = await q.find();
|
|
81
34
|
return {
|
|
82
|
-
posts: (res.items ?? []).map((p: Raw) => toSummary(p)),
|
|
35
|
+
posts: (res.items ?? []).map((p: Raw) => toSummary(p, imgSrc)),
|
|
83
36
|
nextCursor: res.hasNext() ? (res.cursors?.next ?? null) : null,
|
|
84
37
|
};
|
|
85
38
|
}
|
|
@@ -90,10 +43,10 @@ export async function fetchPosts({ limit = 20, cursor, categoryId, tagId }: Fetc
|
|
|
90
43
|
*/
|
|
91
44
|
export async function fetchPostBySlug(slug: string): Promise<PostDetail | null> {
|
|
92
45
|
const res = await posts
|
|
93
|
-
.queryPosts({ fieldsets: [
|
|
46
|
+
.queryPosts({ fieldsets: [...DETAIL_FIELDSETS] as any })
|
|
94
47
|
.eq("slug", slug)
|
|
95
48
|
.limit(1)
|
|
96
49
|
.find();
|
|
97
50
|
const raw = res.items?.[0];
|
|
98
|
-
return raw ? toDetail(raw as Raw) : null;
|
|
51
|
+
return raw ? toDetail(raw as Raw, imgSrc) : null;
|
|
99
52
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Category + tag rules and DTO mapping — transport-agnostic, imported by both ./taxonomy.ts (SDK)
|
|
2
|
+
// and the REST twin in references/blog/rest/taxonomy.ts (fetch). Raw entities may carry `_id`
|
|
3
|
+
// (SDK) or `id` (REST). Has its own rawId/mediaValue (copies of posts-core's) so it stands alone
|
|
4
|
+
// when stripped; imports are type-only.
|
|
5
|
+
import type { BlogCategory, BlogTag } from "./types";
|
|
6
|
+
import type { ImgSrc, Raw } from "./posts-core";
|
|
7
|
+
|
|
8
|
+
/** Menu order — displayPosition ascending; -1 (unplaced) sorts first, which is also what the SDK path shows. */
|
|
9
|
+
export const CATEGORY_SORT = [{ fieldName: "displayPosition", order: "ASC" }] as const;
|
|
10
|
+
/** Most-published-posts first. */
|
|
11
|
+
export const TAG_SORT = [{ fieldName: "publishedPostCount", order: "DESC" }] as const;
|
|
12
|
+
/** Blog taxonomies are small — one read of up to 100 is the whole list. */
|
|
13
|
+
export const TAXONOMY_LIMIT = 100;
|
|
14
|
+
|
|
15
|
+
const rawId = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
|
|
16
|
+
|
|
17
|
+
// A copy of posts-core's mediaValue: the REST Image object → the wix:image form imgSrc scales.
|
|
18
|
+
function mediaValue(value: unknown): string {
|
|
19
|
+
if (!value) return "";
|
|
20
|
+
if (typeof value === "string") return value;
|
|
21
|
+
const o = value as Raw;
|
|
22
|
+
if (o.id && !String(o.id).startsWith("http")) {
|
|
23
|
+
const size = o.width && o.height ? `#originWidth=${o.width}&originHeight=${o.height}` : "";
|
|
24
|
+
return `wix:image://v1/${o.id}/${o.filename ?? ""}${size}`;
|
|
25
|
+
}
|
|
26
|
+
return o.url ?? o.image ?? "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function toCategory(raw: Raw, imgSrc: ImgSrc): BlogCategory {
|
|
30
|
+
return {
|
|
31
|
+
id: rawId(raw),
|
|
32
|
+
slug: raw.slug ?? "",
|
|
33
|
+
label: raw.label ?? "", // the API's display name is `label`, never `name`
|
|
34
|
+
description: raw.description ?? "",
|
|
35
|
+
postCount: raw.postCount ?? 0,
|
|
36
|
+
coverUrl: imgSrc(mediaValue(raw.coverImage), 1200, 675),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toTag(raw: Raw): BlogTag {
|
|
41
|
+
return {
|
|
42
|
+
id: rawId(raw),
|
|
43
|
+
slug: raw.slug ?? "",
|
|
44
|
+
label: raw.label ?? "",
|
|
45
|
+
postCount: raw.publishedPostCount ?? 0, // PUBLISHED posts — postCount also counts drafts
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,42 +1,24 @@
|
|
|
1
|
-
// Category + tag reads (Wix Blog V3) — the only file that touches raw taxonomy
|
|
2
|
-
// Everything it returns is a plain DTO from ./types.
|
|
1
|
+
// Category + tag reads (Wix Blog V3) over the SDK — the only file that touches raw taxonomy
|
|
2
|
+
// entities on this transport. Everything it returns is a plain DTO from ./types. The mappers live
|
|
3
|
+
// in ./taxonomy-core (shared with the REST twin in references/blog/rest/); this file is the
|
|
4
|
+
// transport only. Copy as-is; extend by adding functions.
|
|
3
5
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/category/query-categories.md
|
|
4
6
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/tags/query-tags.md
|
|
5
7
|
import { categories as categoriesModule, tags as tagsModule } from "@wix/blog";
|
|
6
8
|
import { wixModule } from "../sdk";
|
|
7
9
|
import { imgSrc } from "../media";
|
|
10
|
+
import { TAXONOMY_LIMIT, toCategory, toTag } from "./taxonomy-core";
|
|
11
|
+
import type { Raw } from "./posts-core";
|
|
8
12
|
import type { BlogCategory, BlogTag } from "./types";
|
|
9
13
|
|
|
10
14
|
const categories = wixModule(categoriesModule);
|
|
11
15
|
const tags = wixModule(tagsModule);
|
|
12
16
|
|
|
13
|
-
type Raw = Record<string, any>;
|
|
14
|
-
|
|
15
|
-
function toCategory(raw: Raw): BlogCategory {
|
|
16
|
-
return {
|
|
17
|
-
id: raw._id ?? "",
|
|
18
|
-
slug: raw.slug ?? "",
|
|
19
|
-
label: raw.label ?? "",
|
|
20
|
-
description: raw.description ?? "",
|
|
21
|
-
postCount: raw.postCount ?? 0,
|
|
22
|
-
coverUrl: imgSrc(raw.coverImage, 1200, 675),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function toTag(raw: Raw): BlogTag {
|
|
27
|
-
return {
|
|
28
|
-
id: raw._id ?? "",
|
|
29
|
-
slug: raw.slug ?? "",
|
|
30
|
-
label: raw.label ?? "",
|
|
31
|
-
postCount: raw.publishedPostCount ?? 0,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
17
|
/** Categories in menu order (displayPosition) — non-fatal (empty array on failure). */
|
|
36
18
|
export async function fetchBlogCategories(): Promise<BlogCategory[]> {
|
|
37
19
|
try {
|
|
38
|
-
const res = await categories.queryCategories().ascending("displayPosition").limit(
|
|
39
|
-
return (res.items ?? []).map((c: Raw) => toCategory(c)).filter((c) => c.id);
|
|
20
|
+
const res = await categories.queryCategories().ascending("displayPosition").limit(TAXONOMY_LIMIT).find();
|
|
21
|
+
return (res.items ?? []).map((c: Raw) => toCategory(c, imgSrc)).filter((c) => c.id);
|
|
40
22
|
} catch {
|
|
41
23
|
return [];
|
|
42
24
|
}
|
|
@@ -45,7 +27,7 @@ export async function fetchBlogCategories(): Promise<BlogCategory[]> {
|
|
|
45
27
|
/** Tags, most-published-posts first — non-fatal (empty array on failure). */
|
|
46
28
|
export async function fetchBlogTags(): Promise<BlogTag[]> {
|
|
47
29
|
try {
|
|
48
|
-
const res = await tags.queryTags().descending("publishedPostCount").limit(
|
|
30
|
+
const res = await tags.queryTags().descending("publishedPostCount").limit(TAXONOMY_LIMIT).find();
|
|
49
31
|
return (res.items ?? []).map((t: Raw) => toTag(t)).filter((t) => t.id);
|
|
50
32
|
} catch {
|
|
51
33
|
return [];
|
|
@@ -59,7 +41,7 @@ export async function fetchBlogTags(): Promise<BlogTag[]> {
|
|
|
59
41
|
export async function fetchCategoryBySlug(slug: string): Promise<BlogCategory | null> {
|
|
60
42
|
try {
|
|
61
43
|
const res = await categories.getCategoryBySlug(slug);
|
|
62
|
-
return res.category ? toCategory(res.category as Raw) : null;
|
|
44
|
+
return res.category ? toCategory(res.category as Raw, imgSrc) : null;
|
|
63
45
|
} catch {
|
|
64
46
|
return null;
|
|
65
47
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// template below freely. The body island is client:only — the ricos viewer breaks under SSR.
|
|
8
8
|
import SiteLayout from "../../layouts/SiteLayout.astro";
|
|
9
9
|
import RichContent from "../../components/blog/RichContent";
|
|
10
|
-
import { fetchPostBySlug } from "../../wix/blog/posts";
|
|
10
|
+
import { fetchPostBySlug, postMetaLine } from "../../wix/blog/posts";
|
|
11
11
|
import { fetchBlogCategories, fetchBlogTags } from "../../wix/blog/taxonomy";
|
|
12
12
|
import type { BlogCategory, BlogTag } from "../../wix/blog/types";
|
|
13
13
|
import { WIX_APPS } from "@wix/essentials";
|
|
@@ -70,9 +70,7 @@ const postTags = allTags.filter((t) => post!.tagIds.includes(t.id));
|
|
|
70
70
|
{postCategories.length > 0 && <p class="eyebrow">{postCategories.map((c) => c.label).join(" · ")}</p>}
|
|
71
71
|
<h1 class="mt-2 text-3xl font-semibold tracking-tight">{post.title}</h1>
|
|
72
72
|
<p class="mt-3 text-sm text-muted-foreground">
|
|
73
|
-
|
|
74
|
-
{post.dateLabel && post.minutesToRead > 0 ? " · " : ""}
|
|
75
|
-
{post.minutesToRead > 0 ? `${post.minutesToRead} min read` : ""}
|
|
73
|
+
<time datetime={post.dateISO}>{postMetaLine(post)}</time>
|
|
76
74
|
</p>
|
|
77
75
|
{
|
|
78
76
|
post.coverUrl && (
|