create-nextblock 0.15.10 → 0.16.2

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 (266) 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 -0
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
  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/sandboxResetSql.ts +3092 -2900
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
  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 +180 -3
  46. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
  47. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  48. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
  51. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  52. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  53. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  54. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  55. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  56. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  58. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  59. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  60. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  63. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  64. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  65. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
  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/[id]/edit/EditPageClient.tsx +28 -0
  74. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
  77. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  80. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  81. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  82. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  83. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  86. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  96. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  98. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  99. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  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/new/page.tsx +8 -8
  103. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
  104. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
  105. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
  106. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
  107. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
  108. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
  109. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
  110. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  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/DeleteUserButton.tsx +12 -12
  114. package/templates/nextblock-template/app/layout.tsx +671 -671
  115. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  116. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  117. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  118. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  119. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  120. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  121. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  122. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  123. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  124. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  125. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  126. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  127. package/templates/nextblock-template/app/providers.tsx +96 -96
  128. package/templates/nextblock-template/app/robots.ts +123 -0
  129. package/templates/nextblock-template/app/sitemap.ts +1 -1
  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 -0
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
  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 -0
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
  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 -0
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
  231. package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
  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 +0 -1
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/proxy.ts +240 -20
  256. package/templates/nextblock-template/scripts/backup.js +115 -115
  257. package/templates/nextblock-template/scripts/restore.js +385 -385
  258. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  259. package/templates/nextblock-template/tailwind.config.js +25 -25
  260. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  261. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  262. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  263. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  264. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  265. package/tsconfig.base.json +3 -3
  266. package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Renders the operator's stored robots settings into the two shapes the app needs:
3
+ * the `MetadataRoute.Robots` object Next.js serialises for `app/robots.ts`, and the
4
+ * literal robots.txt text the admin preview shows, because somebody has to be able to
5
+ * read the file they are about to publish.
6
+ *
7
+ * Those two are not siblings, and that is the whole point of this module. The
8
+ * metadata object is the single source of truth; the text is produced from it by
9
+ * {@link renderRobotsMetadata}, a line-for-line port of the serialiser Next actually
10
+ * runs — `resolveRobots` in
11
+ * `next/dist/build/webpack/loaders/metadata/resolve-route-data`, which the route
12
+ * module Next generates for a dynamic `robots.ts` calls as
13
+ * `resolveRouteData(await handler(), 'robots')`. The preview is therefore the file,
14
+ * byte for byte, and not a second opinion about it.
15
+ *
16
+ * It used to be a second opinion, and that was a bug worth recording. `buildRobotsTxt`
17
+ * appended `settings.customRules` verbatim while `buildRobotsMetadata` could only
18
+ * re-express the standard directives it managed to parse, so every other custom line
19
+ * was shown to the operator as live and never reached a crawler. The wrong assumption
20
+ * behind that split was that `MetadataRoute.Robots` has no escape hatch for arbitrary
21
+ * lines. It has one: each rule's `other` record, which Next emits verbatim as
22
+ * `key: value`. Custom lines now genuinely reach the served file, and the two kinds
23
+ * that still cannot be represented — comments, and lines with no `:` at all, neither
24
+ * of which any crawler acts on — are reported by {@link listUnservedCustomRuleLines}
25
+ * so the SEO screen can say so, rather than being displayed as though they were live.
26
+ *
27
+ * Nothing here throws or reads the environment. Callers hand in `isSandbox` and the
28
+ * sitemap URL, which keeps the module pure enough to test and — more usefully —
29
+ * means the admin preview can render "what production would serve" rather than
30
+ * "what this dev machine would serve".
31
+ */
32
+
33
+ import type { MetadataRoute } from 'next';
34
+ import type { RobotsSettings } from '@nextblock-cms/utils/seo';
35
+
36
+ export interface RobotsTxtOptions {
37
+ /**
38
+ * True on a disposable sandbox deployment. This overrides every stored setting,
39
+ * so it is passed in rather than read from `process.env` here — the admin preview
40
+ * must be able to show the production answer, and the sandbox's demo admin login
41
+ * must not be able to change what the sandbox serves.
42
+ */
43
+ isSandbox: boolean;
44
+ /**
45
+ * Absolute URL of the sitemap, or null when there is nothing to advertise. Still
46
+ * subject to `settings.sitemapEnabled`: a null (or blank) URL means "we could not
47
+ * build one", the setting means "the operator does not want one".
48
+ */
49
+ sitemapUrl: string | null;
50
+ }
51
+
52
+ /**
53
+ * One entry of `MetadataRoute.Robots['rules']`, derived from Next's own type rather
54
+ * than restated, so a future change to the metadata contract surfaces here as a
55
+ * compile error instead of as a silently ignored field.
56
+ */
57
+ type RobotsRuleList = Extract<MetadataRoute.Robots['rules'], readonly unknown[]>;
58
+ type RobotsRule = RobotsRuleList[number];
59
+
60
+ /**
61
+ * The sandbox file, and the reasoning that has to travel with it.
62
+ *
63
+ * Sandbox is a copy of production, so it must stay out of the search index. The
64
+ * counter-intuitive part is that we ALLOW crawling in order to achieve that. Every
65
+ * sandbox response already carries `noindex` — the `X-Robots-Tag` header from
66
+ * next.config.js plus the robots meta in app/layout.tsx — and Googlebot can only
67
+ * obey a noindex it is allowed to fetch. `Disallow: /` would block the fetch, so
68
+ * Google would never read the noindex and could leave URL-only entries in the index
69
+ * forever: the exact opposite of what we want. The Sitemap line is omitted because
70
+ * the sandbox sitemap is empty by design (see app/sitemap.ts).
71
+ *
72
+ * This branch ignores the stored settings wholesale, deliberately. A sandbox visitor
73
+ * signs in with a shared demo admin account, and "the demo admin flipped indexing
74
+ * off" must not be able to turn the sandbox into a permanently half-indexed ghost.
75
+ */
76
+ const SANDBOX_USER_AGENT_RULE = { allow: ['/'], disallow: [], userAgent: '*' } as const;
77
+
78
+ /** The file served when an operator switches indexing off for the whole site. */
79
+ const INDEXING_DISABLED_USER_AGENT_RULE = { allow: [], disallow: ['/'], userAgent: '*' } as const;
80
+
81
+ /** What a site with no usable configuration gets: fully crawlable. */
82
+ const PERMISSIVE_USER_AGENT_RULE = { allow: ['/'], disallow: [], userAgent: '*' } as const;
83
+
84
+ interface ResolvedUserAgentRule {
85
+ allow: string[];
86
+ disallow: string[];
87
+ userAgent: string;
88
+ }
89
+
90
+ /**
91
+ * The single decision layer both serialisers run through.
92
+ *
93
+ * Returning the resolved groups rather than a formatted string is what makes the two
94
+ * public functions incapable of disagreeing about who may crawl what: they differ
95
+ * only in how they render this answer, never in what it is.
96
+ */
97
+ function resolveUserAgentRules(
98
+ settings: RobotsSettings,
99
+ options: RobotsTxtOptions,
100
+ ): ResolvedUserAgentRule[] {
101
+ if (options.isSandbox) {
102
+ return [{ ...SANDBOX_USER_AGENT_RULE, allow: [...SANDBOX_USER_AGENT_RULE.allow], disallow: [] }];
103
+ }
104
+
105
+ if (!settings.isIndexingEnabled) {
106
+ return [
107
+ {
108
+ ...INDEXING_DISABLED_USER_AGENT_RULE,
109
+ allow: [],
110
+ disallow: [...INDEXING_DISABLED_USER_AGENT_RULE.disallow],
111
+ },
112
+ ];
113
+ }
114
+
115
+ const rules = settings.userAgentRules.map((rule) => ({
116
+ allow: [...rule.allow],
117
+ disallow: [...rule.disallow],
118
+ userAgent: rule.userAgent,
119
+ }));
120
+
121
+ // A robots.txt with no User-agent group is not a stricter file, it is a
122
+ // meaningless one, and crawlers disagree about how to read one — so a
123
+ // configuration that reduced to nothing falls back to the permissive wildcard
124
+ // rather than to an empty file.
125
+ return rules.length > 0
126
+ ? rules
127
+ : [{ ...PERMISSIVE_USER_AGENT_RULE, allow: [...PERMISSIVE_USER_AGENT_RULE.allow], disallow: [] }];
128
+ }
129
+
130
+ /**
131
+ * The operator's free-form lines, normalised for parsing.
132
+ *
133
+ * CRLF is folded to LF because the textarea these come from submits whatever line
134
+ * endings the operator's browser produced, and a Windows-authored block would
135
+ * otherwise put stray carriage returns into a file that is parsed line by line.
136
+ * Surrounding whitespace is trimmed so a block that is nothing but blank lines
137
+ * reduces to the empty string.
138
+ */
139
+ function normalizeCustomRules(customRules: string): string {
140
+ if (typeof customRules !== 'string') {
141
+ return '';
142
+ }
143
+
144
+ return customRules.replace(/\r\n/g, '\n').trim();
145
+ }
146
+
147
+ /**
148
+ * The sitemap line's URL, or null when there is nothing worth advertising.
149
+ *
150
+ * A whitespace-only URL is treated as absent rather than emitted, because
151
+ * `Sitemap:` with a blank value is a line a crawler will try to fetch and fail on.
152
+ */
153
+ function resolveSitemapUrl(settings: RobotsSettings, options: RobotsTxtOptions): string | null {
154
+ if (options.isSandbox || !settings.isIndexingEnabled || !settings.sitemapEnabled) {
155
+ return null;
156
+ }
157
+
158
+ const trimmed = (options.sitemapUrl ?? '').trim();
159
+ return trimmed === '' ? null : trimmed;
160
+ }
161
+
162
+ /** One custom-rules group while it is still being collected, before it becomes a rule. */
163
+ interface CustomRuleGroup {
164
+ allow: string[];
165
+ crawlDelay?: number;
166
+ disallow: string[];
167
+ /** Directive name (original casing) to the values typed under it, in order. */
168
+ other: Record<string, string[]>;
169
+ userAgents: string[];
170
+ }
171
+
172
+ interface ParsedCustomRules {
173
+ rules: RobotsRule[];
174
+ /**
175
+ * Lines the served file cannot carry, in the order they were typed. Every one of
176
+ * them is a line crawlers would ignore anyway — see {@link parseCustomRules} — but
177
+ * they are surfaced rather than swallowed so the operator is never left looking for
178
+ * a line that quietly disappeared.
179
+ */
180
+ unserved: string[];
181
+ }
182
+
183
+ /**
184
+ * Turns the free-form custom-rules block into `MetadataRoute.Robots` rules.
185
+ *
186
+ * Standard directives become the typed fields Next understands; everything else rides
187
+ * the per-rule `other` record, which Next's serialiser emits verbatim as
188
+ * `key: value`, preserving the key's casing. That covers the non-standard directives
189
+ * operators actually write — `Clean-param`, `Request-rate`, `Host`, an extra
190
+ * `Sitemap` — none of which had any representation before.
191
+ *
192
+ * Two shapes still cannot be represented, and both are reported as unserved rather
193
+ * than pretended away:
194
+ *
195
+ * - comments, because every line Next emits is a `name: value` pair; and
196
+ * - lines with no `:` (or an empty name), which are not robots.txt syntax at all.
197
+ *
198
+ * Neither changes what a crawler does, so the served file loses no meaning — but the
199
+ * preview must not show them, and the operator deserves to be told why they are gone.
200
+ *
201
+ * Note that `other` lines render after their group's Allow/Disallow/Crawl-delay
202
+ * regardless of where the operator typed them. That reordering is harmless (robots.txt
203
+ * groups are unordered sets of directives) and, more to the point, it is visible: the
204
+ * preview is generated from these same rules, so what the operator reads is what
205
+ * crawlers get.
206
+ */
207
+ function parseCustomRules(customRules: string): ParsedCustomRules {
208
+ const normalized = normalizeCustomRules(customRules);
209
+ if (normalized === '') {
210
+ return { rules: [], unserved: [] };
211
+ }
212
+
213
+ const groups: CustomRuleGroup[] = [];
214
+ const unserved: string[] = [];
215
+
216
+ for (const rawLine of normalized.split('\n')) {
217
+ const line = rawLine.trim();
218
+ if (line === '') {
219
+ continue;
220
+ }
221
+
222
+ if (line.startsWith('#')) {
223
+ unserved.push(line);
224
+ continue;
225
+ }
226
+
227
+ const separator = line.indexOf(':');
228
+ if (separator === -1) {
229
+ unserved.push(line);
230
+ continue;
231
+ }
232
+
233
+ const name = line.slice(0, separator).trim();
234
+ const value = line.slice(separator + 1).trim();
235
+ if (name === '') {
236
+ unserved.push(line);
237
+ continue;
238
+ }
239
+
240
+ const directive = name.toLowerCase();
241
+ let current = groups[groups.length - 1];
242
+
243
+ if (directive === 'user-agent') {
244
+ // A blank agent name would open a group that applies to nobody, silently
245
+ // orphaning every directive typed under it, so it is rejected outright.
246
+ if (value === '') {
247
+ unserved.push(line);
248
+ continue;
249
+ }
250
+
251
+ // Consecutive `User-agent:` lines share the rules that follow them, which is
252
+ // standard robots.txt and something operators genuinely write. Only start a
253
+ // new group once the previous one has actually collected a directive.
254
+ if (current && isEmptyGroup(current)) {
255
+ current.userAgents.push(value);
256
+ } else {
257
+ groups.push({ allow: [], disallow: [], other: {}, userAgents: [value] });
258
+ }
259
+ continue;
260
+ }
261
+
262
+ // A directive with no preceding `User-agent:` is malformed robots.txt, but the
263
+ // operator's intent is unambiguous, so it is attributed to the wildcard agent
264
+ // rather than discarded.
265
+ if (!current) {
266
+ current = { allow: [], disallow: [], other: {}, userAgents: ['*'] };
267
+ groups.push(current);
268
+ }
269
+
270
+ if (directive === 'allow') {
271
+ // An empty value is kept rather than skipped: `Disallow:` with nothing after it
272
+ // is the specified way of saying "nothing is disallowed", and dropping it would
273
+ // turn a meaningful line into a missing one.
274
+ current.allow.push(value);
275
+ continue;
276
+ }
277
+
278
+ if (directive === 'disallow') {
279
+ current.disallow.push(value);
280
+ continue;
281
+ }
282
+
283
+ if (directive === 'crawl-delay') {
284
+ const delay = Number.parseFloat(value);
285
+ // Next emits the typed `crawlDelay` field only when it is truthy, so a zero,
286
+ // a second crawl-delay in the same group, or something unparseable would be
287
+ // dropped from the served file. Those go through `other` instead, where they
288
+ // are emitted verbatim — the operator's line survives either way.
289
+ if (current.crawlDelay === undefined && Number.isFinite(delay) && delay > 0) {
290
+ current.crawlDelay = delay;
291
+ } else {
292
+ pushOther(current, name, value);
293
+ }
294
+ continue;
295
+ }
296
+
297
+ pushOther(current, name, value);
298
+ }
299
+
300
+ return { rules: groups.map(toCustomRule), unserved };
301
+ }
302
+
303
+ /** True while a group has collected nothing but `User-agent:` lines. */
304
+ function isEmptyGroup(group: CustomRuleGroup): boolean {
305
+ return (
306
+ group.allow.length === 0 &&
307
+ group.disallow.length === 0 &&
308
+ group.crawlDelay === undefined &&
309
+ Object.keys(group.other).length === 0
310
+ );
311
+ }
312
+
313
+ /**
314
+ * Records a non-standard directive. Repeats accumulate into an array because Next
315
+ * emits one line per array entry, which is the only way a group can carry the same
316
+ * directive name twice.
317
+ */
318
+ function pushOther(group: CustomRuleGroup, name: string, value: string): void {
319
+ const existing = group.other[name];
320
+ if (existing) {
321
+ existing.push(value);
322
+ } else {
323
+ group.other[name] = [value];
324
+ }
325
+ }
326
+
327
+ function toCustomRule(group: CustomRuleGroup): RobotsRule {
328
+ const rule: RobotsRule = {
329
+ userAgent: group.userAgents.length === 1 ? group.userAgents[0] : group.userAgents,
330
+ };
331
+
332
+ if (group.allow.length > 0) {
333
+ rule.allow = group.allow;
334
+ }
335
+ if (group.disallow.length > 0) {
336
+ rule.disallow = group.disallow;
337
+ }
338
+ if (group.crawlDelay !== undefined) {
339
+ rule.crawlDelay = group.crawlDelay;
340
+ }
341
+
342
+ const otherKeys = Object.keys(group.other);
343
+ if (otherKeys.length > 0) {
344
+ const other: Record<string, string | string[]> = {};
345
+ for (const key of otherKeys) {
346
+ const values = group.other[key];
347
+ other[key] = values.length === 1 ? values[0] : values;
348
+ }
349
+ rule.other = other;
350
+ }
351
+
352
+ return rule;
353
+ }
354
+
355
+ /**
356
+ * The `MetadataRoute.Robots` object served at /robots.txt by `app/robots.ts`, and the
357
+ * object {@link buildRobotsTxt} renders for the admin preview.
358
+ *
359
+ * Empty `allow` / `disallow` arrays are omitted rather than passed through: Next
360
+ * renders each entry as its own line, so an empty array contributes nothing while
361
+ * still occupying a field, and leaving it out keeps the emitted object honest about
362
+ * what the operator actually configured.
363
+ */
364
+ export function buildRobotsMetadata(
365
+ settings: RobotsSettings,
366
+ options: RobotsTxtOptions,
367
+ ): MetadataRoute.Robots {
368
+ const rules: RobotsRule[] = resolveUserAgentRules(settings, options).map((rule) => {
369
+ const metadataRule: RobotsRule = { userAgent: rule.userAgent };
370
+ if (rule.allow.length > 0) {
371
+ metadataRule.allow = rule.allow;
372
+ }
373
+ if (rule.disallow.length > 0) {
374
+ metadataRule.disallow = rule.disallow;
375
+ }
376
+ // A group consisting of nothing but its `User-agent:` header is malformed, and
377
+ // some parsers discard the groups that follow it. An empty `Disallow` value is
378
+ // the specified way to say "nothing is disallowed", so that is what an otherwise
379
+ // empty group carries.
380
+ if (rule.allow.length === 0 && rule.disallow.length === 0) {
381
+ metadataRule.disallow = [''];
382
+ }
383
+ return metadataRule;
384
+ });
385
+
386
+ // Custom rules survive an indexing-off switch, because they are how an operator
387
+ // carves out the exceptions that must keep working regardless (a `.well-known`
388
+ // path, a verification file). They do NOT survive the sandbox, for the reason
389
+ // recorded on SANDBOX_USER_AGENT_RULE.
390
+ if (!options.isSandbox) {
391
+ rules.push(...parseCustomRules(settings.customRules).rules);
392
+ }
393
+
394
+ const result: MetadataRoute.Robots = { rules };
395
+
396
+ const sitemapUrl = resolveSitemapUrl(settings, options);
397
+ if (sitemapUrl) {
398
+ result.sitemap = sitemapUrl;
399
+ }
400
+
401
+ return result;
402
+ }
403
+
404
+ /**
405
+ * The literal robots.txt text for a settings object — what the admin preview shows.
406
+ *
407
+ * This is {@link buildRobotsMetadata} put through {@link renderRobotsMetadata}, which
408
+ * is what makes the preview incapable of disagreeing with the served file: there is
409
+ * one object, one renderer, and the renderer is Next's own algorithm. The route
410
+ * itself still returns the metadata object rather than this string, so caching,
411
+ * revalidation and content type are handled exactly the way they are for the sitemap.
412
+ */
413
+ export function buildRobotsTxt(settings: RobotsSettings, options: RobotsTxtOptions): string {
414
+ return renderRobotsMetadata(buildRobotsMetadata(settings, options));
415
+ }
416
+
417
+ /**
418
+ * The custom-rules lines that will not appear in the served file, so the SEO screen
419
+ * can tell the operator instead of leaving them to notice the absence.
420
+ *
421
+ * In the sandbox that is the entire block: the sandbox answer ignores stored settings
422
+ * wholesale (see SANDBOX_USER_AGENT_RULE), so every custom line is inert there.
423
+ */
424
+ export function listUnservedCustomRuleLines(
425
+ settings: RobotsSettings,
426
+ options: RobotsTxtOptions,
427
+ ): string[] {
428
+ const normalized = normalizeCustomRules(settings.customRules);
429
+
430
+ if (options.isSandbox) {
431
+ return normalized === ''
432
+ ? []
433
+ : normalized
434
+ .split('\n')
435
+ .map((line) => line.trim())
436
+ .filter((line) => line !== '');
437
+ }
438
+
439
+ return parseCustomRules(normalized).unserved;
440
+ }
441
+
442
+ /**
443
+ * Serialises a `MetadataRoute.Robots` object exactly as Next.js does.
444
+ *
445
+ * This is a deliberate port of `resolveRobots` in
446
+ * `next/dist/build/webpack/loaders/metadata/resolve-route-data`, down to the
447
+ * capitalised `User-Agent:`, the blank line after every group including the last, and
448
+ * the order of fields within a group. It exists so the admin preview can be the
449
+ * served bytes rather than an approximation of them; the colocated test asserts the
450
+ * two agree by importing Next's serialiser directly, so a change to Next's rendering
451
+ * fails a test here instead of silently turning the preview back into a lie.
452
+ */
453
+ export function renderRobotsMetadata(metadata: MetadataRoute.Robots): string {
454
+ let content = '';
455
+
456
+ const rules = Array.isArray(metadata.rules) ? metadata.rules : [metadata.rules];
457
+
458
+ for (const rule of rules) {
459
+ for (const agent of toArray(rule.userAgent || ['*'])) {
460
+ content += `User-Agent: ${agent}\n`;
461
+ }
462
+
463
+ if (rule.allow) {
464
+ for (const item of toArray(rule.allow)) {
465
+ content += `Allow: ${item}\n`;
466
+ }
467
+ }
468
+
469
+ if (rule.disallow) {
470
+ for (const item of toArray(rule.disallow)) {
471
+ content += `Disallow: ${item}\n`;
472
+ }
473
+ }
474
+
475
+ if (rule.crawlDelay) {
476
+ content += `Crawl-delay: ${rule.crawlDelay}\n`;
477
+ }
478
+
479
+ if (rule.other) {
480
+ for (const key of Object.keys(rule.other)) {
481
+ const value = rule.other[key];
482
+ if (value === null || value === undefined) {
483
+ continue;
484
+ }
485
+ for (const entry of toArray(value)) {
486
+ content += `${key}: ${entry}\n`;
487
+ }
488
+ }
489
+ }
490
+
491
+ content += '\n';
492
+ }
493
+
494
+ if (metadata.host) {
495
+ content += `Host: ${metadata.host}\n`;
496
+ }
497
+
498
+ if (metadata.sitemap) {
499
+ for (const item of toArray(metadata.sitemap)) {
500
+ content += `Sitemap: ${item}\n`;
501
+ }
502
+ }
503
+
504
+ return content;
505
+ }
506
+
507
+ /** Next's `resolveArray`: a bare value becomes a one-element list, a list is left alone. */
508
+ function toArray<T>(value: T | T[]): T[] {
509
+ return Array.isArray(value) ? value : [value];
510
+ }