create-nextblock 0.16.1 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -221
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +4 -4
  30. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +4542 -2904
  31. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  32. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  33. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  34. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  35. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  37. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  38. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  39. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  40. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  41. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
  42. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  43. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  44. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  45. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
  46. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  47. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  48. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  49. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  50. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  51. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  52. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
  53. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
  54. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  55. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  56. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  58. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  59. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  60. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  61. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  63. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  64. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  65. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  66. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  67. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  68. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  69. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  70. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  71. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  72. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  73. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
  74. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +624 -618
  77. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
  78. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  79. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  80. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  81. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  82. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  83. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  84. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  85. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  86. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  90. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  91. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  92. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  93. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  96. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  98. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  99. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  100. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  101. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  102. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  103. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  104. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  105. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  106. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  107. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  108. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  109. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  110. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  111. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  112. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  113. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/layout.tsx +671 -671
  116. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  117. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  118. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  119. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  120. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  121. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  122. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  123. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  124. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  125. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  126. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  127. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  128. package/templates/nextblock-template/app/providers.tsx +96 -96
  129. package/templates/nextblock-template/app/robots.ts +123 -123
  130. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  131. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  132. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  133. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  134. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  135. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  136. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  137. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  138. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  139. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  140. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  141. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  142. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  143. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  144. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  145. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  146. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  147. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  148. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  149. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  150. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  151. package/templates/nextblock-template/components/Header.tsx +38 -38
  152. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  153. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  154. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  155. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  156. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  157. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  158. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  159. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  160. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  161. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  162. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  163. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  164. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  165. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  166. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  167. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  168. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  169. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  170. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  171. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  172. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  173. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  174. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  175. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  176. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  177. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  178. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  179. package/templates/nextblock-template/components/form-message.tsx +32 -32
  180. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +251 -244
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  189. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  192. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  193. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  194. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  195. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  196. package/templates/nextblock-template/docs/README.md +42 -42
  197. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  198. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  199. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  200. package/templates/nextblock-template/index.d.ts +7 -7
  201. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  202. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  203. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  204. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  205. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  206. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  207. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  208. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  209. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  210. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  211. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  212. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  213. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  215. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  216. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  217. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  218. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  219. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  220. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  221. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  222. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  223. package/templates/nextblock-template/lib/search/types.ts +27 -27
  224. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +157 -140
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +384 -350
  231. package/templates/nextblock-template/lib/seo/page-document.ts +463 -412
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -22
  239. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  240. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  241. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  242. package/templates/nextblock-template/lib/site-url.ts +102 -102
  243. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  244. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  245. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  246. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  247. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  248. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  249. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  250. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  251. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  252. package/templates/nextblock-template/next-env.d.ts +1 -2
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/scripts/backup.js +115 -115
  256. package/templates/nextblock-template/scripts/restore.js +385 -385
  257. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  258. package/templates/nextblock-template/tailwind.config.js +25 -25
  259. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  260. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  261. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  262. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  263. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  264. package/tsconfig.base.json +3 -3
@@ -1,243 +1,243 @@
1
- 'use client';
2
-
3
- import { useMemo, useState } from 'react';
4
- import { ImageIcon, MessageCircle, Repeat2, Heart, ThumbsUp, Share2 } from 'lucide-react';
5
- import { cn } from '@nextblock-cms/utils';
6
-
7
- /**
8
- * A read-only rehearsal of the share card a page will produce.
9
- *
10
- * The value here is not decoration. Meta title and description are the two fields an
11
- * operator is most likely to get wrong in a way nothing else in the CMS will catch:
12
- * they are invisible on the page itself, and the consequence — a headline chopped
13
- * mid-word in someone's feed — only shows up after the link has already been shared.
14
- * Rendering the truncation locally turns that into a visible, immediate edit.
15
- *
16
- * The three tabs are genuinely different, not one box repainted, because the platforms
17
- * genuinely differ. Facebook shows a wide image above a grey caption bar with the domain
18
- * in small caps and a hard title clamp. X's summary_large_image card has no caption bar
19
- * and stamps the domain UNDER the text. LinkedIn keeps the image and gives the headline
20
- * more room but drops the description entirely — which is exactly the surprise worth
21
- * showing someone who has just spent five minutes wording a description. If all three
22
- * looked the same, the component would be teaching a falsehood.
23
- *
24
- * The numbers below approximate observed rendering rather than a published spec — every
25
- * platform reserves the right to reflow — so they are tuned to be slightly pessimistic.
26
- * A preview that clips a little early nudges toward safer copy; one that clips late gives
27
- * false confidence, which is the failure that actually costs something.
28
- */
29
- interface SocialPreviewProps {
30
- description: string;
31
- imageUrl?: string | null;
32
- siteName?: string | null;
33
- title: string;
34
- url: string;
35
- }
36
-
37
- type SocialPlatform = 'facebook' | 'linkedin' | 'x';
38
-
39
- const PLATFORM_TABS: ReadonlyArray<{ id: SocialPlatform; label: string }> = [
40
- { id: 'facebook', label: 'Facebook' },
41
- { id: 'x', label: 'X' },
42
- { id: 'linkedin', label: 'LinkedIn' },
43
- ];
44
-
45
- /**
46
- * Per-platform clamps, in characters. `description: 0` means the platform does not render
47
- * a description at all — a meaningfully different statement from "renders a short one",
48
- * and drawn differently below.
49
- */
50
- const PLATFORM_LIMITS: Record<SocialPlatform, { description: number; title: number }> = {
51
- facebook: { description: 110, title: 65 },
52
- linkedin: { description: 0, title: 90 },
53
- x: { description: 125, title: 70 },
54
- };
55
-
56
- /**
57
- * Clip to a character budget the way a feed does — at a word boundary, with an ellipsis —
58
- * rather than mid-word, so the preview shows the shape of the real damage.
59
- */
60
- function clampToLength(value: string, maxLength: number): string {
61
- const normalized = value.replace(/\s+/g, ' ').trim();
62
- if (maxLength <= 0 || normalized.length <= maxLength) {
63
- return normalized;
64
- }
65
-
66
- const sliced = normalized.slice(0, maxLength);
67
- const lastSpace = sliced.lastIndexOf(' ');
68
- const kept = lastSpace > maxLength * 0.6 ? sliced.slice(0, lastSpace) : sliced;
69
- return `${kept.trimEnd()}…`;
70
- }
71
-
72
- /**
73
- * The domain each platform stamps on the card. Falls back to the raw string when the URL
74
- * is not parseable — while a slug is being typed, `url` is briefly not a valid URL.
75
- */
76
- function toDisplayDomain(url: string): string {
77
- try {
78
- return new URL(url).hostname.replace(/^www\./, '');
79
- } catch {
80
- return url.replace(/^https?:\/\//i, '').split('/')[0] || url;
81
- }
82
- }
83
-
84
- /**
85
- * Remote share images render through a plain `<img>`, deliberately.
86
- *
87
- * `next/image` would require every host an operator might point a feature image at to be
88
- * pre-declared in `next.config` `images.remotePatterns`, and an undeclared host does not
89
- * degrade — it throws. The CMS already makes this call the same way: the external-URL
90
- * branch of `ImageBlockEditor` drops to a plain `<img>` for precisely this reason. A
91
- * preview thumbnail gains nothing from the optimizer in exchange for that fragility.
92
- */
93
- function PreviewImage({
94
- aspectClassName,
95
- imageUrl,
96
- title,
97
- }: {
98
- aspectClassName: string;
99
- imageUrl?: string | null;
100
- title: string;
101
- }) {
102
- if (!imageUrl) {
103
- return (
104
- <div
105
- className={cn(
106
- 'flex w-full flex-col items-center justify-center gap-1 bg-muted text-muted-foreground',
107
- aspectClassName
108
- )}
109
- >
110
- <ImageIcon aria-hidden="true" className="h-6 w-6" />
111
- <span className="text-[10px] font-medium uppercase tracking-wide">No share image</span>
112
- </div>
113
- );
114
- }
115
-
116
- return (
117
- // eslint-disable-next-line @next/next/no-img-element
118
- <img
119
- alt={`Share preview for ${title}`}
120
- className={cn('w-full bg-muted object-cover', aspectClassName)}
121
- loading="lazy"
122
- src={imageUrl}
123
- />
124
- );
125
- }
126
-
127
- export default function SocialPreview({
128
- description,
129
- imageUrl,
130
- siteName,
131
- title,
132
- url,
133
- }: SocialPreviewProps) {
134
- const [platform, setPlatform] = useState<SocialPlatform>('facebook');
135
-
136
- const domain = useMemo(() => toDisplayDomain(url), [url]);
137
- const limits = PLATFORM_LIMITS[platform];
138
- const clampedTitle = clampToLength(title || 'Untitled', limits.title);
139
- const clampedDescription = clampToLength(description, limits.description);
140
- // LinkedIn labels the card with the publisher name when one is known and quietly falls
141
- // back to the bare domain when it is not; Facebook and X only ever show the domain.
142
- const publisher = (siteName ?? '').trim() || domain;
143
-
144
- return (
145
- <div className="space-y-2">
146
- {/* Hand-rolled tabs: this design system ships no Tabs primitive, and the house
147
- pattern for a small switcher is buttons plus useState. The ARIA roles restore
148
- the semantics a real tab component would have provided for free. */}
149
- <div
150
- aria-label="Social platform preview"
151
- className="inline-flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5"
152
- role="tablist"
153
- >
154
- {PLATFORM_TABS.map((tab) => (
155
- <button
156
- aria-selected={platform === tab.id}
157
- className={cn(
158
- 'rounded px-2.5 py-1 text-[11px] font-medium transition-colors',
159
- platform === tab.id
160
- ? 'bg-background text-foreground shadow-sm'
161
- : 'text-muted-foreground hover:text-foreground'
162
- )}
163
- key={tab.id}
164
- onClick={() => setPlatform(tab.id)}
165
- role="tab"
166
- type="button"
167
- >
168
- {tab.label}
169
- </button>
170
- ))}
171
- </div>
172
-
173
- {platform === 'facebook' && (
174
- <div className="max-w-[500px] overflow-hidden rounded-lg border border-border bg-card">
175
- <PreviewImage aspectClassName="aspect-[1.91/1]" imageUrl={imageUrl} title={title} />
176
- {/* Facebook's grey caption bar: domain in small caps above a bold headline. */}
177
- <div className="space-y-0.5 bg-muted/60 px-3 py-2">
178
- <p className="text-[11px] uppercase tracking-wide text-muted-foreground">{domain}</p>
179
- <p className="text-sm font-semibold leading-snug text-foreground">{clampedTitle}</p>
180
- {clampedDescription && (
181
- <p className="text-xs leading-snug text-muted-foreground">{clampedDescription}</p>
182
- )}
183
- </div>
184
- <div className="flex items-center gap-4 border-t border-border px-3 py-1.5 text-[11px] text-muted-foreground">
185
- <span className="flex items-center gap-1">
186
- <ThumbsUp aria-hidden="true" className="h-3 w-3" /> Like
187
- </span>
188
- <span className="flex items-center gap-1">
189
- <MessageCircle aria-hidden="true" className="h-3 w-3" /> Comment
190
- </span>
191
- <span className="flex items-center gap-1">
192
- <Share2 aria-hidden="true" className="h-3 w-3" /> Share
193
- </span>
194
- </div>
195
- </div>
196
- )}
197
-
198
- {platform === 'x' && (
199
- // X rounds the card hard, drops the caption bar, and puts the domain BELOW the
200
- // text instead of above the headline.
201
- <div className="max-w-[500px] overflow-hidden rounded-2xl border border-border bg-card">
202
- <PreviewImage aspectClassName="aspect-[1.91/1]" imageUrl={imageUrl} title={title} />
203
- <div className="space-y-0.5 px-3 py-2">
204
- <p className="text-sm font-medium leading-snug text-foreground">{clampedTitle}</p>
205
- {clampedDescription && (
206
- <p className="text-xs leading-snug text-muted-foreground">{clampedDescription}</p>
207
- )}
208
- <p className="text-xs text-muted-foreground">{domain}</p>
209
- </div>
210
- <div className="flex items-center gap-5 border-t border-border px-3 py-1.5 text-[11px] text-muted-foreground">
211
- <span className="flex items-center gap-1">
212
- <MessageCircle aria-hidden="true" className="h-3 w-3" /> Reply
213
- </span>
214
- <span className="flex items-center gap-1">
215
- <Repeat2 aria-hidden="true" className="h-3 w-3" /> Repost
216
- </span>
217
- <span className="flex items-center gap-1">
218
- <Heart aria-hidden="true" className="h-3 w-3" /> Like
219
- </span>
220
- </div>
221
- </div>
222
- )}
223
-
224
- {platform === 'linkedin' && (
225
- // LinkedIn is the outlier worth showing: square-ish chrome, a roomier headline,
226
- // and NO description at all — the meta description simply never appears here.
227
- <div className="max-w-[520px] overflow-hidden rounded-sm border border-border bg-card">
228
- <PreviewImage aspectClassName="aspect-[1.91/1]" imageUrl={imageUrl} title={title} />
229
- <div className="space-y-1 bg-muted/40 px-3 py-2.5">
230
- <p className="text-sm font-semibold leading-snug text-foreground">{clampedTitle}</p>
231
- <p className="text-[11px] text-muted-foreground">
232
- {publisher}
233
- {publisher === domain ? '' : ` • ${domain}`}
234
- </p>
235
- <p className="text-[10px] italic text-muted-foreground/70">
236
- LinkedIn does not display the meta description on link previews.
237
- </p>
238
- </div>
239
- </div>
240
- )}
241
- </div>
242
- );
243
- }
1
+ 'use client';
2
+
3
+ import { useMemo, useState } from 'react';
4
+ import { ImageIcon, MessageCircle, Repeat2, Heart, ThumbsUp, Share2 } from 'lucide-react';
5
+ import { cn } from '@nextblock-cms/utils';
6
+
7
+ /**
8
+ * A read-only rehearsal of the share card a page will produce.
9
+ *
10
+ * The value here is not decoration. Meta title and description are the two fields an
11
+ * operator is most likely to get wrong in a way nothing else in the CMS will catch:
12
+ * they are invisible on the page itself, and the consequence — a headline chopped
13
+ * mid-word in someone's feed — only shows up after the link has already been shared.
14
+ * Rendering the truncation locally turns that into a visible, immediate edit.
15
+ *
16
+ * The three tabs are genuinely different, not one box repainted, because the platforms
17
+ * genuinely differ. Facebook shows a wide image above a grey caption bar with the domain
18
+ * in small caps and a hard title clamp. X's summary_large_image card has no caption bar
19
+ * and stamps the domain UNDER the text. LinkedIn keeps the image and gives the headline
20
+ * more room but drops the description entirely — which is exactly the surprise worth
21
+ * showing someone who has just spent five minutes wording a description. If all three
22
+ * looked the same, the component would be teaching a falsehood.
23
+ *
24
+ * The numbers below approximate observed rendering rather than a published spec — every
25
+ * platform reserves the right to reflow — so they are tuned to be slightly pessimistic.
26
+ * A preview that clips a little early nudges toward safer copy; one that clips late gives
27
+ * false confidence, which is the failure that actually costs something.
28
+ */
29
+ interface SocialPreviewProps {
30
+ description: string;
31
+ imageUrl?: string | null;
32
+ siteName?: string | null;
33
+ title: string;
34
+ url: string;
35
+ }
36
+
37
+ type SocialPlatform = 'facebook' | 'linkedin' | 'x';
38
+
39
+ const PLATFORM_TABS: ReadonlyArray<{ id: SocialPlatform; label: string }> = [
40
+ { id: 'facebook', label: 'Facebook' },
41
+ { id: 'x', label: 'X' },
42
+ { id: 'linkedin', label: 'LinkedIn' },
43
+ ];
44
+
45
+ /**
46
+ * Per-platform clamps, in characters. `description: 0` means the platform does not render
47
+ * a description at all — a meaningfully different statement from "renders a short one",
48
+ * and drawn differently below.
49
+ */
50
+ const PLATFORM_LIMITS: Record<SocialPlatform, { description: number; title: number }> = {
51
+ facebook: { description: 110, title: 65 },
52
+ linkedin: { description: 0, title: 90 },
53
+ x: { description: 125, title: 70 },
54
+ };
55
+
56
+ /**
57
+ * Clip to a character budget the way a feed does — at a word boundary, with an ellipsis —
58
+ * rather than mid-word, so the preview shows the shape of the real damage.
59
+ */
60
+ function clampToLength(value: string, maxLength: number): string {
61
+ const normalized = value.replace(/\s+/g, ' ').trim();
62
+ if (maxLength <= 0 || normalized.length <= maxLength) {
63
+ return normalized;
64
+ }
65
+
66
+ const sliced = normalized.slice(0, maxLength);
67
+ const lastSpace = sliced.lastIndexOf(' ');
68
+ const kept = lastSpace > maxLength * 0.6 ? sliced.slice(0, lastSpace) : sliced;
69
+ return `${kept.trimEnd()}…`;
70
+ }
71
+
72
+ /**
73
+ * The domain each platform stamps on the card. Falls back to the raw string when the URL
74
+ * is not parseable — while a slug is being typed, `url` is briefly not a valid URL.
75
+ */
76
+ function toDisplayDomain(url: string): string {
77
+ try {
78
+ return new URL(url).hostname.replace(/^www\./, '');
79
+ } catch {
80
+ return url.replace(/^https?:\/\//i, '').split('/')[0] || url;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Remote share images render through a plain `<img>`, deliberately.
86
+ *
87
+ * `next/image` would require every host an operator might point a feature image at to be
88
+ * pre-declared in `next.config` `images.remotePatterns`, and an undeclared host does not
89
+ * degrade — it throws. The CMS already makes this call the same way: the external-URL
90
+ * branch of `ImageBlockEditor` drops to a plain `<img>` for precisely this reason. A
91
+ * preview thumbnail gains nothing from the optimizer in exchange for that fragility.
92
+ */
93
+ function PreviewImage({
94
+ aspectClassName,
95
+ imageUrl,
96
+ title,
97
+ }: {
98
+ aspectClassName: string;
99
+ imageUrl?: string | null;
100
+ title: string;
101
+ }) {
102
+ if (!imageUrl) {
103
+ return (
104
+ <div
105
+ className={cn(
106
+ 'flex w-full flex-col items-center justify-center gap-1 bg-muted text-muted-foreground',
107
+ aspectClassName
108
+ )}
109
+ >
110
+ <ImageIcon aria-hidden="true" className="h-6 w-6" />
111
+ <span className="text-[10px] font-medium uppercase tracking-wide">No share image</span>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ return (
117
+ // eslint-disable-next-line @next/next/no-img-element
118
+ <img
119
+ alt={`Share preview for ${title}`}
120
+ className={cn('w-full bg-muted object-cover', aspectClassName)}
121
+ loading="lazy"
122
+ src={imageUrl}
123
+ />
124
+ );
125
+ }
126
+
127
+ export default function SocialPreview({
128
+ description,
129
+ imageUrl,
130
+ siteName,
131
+ title,
132
+ url,
133
+ }: SocialPreviewProps) {
134
+ const [platform, setPlatform] = useState<SocialPlatform>('facebook');
135
+
136
+ const domain = useMemo(() => toDisplayDomain(url), [url]);
137
+ const limits = PLATFORM_LIMITS[platform];
138
+ const clampedTitle = clampToLength(title || 'Untitled', limits.title);
139
+ const clampedDescription = clampToLength(description, limits.description);
140
+ // LinkedIn labels the card with the publisher name when one is known and quietly falls
141
+ // back to the bare domain when it is not; Facebook and X only ever show the domain.
142
+ const publisher = (siteName ?? '').trim() || domain;
143
+
144
+ return (
145
+ <div className="space-y-2">
146
+ {/* Hand-rolled tabs: this design system ships no Tabs primitive, and the house
147
+ pattern for a small switcher is buttons plus useState. The ARIA roles restore
148
+ the semantics a real tab component would have provided for free. */}
149
+ <div
150
+ aria-label="Social platform preview"
151
+ className="inline-flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5"
152
+ role="tablist"
153
+ >
154
+ {PLATFORM_TABS.map((tab) => (
155
+ <button
156
+ aria-selected={platform === tab.id}
157
+ className={cn(
158
+ 'rounded px-2.5 py-1 text-[11px] font-medium transition-colors',
159
+ platform === tab.id
160
+ ? 'bg-background text-foreground shadow-sm'
161
+ : 'text-muted-foreground hover:text-foreground'
162
+ )}
163
+ key={tab.id}
164
+ onClick={() => setPlatform(tab.id)}
165
+ role="tab"
166
+ type="button"
167
+ >
168
+ {tab.label}
169
+ </button>
170
+ ))}
171
+ </div>
172
+
173
+ {platform === 'facebook' && (
174
+ <div className="max-w-[500px] overflow-hidden rounded-lg border border-border bg-card">
175
+ <PreviewImage aspectClassName="aspect-[1.91/1]" imageUrl={imageUrl} title={title} />
176
+ {/* Facebook's grey caption bar: domain in small caps above a bold headline. */}
177
+ <div className="space-y-0.5 bg-muted/60 px-3 py-2">
178
+ <p className="text-[11px] uppercase tracking-wide text-muted-foreground">{domain}</p>
179
+ <p className="text-sm font-semibold leading-snug text-foreground">{clampedTitle}</p>
180
+ {clampedDescription && (
181
+ <p className="text-xs leading-snug text-muted-foreground">{clampedDescription}</p>
182
+ )}
183
+ </div>
184
+ <div className="flex items-center gap-4 border-t border-border px-3 py-1.5 text-[11px] text-muted-foreground">
185
+ <span className="flex items-center gap-1">
186
+ <ThumbsUp aria-hidden="true" className="h-3 w-3" /> Like
187
+ </span>
188
+ <span className="flex items-center gap-1">
189
+ <MessageCircle aria-hidden="true" className="h-3 w-3" /> Comment
190
+ </span>
191
+ <span className="flex items-center gap-1">
192
+ <Share2 aria-hidden="true" className="h-3 w-3" /> Share
193
+ </span>
194
+ </div>
195
+ </div>
196
+ )}
197
+
198
+ {platform === 'x' && (
199
+ // X rounds the card hard, drops the caption bar, and puts the domain BELOW the
200
+ // text instead of above the headline.
201
+ <div className="max-w-[500px] overflow-hidden rounded-2xl border border-border bg-card">
202
+ <PreviewImage aspectClassName="aspect-[1.91/1]" imageUrl={imageUrl} title={title} />
203
+ <div className="space-y-0.5 px-3 py-2">
204
+ <p className="text-sm font-medium leading-snug text-foreground">{clampedTitle}</p>
205
+ {clampedDescription && (
206
+ <p className="text-xs leading-snug text-muted-foreground">{clampedDescription}</p>
207
+ )}
208
+ <p className="text-xs text-muted-foreground">{domain}</p>
209
+ </div>
210
+ <div className="flex items-center gap-5 border-t border-border px-3 py-1.5 text-[11px] text-muted-foreground">
211
+ <span className="flex items-center gap-1">
212
+ <MessageCircle aria-hidden="true" className="h-3 w-3" /> Reply
213
+ </span>
214
+ <span className="flex items-center gap-1">
215
+ <Repeat2 aria-hidden="true" className="h-3 w-3" /> Repost
216
+ </span>
217
+ <span className="flex items-center gap-1">
218
+ <Heart aria-hidden="true" className="h-3 w-3" /> Like
219
+ </span>
220
+ </div>
221
+ </div>
222
+ )}
223
+
224
+ {platform === 'linkedin' && (
225
+ // LinkedIn is the outlier worth showing: square-ish chrome, a roomier headline,
226
+ // and NO description at all — the meta description simply never appears here.
227
+ <div className="max-w-[520px] overflow-hidden rounded-sm border border-border bg-card">
228
+ <PreviewImage aspectClassName="aspect-[1.91/1]" imageUrl={imageUrl} title={title} />
229
+ <div className="space-y-1 bg-muted/40 px-3 py-2.5">
230
+ <p className="text-sm font-semibold leading-snug text-foreground">{clampedTitle}</p>
231
+ <p className="text-[11px] text-muted-foreground">
232
+ {publisher}
233
+ {publisher === domain ? '' : ` • ${domain}`}
234
+ </p>
235
+ <p className="text-[10px] italic text-muted-foreground/70">
236
+ LinkedIn does not display the meta description on link previews.
237
+ </p>
238
+ </div>
239
+ </div>
240
+ )}
241
+ </div>
242
+ );
243
+ }