@wix/agent-skills 1.17.0 → 1.18.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/SKILL.md +115 -12
- package/skills/wix-app/references/APP_MARKET_REVIEW.md +1 -1
- package/skills/wix-app/references/BACKEND_API.md +18 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +6 -0
- package/skills/wix-app/references/DASHBOARD_MODAL.md +10 -2
- package/skills/wix-app/references/DASHBOARD_PAGE.md +32 -70
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +74 -83
- package/skills/wix-app/references/SITE_PLUGIN.md +12 -0
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +74 -68
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +79 -0
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +73 -0
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +52 -0
- package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +184 -0
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +12 -11
- package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +1 -0
- package/skills/wix-app/references/editor-react-component/BROWSER-SUPPORT.md +56 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +16 -8
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +47 -14
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +3 -0
- package/skills/wix-base44-connector/SKILL.md +133 -44
- package/skills/wix-base44-connector/references/CAPABILITY_MAP.md +78 -0
- package/skills/wix-base44-connector/scripts/utils.js +216 -30
- package/skills/wix-headless/entry/skill.md +1 -1
- package/skills/wix-headless/references/CAPABILITIES.md +9 -1
- package/skills/wix-headless/references/DESIGN.md +1 -0
- package/skills/wix-headless/references/DISCOVERY.md +2 -1
- package/skills/wix-headless/references/IMAGE_GENERATION.md +1 -0
- package/skills/wix-headless/references/SDK_HANDOFF.md +4 -1
- package/skills/wix-headless/references/SEED.md +2 -1
- package/skills/wix-headless/references/SETUP.md +1 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +2 -2
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +26 -8
- package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +26 -10
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +8 -8
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +1 -1
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +1 -1
- package/skills/wix-headless/references/inline-recipes/how-to-code-rentals.md +254 -0
- package/skills/wix-headless/references/inline-recipes/setup-forms.md +57 -10
- package/skills/wix-headless/references/inline-recipes/setup-online-store.md +13 -0
- package/skills/wix-headless/references/inline-recipes/setup-rentals.md +254 -0
- package/skills/wix-headless/references/non-astro.md +1 -1
- package/skills/wix-headless-fast/SKILL.md +166 -0
- package/skills/wix-headless-fast/entry/skill.md +99 -0
- package/skills/wix-headless-fast/install/deploy.mjs +433 -0
- package/skills/wix-headless-fast/install/fast-path.mjs +141 -0
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +137 -0
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +137 -0
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +113 -0
- package/skills/wix-headless-fast/references/blog/app/components/blog/RichContent.tsx +42 -0
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +123 -0
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +70 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +99 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +76 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/types.ts +62 -0
- package/skills/wix-headless-fast/references/blog/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +99 -0
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog.astro +24 -0
- package/skills/wix-headless-fast/references/blog/lock/astro/package-lock.json +16156 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +72 -0
- package/skills/wix-headless-fast/references/blog/seed/seed-blog.mjs +287 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +129 -0
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +143 -0
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServicesView.tsx +119 -0
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +138 -0
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +54 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +206 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +96 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +70 -0
- package/skills/wix-headless-fast/references/bookings/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/bookings/app-astro/pages/services/[slug].astro +76 -0
- package/skills/wix-headless-fast/references/bookings/app-astro/pages/services.astro +22 -0
- package/skills/wix-headless-fast/references/bookings/lock/astro/package-lock.json +11962 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +61 -0
- package/skills/wix-headless-fast/references/bookings/seed/seed-bookings.mjs +302 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +179 -0
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +83 -0
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +57 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +151 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/types.ts +75 -0
- package/skills/wix-headless-fast/references/cms/app-astro/layouts/SiteLayout.astro +41 -0
- package/skills/wix-headless-fast/references/cms/lock/astro/package-lock.json +11262 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +103 -0
- package/skills/wix-headless-fast/references/cms/seed/seed-cms.mjs +324 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +149 -0
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +66 -0
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +176 -0
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +118 -0
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +117 -0
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +59 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +136 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +128 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/types.ts +61 -0
- package/skills/wix-headless-fast/references/events/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/events/app-astro/pages/event-confirmation.astro +12 -0
- package/skills/wix-headless-fast/references/events/app-astro/pages/events/[slug].astro +80 -0
- package/skills/wix-headless-fast/references/events/app-astro/pages/events.astro +21 -0
- package/skills/wix-headless-fast/references/events/lock/astro/package-lock.json +11884 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +86 -0
- package/skills/wix-headless-fast/references/events/seed/seed-events.mjs +276 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +150 -0
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +317 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +269 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +231 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/types.ts +135 -0
- package/skills/wix-headless-fast/references/forms/lock/astro/package-lock.json +10593 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +108 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +320 -0
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +63 -0
- package/skills/wix-headless-fast/references/members/app/components/members/AccountView.tsx +60 -0
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +155 -0
- package/skills/wix-headless-fast/references/members/app/components/members/MemberMenu.tsx +78 -0
- package/skills/wix-headless-fast/references/members/app/components/members/RequireAuth.tsx +47 -0
- package/skills/wix-headless-fast/references/members/app/hooks/members/useMember.ts +66 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +88 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +53 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/member-store.ts +122 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +50 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/types.ts +21 -0
- package/skills/wix-headless-fast/references/members/app-astro/layouts/SiteLayout.astro +42 -0
- package/skills/wix-headless-fast/references/members/app-astro/pages/account.astro +10 -0
- package/skills/wix-headless-fast/references/members/app-astro/pages/login.astro +11 -0
- package/skills/wix-headless-fast/references/members/lock/astro/package-lock.json +10329 -0
- package/skills/wix-headless-fast/references/members/seed/SEED.md +39 -0
- package/skills/wix-headless-fast/references/members/seed/seed-members.mjs +94 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +134 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +110 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +90 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +115 -0
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +59 -0
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +39 -0
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +165 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/types.ts +55 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/pages/portfolio/[slug].astro +30 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/pages/portfolio.astro +21 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/pages/projects/[slug].astro +29 -0
- package/skills/wix-headless-fast/references/portfolio/lock/astro/package-lock.json +10197 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +78 -0
- package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +258 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +133 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +58 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +126 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +37 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +39 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +75 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +45 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/types.ts +30 -0
- package/skills/wix-headless-fast/references/pricing-plans/app-astro/layouts/SiteLayout.astro +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app-astro/pages/plans/[slug].astro +40 -0
- package/skills/wix-headless-fast/references/pricing-plans/app-astro/pages/plans.astro +21 -0
- package/skills/wix-headless-fast/references/pricing-plans/lock/astro/package-lock.json +11226 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +71 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/seed-pricing-plans.mjs +279 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +165 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +142 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartButton.tsx +26 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +121 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/ReservationView.tsx +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +55 -0
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useOrderCart.ts +43 -0
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +177 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +165 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/order-store.ts +103 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +254 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +146 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/types.ts +168 -0
- package/skills/wix-headless-fast/references/restaurants/app-astro/layouts/SiteLayout.astro +47 -0
- package/skills/wix-headless-fast/references/restaurants/app-astro/pages/menu.astro +22 -0
- package/skills/wix-headless-fast/references/restaurants/app-astro/pages/reservations.astro +12 -0
- package/skills/wix-headless-fast/references/restaurants/lock/astro/package-lock.json +12560 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +81 -0
- package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +479 -0
- package/skills/wix-headless-fast/references/shared/CONTENT.md +22 -0
- package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +73 -0
- package/skills/wix-headless-fast/references/shared/DESIGN.md +26 -0
- package/skills/wix-headless-fast/references/shared/app/styles/global.css +52 -0
- package/skills/wix-headless-fast/references/shared/app/wix/config.ts +15 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +27 -0
- package/skills/wix-headless-fast/references/shared/app/wix/money.ts +24 -0
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +76 -0
- package/skills/wix-headless-fast/references/shared/capabilities/media-upload/app/wix/media-upload/client.ts +46 -0
- package/skills/wix-headless-fast/references/shared/capabilities/media-upload/app-astro/pages/api/wix/media-upload.ts +46 -0
- package/skills/wix-headless-fast/references/shared/seed/images.mjs +145 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +149 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartButton.tsx +26 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +121 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +100 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +130 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +48 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +66 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +103 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useCart.ts +48 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +140 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +80 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +101 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +221 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +215 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +109 -0
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +46 -0
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +56 -0
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +23 -0
- package/skills/wix-headless-fast/references/storefront/lock/astro/package-lock.json +12247 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +85 -0
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +415 -0
- package/skills/wix-manage/SKILL.md +113 -89
- package/skills/wix-manage/references/dashboard-navigation/routes.json +12 -52
- package/skills/wix-manage/references/ecommerce/shipping/ecom-shipping-api.md +0 -2
- package/skills/wix-manage/references/forms/create-form.md +272 -184
- package/skills/wix-manage/references/forms/forms-dashboard-navigation.md +13 -10
- package/skills/wix-manage/references/forms/update-form.md +122 -0
- package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +141 -0
- package/skills/wix-manage/references/google-business-profile/connect-google-business-profile.md +134 -0
- package/skills/wix-manage/references/google-business-profile/manage-google-business-profile-locations.md +182 -0
- package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +3 -0
- package/skills/wix-manage/references/media/generate-image-with-ai.md +86 -0
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +37 -7
- package/skills/wix-manage/references/stores/update-product-with-options.md +83 -0
- package/skills/wix-vibe-headless/SKILL.md +18 -4
- package/skills/wix-vibe-headless/install/deploy.cjs +12 -4
- package/skills/wix-vibe-headless/install/pin-agents-md.cjs +2 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +3 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +3 -1
- package/skills/wix-vibe-headless/platforms/base44.md +27 -61
- package/skills/wix-vibe-headless/platforms/generic.md +6 -3
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/RentalDurationPicker.jsx +67 -0
- package/skills/wix-vibe-headless/references/bookings/app/hooks/useRentalDuration.js +128 -0
- package/skills/wix-vibe-headless/references/bookings/app/hooks/useServiceDetail.js +5 -3
- package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-rentals.js +250 -0
- package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-services.js +12 -5
- package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +41 -0
- package/skills/wix-vibe-headless/references/bookings/seed/seed-rentals.js +267 -0
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +6 -3
- package/skills/wix-vibe-headless/references/forms/INSTRUCTIONS.md +486 -0
- package/skills/wix-vibe-headless/references/forms/app/hooks/useWixForm.js +425 -0
- package/skills/wix-vibe-headless/references/forms/app/lib/wix-form-schema-utils.js +187 -0
- package/skills/wix-vibe-headless/references/forms/app/rest/wix-forms-submissions.js +238 -0
- package/skills/wix-vibe-headless/references/forms/app/rest/wix-forms.js +108 -0
- package/skills/wix-vibe-headless/references/forms/seed/SEED.md +1836 -0
- package/skills/wix-vibe-headless/references/rentals/INSTRUCTIONS.md +13 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +137 -84
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +15 -8
- package/skills/wix-vibe-headless/references/storefront/app/lib/storeImage.js +37 -0
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +18 -6
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +30 -7
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +46 -10
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +0 -110
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductGallery.jsx +0 -51
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +0 -49
- package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +0 -92
- package/skills/wix-vibe-headless/references/storefront/app/pages/ProductDetail.jsx +0 -139
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Blog — playbook
|
|
2
|
+
|
|
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.
|
|
9
|
+
|
|
10
|
+
## The file map (deployed into `src/`)
|
|
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.
|
|
15
|
+
|
|
16
|
+
| file | what it is |
|
|
17
|
+
|---|---|
|
|
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) |
|
|
27
|
+
|
|
28
|
+
Astro stack additionally gets:
|
|
29
|
+
|
|
30
|
+
| file | what it is |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `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
|
+
| `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) |
|
|
35
|
+
|
|
36
|
+
## What you build — the design job
|
|
37
|
+
|
|
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
|
|
41
|
+
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.
|
|
47
|
+
|
|
48
|
+
Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
|
|
49
|
+
Style everything with Tailwind utilities on the tokens. Dark theme: the ricos CSS hardcodes
|
|
50
|
+
near-black text — add a global override scoping `.ricos-content` to the foreground token
|
|
51
|
+
(in Astro use `<style is:global>`; React islands don't inherit scoped Astro styles).
|
|
52
|
+
|
|
53
|
+
### The contracts your components consume
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// PostSummary (tiles) — display-ready:
|
|
57
|
+
// { id, slug, title, excerpt, dateLabel /* "Aug 26, 2026" | "" */, dateISO,
|
|
58
|
+
// minutesToRead /* 0 = unknown */, featured, pinned, coverUrl /* "" = no cover */,
|
|
59
|
+
// categoryIds, tagIds }
|
|
60
|
+
// PostDetail adds: richContent (Ricos JSON | null — render ONLY via RichContent),
|
|
61
|
+
// paragraphs (plain-text fallback body).
|
|
62
|
+
// BlogCategory: { id, slug, label, description, postCount, coverUrl } // display .label
|
|
63
|
+
// BlogTag: { id, slug, label, postCount }
|
|
64
|
+
|
|
65
|
+
// useBlogFeed({ initialPage?, initialCategories?, initialTags?, pageSize? }) →
|
|
66
|
+
// { posts: PostSummary[]|null /* null = loading → skeletons */,
|
|
67
|
+
// categories, tags,
|
|
68
|
+
// activeCategoryId, setActiveCategoryId(id|null), // server-side filter
|
|
69
|
+
// activeTagId, setActiveTagId(id|null), // mutually exclusive with category
|
|
70
|
+
// hasMore, loadMore(), loadingMore, error }
|
|
71
|
+
|
|
72
|
+
// usePost({ slug, initialPost?, initialCategories?, initialTags? }) →
|
|
73
|
+
// { post: PostDetail|null, notFound /* true = render a 404 state, never invent a post */,
|
|
74
|
+
// categories, tags /* THIS post's, resolved — display .label */, error }
|
|
75
|
+
|
|
76
|
+
// <RichContent content={post.richContent} fallbackParagraphs={post.paragraphs} />
|
|
77
|
+
// — the ONLY body render path. In Astro: client:only="react".
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Wiring — Astro (default)
|
|
81
|
+
|
|
82
|
+
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass — merge into the
|
|
83
|
+
other vertical's layout instead if both are deployed).
|
|
84
|
+
2. Write your components under `src/components/blog/` (new names — don't overwrite the
|
|
85
|
+
references), swap the island import in `pages/blog.astro`, and restyle the
|
|
86
|
+
`pages/blog/[...slug].astro` template (keep its frontmatter + SEO pieces + the
|
|
87
|
+
`client:only="react"` RichContent island). **Author your surfaces in as few messages as
|
|
88
|
+
possible** — batch multiple Writes per message.
|
|
89
|
+
3. Write `pages/index.astro` (home) — it exists from the scaffold; Read it before overwriting.
|
|
90
|
+
|
|
91
|
+
### Wiring — React SPA (Vite etc.)
|
|
92
|
+
|
|
93
|
+
Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
|
|
94
|
+
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).
|
|
96
|
+
|
|
97
|
+
## Hard rules
|
|
98
|
+
|
|
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.
|
|
112
|
+
- Keep the post page's SEO pieces and `[...slug]` rest param exactly as shipped.
|
|
113
|
+
|
|
114
|
+
## Point the user to their dashboard
|
|
115
|
+
|
|
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.
|
|
119
|
+
|
|
120
|
+
## Seeding
|
|
121
|
+
|
|
122
|
+
Per `seed/SEED.md` — plain-data `plan.json` into `seed-blog.mjs` from the project root.
|
|
123
|
+
Seed posts that exercise the UI (~3 posts, 2 categories when the brief has sections, varied
|
|
124
|
+
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.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// REFERENCE feed surface: taxonomy filter chips + post grid + load-more on the @theme tokens.
|
|
2
|
+
// Correct and complete; per the skill's model you design and build your own on useBlogFeed.
|
|
3
|
+
import type { ComponentType, ReactNode } from "react";
|
|
4
|
+
import { useBlogFeed } from "../../hooks/blog/useBlogFeed";
|
|
5
|
+
import type { BlogCategory, BlogTag, PostPage, PostSummary } from "../../wix/blog/types";
|
|
6
|
+
|
|
7
|
+
export interface LinkLikeProps {
|
|
8
|
+
href: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PlainLink = ({ href, className, children }: LinkLikeProps) => (
|
|
14
|
+
<a href={href} className={className}>
|
|
15
|
+
{children}
|
|
16
|
+
</a>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export interface PostCardProps {
|
|
20
|
+
post: PostSummary;
|
|
21
|
+
postHref?: (slug: string) => string;
|
|
22
|
+
LinkComponent?: ComponentType<LinkLikeProps>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PostCard({ post, postHref = (slug) => `/blog/${slug}`, LinkComponent = PlainLink }: PostCardProps) {
|
|
26
|
+
return (
|
|
27
|
+
<LinkComponent href={postHref(post.slug)} className="group block no-underline">
|
|
28
|
+
{post.coverUrl && (
|
|
29
|
+
<div className="aspect-[16/9] overflow-hidden rounded-lg bg-secondary">
|
|
30
|
+
<img
|
|
31
|
+
src={post.coverUrl}
|
|
32
|
+
alt={post.title}
|
|
33
|
+
loading="lazy"
|
|
34
|
+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
<h3 className="mt-3 text-base font-semibold leading-snug text-foreground">{post.title}</h3>
|
|
39
|
+
{post.excerpt && <p className="mt-1 line-clamp-2 text-sm text-muted-foreground">{post.excerpt}</p>}
|
|
40
|
+
<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` : ""}
|
|
44
|
+
</p>
|
|
45
|
+
</LinkComponent>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BlogFeedViewProps {
|
|
50
|
+
initialPage?: PostPage;
|
|
51
|
+
initialCategories?: BlogCategory[];
|
|
52
|
+
initialTags?: BlogTag[];
|
|
53
|
+
emptyMessage?: string;
|
|
54
|
+
postHref?: PostCardProps["postHref"];
|
|
55
|
+
LinkComponent?: ComponentType<LinkLikeProps>;
|
|
56
|
+
CardComponent?: ComponentType<PostCardProps>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pill = (active: boolean) =>
|
|
60
|
+
`rounded-full border px-4 py-1.5 text-sm font-medium transition-colors ${
|
|
61
|
+
active
|
|
62
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
63
|
+
: "border-border text-foreground hover:bg-secondary"
|
|
64
|
+
}`;
|
|
65
|
+
|
|
66
|
+
export default function BlogFeedView({
|
|
67
|
+
initialPage,
|
|
68
|
+
initialCategories,
|
|
69
|
+
initialTags,
|
|
70
|
+
emptyMessage = "No posts yet — check back soon.",
|
|
71
|
+
postHref,
|
|
72
|
+
LinkComponent,
|
|
73
|
+
CardComponent = PostCard,
|
|
74
|
+
}: BlogFeedViewProps) {
|
|
75
|
+
const {
|
|
76
|
+
posts,
|
|
77
|
+
categories,
|
|
78
|
+
activeCategoryId,
|
|
79
|
+
setActiveCategoryId,
|
|
80
|
+
hasMore,
|
|
81
|
+
loadMore,
|
|
82
|
+
loadingMore,
|
|
83
|
+
error,
|
|
84
|
+
} = useBlogFeed({ initialPage, initialCategories, initialTags });
|
|
85
|
+
|
|
86
|
+
const visibleCategories = categories.filter((c) => c.postCount > 0);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
{visibleCategories.length > 1 && (
|
|
91
|
+
<div className="mb-8 flex flex-wrap gap-2" role="group" aria-label="Categories">
|
|
92
|
+
<button type="button" className={pill(activeCategoryId === null)} onClick={() => setActiveCategoryId(null)}>
|
|
93
|
+
All
|
|
94
|
+
</button>
|
|
95
|
+
{visibleCategories.map((c) => (
|
|
96
|
+
<button key={c.id} type="button" className={pill(activeCategoryId === c.id)} onClick={() => setActiveCategoryId(c.id)}>
|
|
97
|
+
{c.label}
|
|
98
|
+
</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
|
103
|
+
{posts === null ? (
|
|
104
|
+
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
|
|
105
|
+
{Array.from({ length: 6 }, (_, i) => (
|
|
106
|
+
<div key={i}>
|
|
107
|
+
<div className="aspect-[16/9] animate-pulse rounded-lg bg-secondary" />
|
|
108
|
+
<div className="mt-3 h-3.5 w-2/3 animate-pulse rounded bg-secondary" />
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
) : posts.length === 0 ? (
|
|
113
|
+
<p className="py-16 text-center text-muted-foreground">{emptyMessage}</p>
|
|
114
|
+
) : (
|
|
115
|
+
<>
|
|
116
|
+
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
117
|
+
{posts.map((p) => (
|
|
118
|
+
<CardComponent key={p.id} post={p} postHref={postHref} LinkComponent={LinkComponent} />
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
{hasMore && (
|
|
122
|
+
<div className="mt-10 text-center">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={loadMore}
|
|
126
|
+
disabled={loadingMore}
|
|
127
|
+
className="rounded-lg border border-border px-6 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary disabled:opacity-50"
|
|
128
|
+
>
|
|
129
|
+
{loadingMore ? "Loading…" : "Load more"}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// REFERENCE post surface (SPA use — Astro renders the header statically and mounts only
|
|
2
|
+
// RichContent as an island). Correct and complete; per the skill's model you design and
|
|
3
|
+
// build your own on usePost. Chips display .label; the body renders ONLY via RichContent.
|
|
4
|
+
import type { ComponentType } from "react";
|
|
5
|
+
import { usePost } from "../../hooks/blog/usePost";
|
|
6
|
+
import type { BlogCategory, BlogTag, PostDetail } from "../../wix/blog/types";
|
|
7
|
+
import RichContent from "./RichContent";
|
|
8
|
+
import type { LinkLikeProps } from "./BlogFeedView";
|
|
9
|
+
|
|
10
|
+
export interface PostViewProps {
|
|
11
|
+
slug: string;
|
|
12
|
+
initialPost?: PostDetail;
|
|
13
|
+
initialCategories?: BlogCategory[];
|
|
14
|
+
initialTags?: BlogTag[];
|
|
15
|
+
/** Where a chip routes; omit to render chips as plain labels. */
|
|
16
|
+
categoryHref?: (slug: string) => string;
|
|
17
|
+
tagHref?: (slug: string) => string;
|
|
18
|
+
LinkComponent?: ComponentType<LinkLikeProps>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PlainLink = ({ href, className, children }: LinkLikeProps) => (
|
|
22
|
+
<a href={href} className={className}>
|
|
23
|
+
{children}
|
|
24
|
+
</a>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const chipClass =
|
|
28
|
+
"rounded-full border border-border px-3 py-1 text-xs font-medium text-muted-foreground no-underline";
|
|
29
|
+
|
|
30
|
+
export default function PostView({
|
|
31
|
+
slug,
|
|
32
|
+
initialPost,
|
|
33
|
+
initialCategories,
|
|
34
|
+
initialTags,
|
|
35
|
+
categoryHref,
|
|
36
|
+
tagHref,
|
|
37
|
+
LinkComponent = PlainLink,
|
|
38
|
+
}: PostViewProps) {
|
|
39
|
+
const { post, notFound, categories, tags, error } = usePost({
|
|
40
|
+
slug,
|
|
41
|
+
initialPost,
|
|
42
|
+
initialCategories,
|
|
43
|
+
initialTags,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (notFound) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="py-24 text-center">
|
|
49
|
+
<p className="text-lg font-medium">Post not found</p>
|
|
50
|
+
<p className="mt-2 text-sm text-muted-foreground">It may have been unpublished or moved.</p>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (error) return <p className="py-8 text-sm text-red-600">{error}</p>;
|
|
55
|
+
if (post === null) {
|
|
56
|
+
return (
|
|
57
|
+
<div aria-busy="true">
|
|
58
|
+
<div className="h-8 w-2/3 animate-pulse rounded bg-secondary" />
|
|
59
|
+
<div className="mt-6 aspect-[16/9] animate-pulse rounded-lg bg-secondary" />
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<article className="mx-auto max-w-3xl">
|
|
66
|
+
{categories.length > 0 && (
|
|
67
|
+
<p className="eyebrow">
|
|
68
|
+
{categories.map((c, i) => (
|
|
69
|
+
<span key={c.id}>
|
|
70
|
+
{i > 0 && " · "}
|
|
71
|
+
{categoryHref ? (
|
|
72
|
+
<LinkComponent href={categoryHref(c.slug)} className="no-underline">
|
|
73
|
+
{c.label}
|
|
74
|
+
</LinkComponent>
|
|
75
|
+
) : (
|
|
76
|
+
c.label
|
|
77
|
+
)}
|
|
78
|
+
</span>
|
|
79
|
+
))}
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
<h1 className="mt-2 text-3xl font-semibold tracking-tight">{post.title}</h1>
|
|
83
|
+
<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` : ""}
|
|
87
|
+
</p>
|
|
88
|
+
{post.coverUrl && (
|
|
89
|
+
<div className="mt-6 aspect-[16/9] overflow-hidden rounded-xl bg-secondary">
|
|
90
|
+
<img src={post.coverUrl} alt={post.title} className="h-full w-full object-cover" />
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
<div className="mt-8">
|
|
94
|
+
<RichContent content={post.richContent} fallbackParagraphs={post.paragraphs} />
|
|
95
|
+
</div>
|
|
96
|
+
{tags.length > 0 && (
|
|
97
|
+
<div className="mt-10 flex flex-wrap gap-2">
|
|
98
|
+
{tags.map((t) =>
|
|
99
|
+
tagHref ? (
|
|
100
|
+
<LinkComponent key={t.id} href={tagHref(t.slug)} className={chipClass}>
|
|
101
|
+
{t.label}
|
|
102
|
+
</LinkComponent>
|
|
103
|
+
) : (
|
|
104
|
+
<span key={t.id} className={chipClass}>
|
|
105
|
+
{t.label}
|
|
106
|
+
</span>
|
|
107
|
+
),
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</article>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The post-body renderer — wire AS-IS (machinery, not a reference). A post body is a Ricos
|
|
2
|
+
// document, not HTML and not text: never innerHTML it, never String() its nodes. This is the
|
|
3
|
+
// documented render path (@wix/ricos viewer + quick-start plugins).
|
|
4
|
+
//
|
|
5
|
+
// In Astro this component MUST be a `client:only="react"` island — the ricos viewer breaks
|
|
6
|
+
// under SSR. Dark theme: the ricos CSS hardcodes near-black text; scope an override on
|
|
7
|
+
// `.ricos-content` forcing the foreground token (in Astro use <style is:global> — React
|
|
8
|
+
// islands don't inherit scoped Astro styles).
|
|
9
|
+
import { quickStartViewerPlugins, RicosViewer } from "@wix/ricos";
|
|
10
|
+
import "@wix/ricos/css/all-plugins-viewer.css";
|
|
11
|
+
|
|
12
|
+
// Module-level, once — never rebuild the plugin list per render.
|
|
13
|
+
const plugins = quickStartViewerPlugins();
|
|
14
|
+
|
|
15
|
+
export interface RichContentProps {
|
|
16
|
+
/** PostDetail.richContent — the Ricos document (plain JSON). */
|
|
17
|
+
content: Record<string, unknown> | null;
|
|
18
|
+
/** PostDetail.paragraphs — the honest fallback body when richContent is null. */
|
|
19
|
+
fallbackParagraphs?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function RichContent({ content, fallbackParagraphs = [] }: RichContentProps) {
|
|
23
|
+
if (content) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="ricos-content">
|
|
26
|
+
<RicosViewer content={content as any} plugins={plugins} />
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (fallbackParagraphs.length > 0) {
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-4">
|
|
33
|
+
{fallbackParagraphs.map((p, i) => (
|
|
34
|
+
<p key={i} className="leading-relaxed">
|
|
35
|
+
{p}
|
|
36
|
+
</p>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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";
|
|
9
|
+
|
|
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
|
+
}
|
|
32
|
+
|
|
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);
|
|
47
|
+
|
|
48
|
+
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
|
+
|
|
104
|
+
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,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// One post by slug + its resolved category/tag chips. SSR-friendly: pass the server-fetched
|
|
2
|
+
// post (and taxonomy) as `initial*` and no client fetch happens; a SPA passes only the slug.
|
|
3
|
+
// Chips resolve from the FULL taxonomy lists (blog taxonomies are small, fetched once) —
|
|
4
|
+
// never per-id lookups in a render loop.
|
|
5
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { fetchPostBySlug } from "../../wix/blog/posts";
|
|
7
|
+
import { fetchBlogCategories, fetchBlogTags } from "../../wix/blog/taxonomy";
|
|
8
|
+
import type { BlogCategory, BlogTag, PostDetail } from "../../wix/blog/types";
|
|
9
|
+
|
|
10
|
+
export interface UsePostOptions {
|
|
11
|
+
slug: string;
|
|
12
|
+
initialPost?: PostDetail;
|
|
13
|
+
initialCategories?: BlogCategory[];
|
|
14
|
+
initialTags?: BlogTag[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UsePost {
|
|
18
|
+
/** null while loading (render a skeleton) AND when not found — disambiguate via notFound. */
|
|
19
|
+
post: PostDetail | null;
|
|
20
|
+
/** True once the slug definitively resolved to nothing — render a not-found state. */
|
|
21
|
+
notFound: boolean;
|
|
22
|
+
/** This post's categories/tags, resolved to full objects (display .label, route by .slug). */
|
|
23
|
+
categories: BlogCategory[];
|
|
24
|
+
tags: BlogTag[];
|
|
25
|
+
error: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function usePost({ slug, initialPost, initialCategories, initialTags }: UsePostOptions): UsePost {
|
|
29
|
+
const [post, setPost] = useState<PostDetail | null>(initialPost ?? null);
|
|
30
|
+
const [notFound, setNotFound] = useState(false);
|
|
31
|
+
const [allCategories, setAllCategories] = useState<BlogCategory[]>(initialCategories ?? []);
|
|
32
|
+
const [allTags, setAllTags] = useState<BlogTag[]>(initialTags ?? []);
|
|
33
|
+
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let alive = true;
|
|
37
|
+
if (!initialPost || initialPost.slug !== slug) {
|
|
38
|
+
setPost(null);
|
|
39
|
+
setNotFound(false);
|
|
40
|
+
fetchPostBySlug(slug)
|
|
41
|
+
.then((p) => {
|
|
42
|
+
if (!alive) return;
|
|
43
|
+
if (p) setPost(p);
|
|
44
|
+
else setNotFound(true);
|
|
45
|
+
})
|
|
46
|
+
.catch((e) => alive && setError(e instanceof Error ? e.message : String(e)));
|
|
47
|
+
}
|
|
48
|
+
if (!initialCategories) {
|
|
49
|
+
fetchBlogCategories().then((c) => alive && setAllCategories(c));
|
|
50
|
+
}
|
|
51
|
+
if (!initialTags) {
|
|
52
|
+
fetchBlogTags().then((t) => alive && setAllTags(t));
|
|
53
|
+
}
|
|
54
|
+
return () => {
|
|
55
|
+
alive = false;
|
|
56
|
+
};
|
|
57
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
+
}, [slug]);
|
|
59
|
+
|
|
60
|
+
const categories = useMemo(
|
|
61
|
+
() => (post ? allCategories.filter((c) => post.categoryIds.includes(c.id)) : []),
|
|
62
|
+
[post, allCategories],
|
|
63
|
+
);
|
|
64
|
+
const tags = useMemo(
|
|
65
|
+
() => (post ? allTags.filter((t) => post.tagIds.includes(t.id)) : []),
|
|
66
|
+
[post, allTags],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return { post, notFound, categories, tags, error };
|
|
70
|
+
}
|