create-nextblock 0.13.9 → 0.13.11

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 (260) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  19. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  20. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  21. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  22. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  23. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  24. package/templates/nextblock-template/app/actions/productGridActions.ts +40 -40
  25. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  26. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  27. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  28. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  29. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  30. package/templates/nextblock-template/app/api/cms/ecommerce/product-picker/route.ts +151 -151
  31. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  32. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  33. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5609
  35. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  36. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  37. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  38. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  39. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  40. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  41. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  42. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  43. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  44. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  45. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +10 -1
  46. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +5 -1
  47. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  48. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +55 -24
  49. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  50. package/templates/nextblock-template/app/cms/blocks/components/MultiEntityPicker.tsx +263 -251
  51. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  52. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  53. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  54. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  55. package/templates/nextblock-template/app/cms/components/EcommerceActiveContext.tsx +27 -27
  56. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  57. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  58. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  59. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  60. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  61. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  62. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  63. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  64. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  65. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  66. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  67. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  68. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  69. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  70. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  71. package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -189
  72. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  73. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  74. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  75. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  76. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  77. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/categories/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/new/page.tsx +135 -135
  82. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  83. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  84. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  85. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  86. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  87. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  88. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  89. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  90. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  91. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  92. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
  93. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  94. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  95. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  96. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  97. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  98. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  99. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageDetectionPanel.tsx +188 -188
  100. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  101. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  102. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  103. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
  104. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  105. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  106. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  107. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  108. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  109. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  110. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  111. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  112. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  113. package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -217
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -44
  116. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  117. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  118. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  119. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  120. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  121. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  122. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  123. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  124. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  125. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  126. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  127. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  128. package/templates/nextblock-template/app/sitemap.ts +130 -130
  129. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  130. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  131. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  132. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  133. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  134. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  135. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  136. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  137. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  138. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  139. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  140. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  141. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  142. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  143. package/templates/nextblock-template/components/blocks/ProductGridClient.tsx +114 -114
  144. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  145. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +8 -1
  146. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  147. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +29 -23
  148. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  149. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  150. package/templates/nextblock-template/components/form-message.tsx +32 -32
  151. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +69 -0
  152. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -0
  153. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  154. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  155. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  156. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  157. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -464
  158. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  159. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  160. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  161. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  162. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  163. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  164. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  165. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  166. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  167. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  168. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  169. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  170. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  171. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  172. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  173. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  174. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  175. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  176. package/templates/nextblock-template/docs/README.md +39 -39
  177. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  178. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  179. package/templates/nextblock-template/index.d.ts +7 -7
  180. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  181. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  182. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  183. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  184. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  185. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +78 -78
  186. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  187. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +21 -1
  188. package/templates/nextblock-template/lib/blocks/product-grid-data.ts +210 -210
  189. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  190. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  191. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  192. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  193. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  194. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  195. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  196. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  197. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  198. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  199. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  200. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  201. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  202. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  203. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  204. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  205. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  206. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  207. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  208. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  209. package/templates/nextblock-template/lib/i18n/country-languages.ts +247 -247
  210. package/templates/nextblock-template/lib/i18n/detection.test.ts +197 -197
  211. package/templates/nextblock-template/lib/i18n/detection.ts +192 -192
  212. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  213. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  214. package/templates/nextblock-template/lib/media/youtube.ts +81 -0
  215. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  216. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  217. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  218. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  219. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  220. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  221. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  222. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  223. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  224. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  225. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  226. package/templates/nextblock-template/lib/search/types.ts +27 -27
  227. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  228. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  229. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  230. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -82
  231. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  232. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  233. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  234. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  235. package/templates/nextblock-template/lib/site-url.ts +48 -48
  236. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  237. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  238. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  239. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  240. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  241. package/templates/nextblock-template/next-env.d.ts +1 -1
  242. package/templates/nextblock-template/package.json +1 -1
  243. package/templates/nextblock-template/postcss.config.js +6 -6
  244. package/templates/nextblock-template/scripts/backup.js +115 -115
  245. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  246. package/templates/nextblock-template/scripts/restore.js +385 -385
  247. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  248. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  249. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  250. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  251. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  252. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  253. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  254. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  255. package/templates/nextblock-template/tailwind.config.js +25 -25
  256. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  257. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  258. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  259. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  260. package/tsconfig.base.json +3 -3
@@ -0,0 +1,81 @@
1
+ // Shared YouTube URL normalization.
2
+ //
3
+ // No public render path may emit a www.youtube.com iframe: the player writes
4
+ // cookies via document.cookie on load (TESTCOOKIESENABLED, LAST_RESULT_ENTRY_KEY,
5
+ // ExcludeSameSiteUnspecifiedTreatedAsLax) which Chrome logs as a DevTools "Cookie"
6
+ // issue and Lighthouse fails through the `inspector-issues` audit (Best Practices
7
+ // 26/27 = 96). Switching to youtube-nocookie.com does NOT stop those writes --
8
+ // only not loading the player does. See components/media/YouTubeFacade.tsx.
9
+
10
+ const YOUTUBE_HOST_RE = /^(?:www\.|m\.|music\.)?(?:youtube\.com|youtube-nocookie\.com)$/i;
11
+ const YOUTU_BE_RE = /^(?:www\.)?youtu\.be$/i;
12
+ const VIDEO_ID_RE = /^[A-Za-z0-9_-]{6,20}$/;
13
+
14
+ export interface ParsedYouTubeUrl {
15
+ videoId: string;
16
+ /** Extra player params carried over from the source URL (v/si/autoplay stripped). */
17
+ params: URLSearchParams;
18
+ }
19
+
20
+ /**
21
+ * Accepts every shape an author can paste or that YouTube's Share > Embed panel
22
+ * emits: watch?v= (v anywhere in the query), youtu.be/, /embed/, /shorts/,
23
+ * /live/, /v/, m. and music. subdomains, and youtube-nocookie.com/embed/.
24
+ * Returns null for non-YouTube URLs.
25
+ */
26
+ export function parseYouTubeUrl(rawUrl: string | undefined | null): ParsedYouTubeUrl | null {
27
+ if (!rawUrl) return null;
28
+ let url: URL;
29
+ try {
30
+ url = new URL(rawUrl, 'https://www.youtube.com');
31
+ } catch {
32
+ return null;
33
+ }
34
+
35
+ let videoId = '';
36
+ if (YOUTU_BE_RE.test(url.hostname)) {
37
+ videoId = url.pathname.split('/').filter(Boolean)[0] || '';
38
+ } else if (YOUTUBE_HOST_RE.test(url.hostname)) {
39
+ const segments = url.pathname.split('/').filter(Boolean);
40
+ const head = segments[0] || '';
41
+ if (head === 'watch') {
42
+ videoId = url.searchParams.get('v') || '';
43
+ } else if (head === 'embed' || head === 'shorts' || head === 'live' || head === 'v') {
44
+ videoId = segments[1] || '';
45
+ }
46
+ }
47
+ if (!VIDEO_ID_RE.test(videoId)) return null;
48
+
49
+ const params = new URLSearchParams(url.search);
50
+ params.delete('v');
51
+ params.delete('si'); // share-tracking token, not needed for playback
52
+ // Never auto-start on load: an autoplaying frame boots the player (and its
53
+ // cookie writes) during a Lighthouse run. The facade re-adds autoplay=1 only
54
+ // after a real user click.
55
+ params.delete('autoplay');
56
+ return { videoId, params };
57
+ }
58
+
59
+ export function buildNoCookieEmbedUrl(
60
+ videoId: string,
61
+ params?: URLSearchParams,
62
+ opts?: { autoplay?: boolean }
63
+ ): string {
64
+ const search = new URLSearchParams(params ? params.toString() : '');
65
+ if (opts?.autoplay) search.set('autoplay', '1');
66
+ const qs = search.toString();
67
+ return `https://www.youtube-nocookie.com/embed/${videoId}${qs ? `?${qs}` : ''}`;
68
+ }
69
+
70
+ /** Last-resort string rewrite for raw-HTML paths that cannot be parsed into nodes. */
71
+ export function rewriteYouTubeHostsInHtml(html: string): string {
72
+ if (!html) return html;
73
+ return html.replace(
74
+ /(?:https?:)?\/\/(?:www\.|m\.|music\.)?youtube\.com\/embed\//gi,
75
+ 'https://www.youtube-nocookie.com/embed/'
76
+ );
77
+ }
78
+
79
+ export function youTubePosterUrl(videoId: string, quality: 'maxres' | 'hq' = 'maxres'): string {
80
+ return `https://i.ytimg.com/vi/${videoId}/${quality === 'maxres' ? 'maxresdefault' : 'hqdefault'}.jpg`;
81
+ }
@@ -1,31 +1,31 @@
1
- 'use server';
2
-
3
- import { createClient } from '@nextblock-cms/db/server';
4
- import { revalidatePath } from 'next/cache';
5
-
6
- /** Persist the onboarding dismiss flag in the `onboarding_state` row (read-merge). */
7
- export async function setOnboardingDismissed(dismissed: boolean) {
8
- const supabase = createClient();
9
-
10
- const { data: existing } = await supabase
11
- .from('site_settings')
12
- .select('value')
13
- .eq('key', 'onboarding_state')
14
- .maybeSingle();
15
-
16
- const current =
17
- existing?.value && typeof existing.value === 'object' && !Array.isArray(existing.value)
18
- ? (existing.value as Record<string, unknown>)
19
- : {};
20
-
21
- const { error } = await supabase
22
- .from('site_settings')
23
- .upsert({ key: 'onboarding_state', value: { ...current, dismissed } });
24
-
25
- if (error) {
26
- console.error('Error updating onboarding state:', error.message);
27
- throw new Error('Failed to update onboarding state.');
28
- }
29
-
30
- revalidatePath('/cms/dashboard');
31
- }
1
+ 'use server';
2
+
3
+ import { createClient } from '@nextblock-cms/db/server';
4
+ import { revalidatePath } from 'next/cache';
5
+
6
+ /** Persist the onboarding dismiss flag in the `onboarding_state` row (read-merge). */
7
+ export async function setOnboardingDismissed(dismissed: boolean) {
8
+ const supabase = createClient();
9
+
10
+ const { data: existing } = await supabase
11
+ .from('site_settings')
12
+ .select('value')
13
+ .eq('key', 'onboarding_state')
14
+ .maybeSingle();
15
+
16
+ const current =
17
+ existing?.value && typeof existing.value === 'object' && !Array.isArray(existing.value)
18
+ ? (existing.value as Record<string, unknown>)
19
+ : {};
20
+
21
+ const { error } = await supabase
22
+ .from('site_settings')
23
+ .upsert({ key: 'onboarding_state', value: { ...current, dismissed } });
24
+
25
+ if (error) {
26
+ console.error('Error updating onboarding state:', error.message);
27
+ throw new Error('Failed to update onboarding state.');
28
+ }
29
+
30
+ revalidatePath('/cms/dashboard');
31
+ }
@@ -1,222 +1,222 @@
1
- import 'server-only';
2
- // Derives the dashboard onboarding checklist live from configuration state. Every check
3
- // reads public/service-role data only, so it works for both ADMIN and WRITER dashboards.
4
- import { createClient } from '@nextblock-cms/db/server';
5
- import { getStoreConfigStatus } from '@nextblock-cms/ecommerce/server';
6
- import { getEmailPublicSettings } from '../config/email-settings';
7
- import { getPrivacySettings } from '../privacy/settings';
8
- import { detectChannel } from '../setup/env-status';
9
- import { getSystemConfiguration } from '../setup/system-config';
10
- import { selfActionsSettingsUrl } from '../updates/repo-identity';
11
- import { isGithubConnectAvailable } from '../updates/github-device';
12
-
13
- export type OnboardingStep = {
14
- key: string;
15
- title: string;
16
- description: string;
17
- href: string;
18
- done: boolean;
19
- optional: boolean;
20
- /** When true, render the CTA as an external link (new tab) instead of an in-app route. */
21
- isExternal?: boolean;
22
- /** When true, render the device-flow "Connect GitHub" control instead of a link. */
23
- connectGithub?: boolean;
24
- };
25
-
26
- export type OnboardingStatus = {
27
- steps: OnboardingStep[];
28
- completed: number;
29
- total: number;
30
- dismissed: boolean;
31
- };
32
-
33
- // Seeded defaults (libs/db baseline seed 00000000000003 + migration 00000000000004). The
34
- // branding/copyright steps count as "done" only when the value has been customized away from
35
- // these — a fresh install ships with the seeds, so a plain presence check would mark every
36
- // step complete immediately.
37
- const SEEDED_SITE_TITLE = 'NextBlock™ CMS';
38
- // Every object_key a fresh install may ship as the DEFAULT logo — the original baseline WebP
39
- // and the migration-00000000000004 email-safe PNG (which repoints the seeded default). A logo
40
- // counts as customized only when it is none of these; a new default swap must be added here or
41
- // branding falsely reads as done out of the box. Mirrors BUNDLED_PUBLIC_MEDIA_KEYS in
42
- // lib/media/resolveMediaUrl.ts.
43
- const SEEDED_LOGO_OBJECT_KEYS = new Set<string>([
44
- 'images/nextblock-logo-small.webp',
45
- 'images/nextblock-logo-button-tiny.png',
46
- ]);
47
- const SEEDED_COPYRIGHT: Record<string, string> = {
48
- en: '© {year} Nextblock CMS. All rights reserved.',
49
- fr: '© {year} Nextblock CMS. Tous droits réservés.',
50
- };
51
-
52
- /** True when at least one language's copyright text is set AND differs from the seed. */
53
- function hasCustomCopyright(value: unknown): boolean {
54
- if (!value || typeof value !== 'object') return false;
55
- return Object.entries(value as Record<string, unknown>).some(([lang, v]) => {
56
- if (typeof v !== 'string') return false;
57
- const trimmed = v.trim();
58
- return trimmed.length > 0 && trimmed !== (SEEDED_COPYRIGHT[lang] ?? '');
59
- });
60
- }
61
-
62
- /** Pull the active logo's media object_key from the (to-one) embedded relation. */
63
- function extractLogoObjectKey(logoRow: unknown): string | null {
64
- if (!logoRow || typeof logoRow !== 'object') return null;
65
- const media = (logoRow as { media?: unknown }).media;
66
- const record = Array.isArray(media) ? media[0] : media;
67
- if (!record || typeof record !== 'object') return null;
68
- const key = (record as { object_key?: unknown }).object_key;
69
- return typeof key === 'string' ? key : null;
70
- }
71
-
72
- export async function getOnboardingStatus(opts: {
73
- isEcommerceActive: boolean;
74
- }): Promise<OnboardingStatus> {
75
- const supabase = createClient();
76
-
77
- const [{ data: settingRows }, { data: logoRow }, emailPublic, privacy] = await Promise.all([
78
- supabase
79
- .from('site_settings')
80
- .select('key, value')
81
- .in('key', ['footer_copyright', 'bot_protection_public', 'onboarding_state', 'site_title']),
82
- supabase
83
- .from('logos')
84
- .select('media:media_id(object_key)')
85
- .order('created_at', { ascending: false })
86
- .limit(1)
87
- .maybeSingle(),
88
- getEmailPublicSettings(),
89
- getPrivacySettings(),
90
- ]);
91
-
92
- const rows = new Map((settingRows ?? []).map((r) => [r.key, r.value]));
93
- const botPublic = (rows.get('bot_protection_public') ?? {}) as Record<string, unknown>;
94
- const onboardingState = (rows.get('onboarding_state') ?? {}) as Record<string, unknown>;
95
-
96
- const siteTitleRaw = rows.get('site_title');
97
- const siteTitle = typeof siteTitleRaw === 'string' ? siteTitleRaw.trim() : '';
98
- const siteTitleCustomized = siteTitle.length > 0 && siteTitle !== SEEDED_SITE_TITLE;
99
- const logoObjectKey = extractLogoObjectKey(logoRow);
100
- const logoCustomized = Boolean(logoObjectKey) && !SEEDED_LOGO_OBJECT_KEYS.has(logoObjectKey ?? '');
101
-
102
- // Branding is "done" once the user renames the site or uploads their own logo — not merely
103
- // because the seeded NextBlock title/logo exist.
104
- const brandingDone = siteTitleCustomized || logoCustomized;
105
- const copyrightDone = hasCustomCopyright(rows.get('footer_copyright'));
106
- const emailDone = Boolean(emailPublic.host) || Boolean(process.env['SMTP_HOST']);
107
- const analyticsDone = Boolean(privacy.gtm_id);
108
- const botProvider = typeof botPublic['provider'] === 'string' ? (botPublic['provider'] as string) : 'none';
109
- const botDone = botProvider !== 'none' && botProvider !== '';
110
-
111
- const steps: OnboardingStep[] = [
112
- {
113
- key: 'admin',
114
- title: 'Create your admin account',
115
- description: 'Your administrator account is set up.',
116
- href: '/cms/users',
117
- done: true,
118
- optional: false,
119
- },
120
- {
121
- key: 'branding',
122
- title: 'Add your branding',
123
- description: 'Upload your logo and set your site identity.',
124
- href: '/cms/settings/logos',
125
- done: brandingDone,
126
- optional: false,
127
- },
128
- {
129
- key: 'copyright',
130
- title: 'Set your copyright / footer',
131
- description: 'Configure the footer copyright shown across your site.',
132
- href: '/cms/settings/copyright',
133
- done: copyrightDone,
134
- optional: false,
135
- },
136
- {
137
- key: 'email',
138
- title: 'Configure email (SMTP)',
139
- description: 'Enable verification emails, password resets, and notifications.',
140
- href: '/cms/settings/email',
141
- done: emailDone,
142
- optional: false,
143
- },
144
- {
145
- key: 'analytics',
146
- title: 'Connect analytics',
147
- description: 'Add a Google Tag Manager ID for consent-gated analytics.',
148
- href: '/cms/settings/google-analytics',
149
- done: analyticsDone,
150
- optional: true,
151
- },
152
- {
153
- key: 'bot',
154
- title: 'Enable bot protection',
155
- description: 'Protect your sign-up and sign-in forms with a CAPTCHA.',
156
- href: '/cms/settings/bot-protection',
157
- done: botDone,
158
- optional: true,
159
- },
160
- ];
161
-
162
- if (opts.isEcommerceActive) {
163
- let paymentsDone = false;
164
- try {
165
- const storeStatus = await getStoreConfigStatus();
166
- paymentsDone = storeStatus.stripe.hasKeys || storeStatus.freemius.hasKeys;
167
- } catch {
168
- paymentsDone = false;
169
- }
170
- // Insert Payments before the optional steps.
171
- steps.splice(4, 0, {
172
- key: 'payments',
173
- title: 'Set up payment providers',
174
- description: 'Add your Stripe and/or Freemius API keys to accept payments.',
175
- href: '/cms/payments',
176
- done: paymentsDone,
177
- optional: false,
178
- });
179
- }
180
-
181
- // Git-backed (Vercel 1-click / fork) installs: remind the operator to enable GitHub
182
- // Actions so the upstream sync workflow can run. "done" flips once the background poll
183
- // (maybeRefreshUpstreamStatus) has seen the workflow run at least once.
184
- if (detectChannel() === 'vercel') {
185
- let actionsActive = false;
186
- try {
187
- const config = await getSystemConfiguration();
188
- const upstream = config.settings?.['upstream_status'] as
189
- | { actions_active?: boolean }
190
- | undefined;
191
- actionsActive = upstream?.actions_active === true;
192
- } catch {
193
- actionsActive = false;
194
- }
195
- const canConnect = !actionsActive && isGithubConnectAvailable();
196
- steps.push({
197
- key: 'github-actions',
198
- title: 'Automatic updates (GitHub Actions)',
199
- description: actionsActive
200
- ? 'The daily upstream-sync workflow is active for your repository.'
201
- : canConnect
202
- ? 'Connect GitHub to install the upstream-sync workflow into your repo — Vercel’s 1-click deploy can’t copy it automatically.'
203
- : 'Vercel deploys have GitHub Actions on by default — this completes once GitHub registers the sync workflow on your default branch. A manually forked repo needs Actions enabled under Settings → Actions.',
204
- href:
205
- selfActionsSettingsUrl() ??
206
- 'https://github.com/nextblock-cms/nextblock/blob/HEAD/docs/13-STAYING-UP-TO-DATE.md',
207
- done: actionsActive,
208
- optional: true,
209
- isExternal: true,
210
- connectGithub: canConnect,
211
- });
212
- }
213
-
214
- const completed = steps.filter((s) => s.done).length;
215
-
216
- return {
217
- steps,
218
- completed,
219
- total: steps.length,
220
- dismissed: onboardingState['dismissed'] === true,
221
- };
222
- }
1
+ import 'server-only';
2
+ // Derives the dashboard onboarding checklist live from configuration state. Every check
3
+ // reads public/service-role data only, so it works for both ADMIN and WRITER dashboards.
4
+ import { createClient } from '@nextblock-cms/db/server';
5
+ import { getStoreConfigStatus } from '@nextblock-cms/ecommerce/server';
6
+ import { getEmailPublicSettings } from '../config/email-settings';
7
+ import { getPrivacySettings } from '../privacy/settings';
8
+ import { detectChannel } from '../setup/env-status';
9
+ import { getSystemConfiguration } from '../setup/system-config';
10
+ import { selfActionsSettingsUrl } from '../updates/repo-identity';
11
+ import { isGithubConnectAvailable } from '../updates/github-device';
12
+
13
+ export type OnboardingStep = {
14
+ key: string;
15
+ title: string;
16
+ description: string;
17
+ href: string;
18
+ done: boolean;
19
+ optional: boolean;
20
+ /** When true, render the CTA as an external link (new tab) instead of an in-app route. */
21
+ isExternal?: boolean;
22
+ /** When true, render the device-flow "Connect GitHub" control instead of a link. */
23
+ connectGithub?: boolean;
24
+ };
25
+
26
+ export type OnboardingStatus = {
27
+ steps: OnboardingStep[];
28
+ completed: number;
29
+ total: number;
30
+ dismissed: boolean;
31
+ };
32
+
33
+ // Seeded defaults (libs/db baseline seed 00000000000003 + migration 00000000000004). The
34
+ // branding/copyright steps count as "done" only when the value has been customized away from
35
+ // these — a fresh install ships with the seeds, so a plain presence check would mark every
36
+ // step complete immediately.
37
+ const SEEDED_SITE_TITLE = 'NextBlock™ CMS';
38
+ // Every object_key a fresh install may ship as the DEFAULT logo — the original baseline WebP
39
+ // and the migration-00000000000004 email-safe PNG (which repoints the seeded default). A logo
40
+ // counts as customized only when it is none of these; a new default swap must be added here or
41
+ // branding falsely reads as done out of the box. Mirrors BUNDLED_PUBLIC_MEDIA_KEYS in
42
+ // lib/media/resolveMediaUrl.ts.
43
+ const SEEDED_LOGO_OBJECT_KEYS = new Set<string>([
44
+ 'images/nextblock-logo-small.webp',
45
+ 'images/nextblock-logo-button-tiny.png',
46
+ ]);
47
+ const SEEDED_COPYRIGHT: Record<string, string> = {
48
+ en: '© {year} Nextblock CMS. All rights reserved.',
49
+ fr: '© {year} Nextblock CMS. Tous droits réservés.',
50
+ };
51
+
52
+ /** True when at least one language's copyright text is set AND differs from the seed. */
53
+ function hasCustomCopyright(value: unknown): boolean {
54
+ if (!value || typeof value !== 'object') return false;
55
+ return Object.entries(value as Record<string, unknown>).some(([lang, v]) => {
56
+ if (typeof v !== 'string') return false;
57
+ const trimmed = v.trim();
58
+ return trimmed.length > 0 && trimmed !== (SEEDED_COPYRIGHT[lang] ?? '');
59
+ });
60
+ }
61
+
62
+ /** Pull the active logo's media object_key from the (to-one) embedded relation. */
63
+ function extractLogoObjectKey(logoRow: unknown): string | null {
64
+ if (!logoRow || typeof logoRow !== 'object') return null;
65
+ const media = (logoRow as { media?: unknown }).media;
66
+ const record = Array.isArray(media) ? media[0] : media;
67
+ if (!record || typeof record !== 'object') return null;
68
+ const key = (record as { object_key?: unknown }).object_key;
69
+ return typeof key === 'string' ? key : null;
70
+ }
71
+
72
+ export async function getOnboardingStatus(opts: {
73
+ isEcommerceActive: boolean;
74
+ }): Promise<OnboardingStatus> {
75
+ const supabase = createClient();
76
+
77
+ const [{ data: settingRows }, { data: logoRow }, emailPublic, privacy] = await Promise.all([
78
+ supabase
79
+ .from('site_settings')
80
+ .select('key, value')
81
+ .in('key', ['footer_copyright', 'bot_protection_public', 'onboarding_state', 'site_title']),
82
+ supabase
83
+ .from('logos')
84
+ .select('media:media_id(object_key)')
85
+ .order('created_at', { ascending: false })
86
+ .limit(1)
87
+ .maybeSingle(),
88
+ getEmailPublicSettings(),
89
+ getPrivacySettings(),
90
+ ]);
91
+
92
+ const rows = new Map((settingRows ?? []).map((r) => [r.key, r.value]));
93
+ const botPublic = (rows.get('bot_protection_public') ?? {}) as Record<string, unknown>;
94
+ const onboardingState = (rows.get('onboarding_state') ?? {}) as Record<string, unknown>;
95
+
96
+ const siteTitleRaw = rows.get('site_title');
97
+ const siteTitle = typeof siteTitleRaw === 'string' ? siteTitleRaw.trim() : '';
98
+ const siteTitleCustomized = siteTitle.length > 0 && siteTitle !== SEEDED_SITE_TITLE;
99
+ const logoObjectKey = extractLogoObjectKey(logoRow);
100
+ const logoCustomized = Boolean(logoObjectKey) && !SEEDED_LOGO_OBJECT_KEYS.has(logoObjectKey ?? '');
101
+
102
+ // Branding is "done" once the user renames the site or uploads their own logo — not merely
103
+ // because the seeded NextBlock title/logo exist.
104
+ const brandingDone = siteTitleCustomized || logoCustomized;
105
+ const copyrightDone = hasCustomCopyright(rows.get('footer_copyright'));
106
+ const emailDone = Boolean(emailPublic.host) || Boolean(process.env['SMTP_HOST']);
107
+ const analyticsDone = Boolean(privacy.gtm_id);
108
+ const botProvider = typeof botPublic['provider'] === 'string' ? (botPublic['provider'] as string) : 'none';
109
+ const botDone = botProvider !== 'none' && botProvider !== '';
110
+
111
+ const steps: OnboardingStep[] = [
112
+ {
113
+ key: 'admin',
114
+ title: 'Create your admin account',
115
+ description: 'Your administrator account is set up.',
116
+ href: '/cms/users',
117
+ done: true,
118
+ optional: false,
119
+ },
120
+ {
121
+ key: 'branding',
122
+ title: 'Add your branding',
123
+ description: 'Upload your logo and set your site identity.',
124
+ href: '/cms/settings/logos',
125
+ done: brandingDone,
126
+ optional: false,
127
+ },
128
+ {
129
+ key: 'copyright',
130
+ title: 'Set your copyright / footer',
131
+ description: 'Configure the footer copyright shown across your site.',
132
+ href: '/cms/settings/copyright',
133
+ done: copyrightDone,
134
+ optional: false,
135
+ },
136
+ {
137
+ key: 'email',
138
+ title: 'Configure email (SMTP)',
139
+ description: 'Enable verification emails, password resets, and notifications.',
140
+ href: '/cms/settings/email',
141
+ done: emailDone,
142
+ optional: false,
143
+ },
144
+ {
145
+ key: 'analytics',
146
+ title: 'Connect analytics',
147
+ description: 'Add a Google Tag Manager ID for consent-gated analytics.',
148
+ href: '/cms/settings/google-analytics',
149
+ done: analyticsDone,
150
+ optional: true,
151
+ },
152
+ {
153
+ key: 'bot',
154
+ title: 'Enable bot protection',
155
+ description: 'Protect your sign-up and sign-in forms with a CAPTCHA.',
156
+ href: '/cms/settings/bot-protection',
157
+ done: botDone,
158
+ optional: true,
159
+ },
160
+ ];
161
+
162
+ if (opts.isEcommerceActive) {
163
+ let paymentsDone = false;
164
+ try {
165
+ const storeStatus = await getStoreConfigStatus();
166
+ paymentsDone = storeStatus.stripe.hasKeys || storeStatus.freemius.hasKeys;
167
+ } catch {
168
+ paymentsDone = false;
169
+ }
170
+ // Insert Payments before the optional steps.
171
+ steps.splice(4, 0, {
172
+ key: 'payments',
173
+ title: 'Set up payment providers',
174
+ description: 'Add your Stripe and/or Freemius API keys to accept payments.',
175
+ href: '/cms/payments',
176
+ done: paymentsDone,
177
+ optional: false,
178
+ });
179
+ }
180
+
181
+ // Git-backed (Vercel 1-click / fork) installs: remind the operator to enable GitHub
182
+ // Actions so the upstream sync workflow can run. "done" flips once the background poll
183
+ // (maybeRefreshUpstreamStatus) has seen the workflow run at least once.
184
+ if (detectChannel() === 'vercel') {
185
+ let actionsActive = false;
186
+ try {
187
+ const config = await getSystemConfiguration();
188
+ const upstream = config.settings?.['upstream_status'] as
189
+ | { actions_active?: boolean }
190
+ | undefined;
191
+ actionsActive = upstream?.actions_active === true;
192
+ } catch {
193
+ actionsActive = false;
194
+ }
195
+ const canConnect = !actionsActive && isGithubConnectAvailable();
196
+ steps.push({
197
+ key: 'github-actions',
198
+ title: 'Automatic updates (GitHub Actions)',
199
+ description: actionsActive
200
+ ? 'The daily upstream-sync workflow is active for your repository.'
201
+ : canConnect
202
+ ? 'Connect GitHub to install the upstream-sync workflow into your repo — Vercel’s 1-click deploy can’t copy it automatically.'
203
+ : 'Vercel deploys have GitHub Actions on by default — this completes once GitHub registers the sync workflow on your default branch. A manually forked repo needs Actions enabled under Settings → Actions.',
204
+ href:
205
+ selfActionsSettingsUrl() ??
206
+ 'https://github.com/nextblock-cms/nextblock/blob/HEAD/docs/13-STAYING-UP-TO-DATE.md',
207
+ done: actionsActive,
208
+ optional: true,
209
+ isExternal: true,
210
+ connectGithub: canConnect,
211
+ });
212
+ }
213
+
214
+ const completed = steps.filter((s) => s.done).length;
215
+
216
+ return {
217
+ steps,
218
+ completed,
219
+ total: steps.length,
220
+ dismissed: onboardingState['dismissed'] === true,
221
+ };
222
+ }