create-nextblock 0.16.4 → 0.17.0

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 (196) hide show
  1. package/CLAUDE.md +22 -0
  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/package.json +1 -1
  8. package/templates/nextblock-template/.browserslistrc +11 -11
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/CLAUDE.md +26 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  14. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  15. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  16. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  17. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  18. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  19. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  20. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  21. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  22. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  23. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +270 -205
  24. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +8096 -7198
  25. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  26. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  27. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  28. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  29. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  30. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  31. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  32. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
  33. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
  34. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  36. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  37. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  38. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  39. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  40. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
  41. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  42. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
  43. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  44. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  45. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  46. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  47. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  48. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  49. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  50. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  51. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  52. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  53. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  54. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  55. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  57. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  58. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
  59. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
  60. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  62. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  63. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  64. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  65. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  66. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  67. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  68. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  69. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  70. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  71. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  72. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  73. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  74. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  75. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  76. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  77. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  78. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  79. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  80. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  81. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  82. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  83. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  84. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  85. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  86. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  87. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  88. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  89. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  90. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  91. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  92. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  93. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  94. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  95. package/templates/nextblock-template/components/Header.tsx +38 -38
  96. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  97. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  98. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  99. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  100. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  101. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  102. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  103. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
  104. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  105. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  106. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  107. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  108. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  109. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  110. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  111. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  112. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  113. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  114. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  115. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  116. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  117. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  118. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  119. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  120. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  121. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
  122. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  123. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
  124. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  125. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  126. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  127. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  128. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  129. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  130. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  131. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  132. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  133. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  134. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  135. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  136. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  137. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  138. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  139. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  140. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  141. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  142. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  143. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  144. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  145. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  146. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  147. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  148. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  149. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  150. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  151. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  152. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  153. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  154. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  155. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  156. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  157. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  158. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  159. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  160. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  161. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  162. package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
  163. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  164. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  165. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  166. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +42 -12
  167. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  168. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  169. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  170. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  171. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  172. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  173. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  174. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  175. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  176. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  177. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  178. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  179. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  180. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  181. package/templates/nextblock-template/next-env.d.ts +1 -0
  182. package/templates/nextblock-template/package.json +1 -1
  183. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  184. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  185. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  186. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  187. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  188. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  189. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  190. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  191. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  192. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  193. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  194. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  195. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  196. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
@@ -17,17 +17,17 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
17
17
  {
18
18
  "version": "00000000000000",
19
19
  "name": "00000000000000_baseline_schema.sql",
20
- "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 00 · schema: enums, functions, tables, sequences, defaults\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nSET check_function_bodies = false;\r\n\r\nCREATE SCHEMA IF NOT EXISTS public;\r\n\r\nCOMMENT ON SCHEMA public IS 'standard public schema';\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.approval_status AS ENUM (\r\n 'pending',\r\n 'approved',\r\n 'denied'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.interaction_type AS ENUM (\r\n 'review',\r\n 'comment'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.menu_location AS ENUM (\r\n 'HEADER',\r\n 'FOOTER',\r\n 'SIDEBAR'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.page_status AS ENUM (\r\n 'draft',\r\n 'published',\r\n 'archived'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.revision_type AS ENUM (\r\n 'snapshot',\r\n 'diff'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.user_role AS ENUM (\r\n 'ADMIN',\r\n 'WRITER',\r\n 'USER'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nCREATE OR REPLACE FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_track_quantities boolean := public.get_ecommerce_track_quantities();\r\n v_item record;\r\n v_inventory_deducted_at timestamptz;\r\n v_sku text;\r\n v_current_quantity integer;\r\nBEGIN\r\n SELECT inventory_deducted_at\r\n INTO v_inventory_deducted_at\r\n FROM public.orders\r\n WHERE id = p_order_id\r\n FOR UPDATE;\r\n\r\n IF NOT FOUND OR v_inventory_deducted_at IS NOT NULL THEN\r\n RETURN;\r\n END IF;\r\n\r\n IF NOT v_track_quantities THEN\r\n UPDATE public.orders\r\n SET inventory_deducted_at = now()\r\n WHERE id = p_order_id;\r\n\r\n RETURN;\r\n END IF;\r\n\r\n FOR v_item IN\r\n SELECT\r\n product_id,\r\n variant_id,\r\n SUM(quantity)::integer AS quantity\r\n FROM public.order_items\r\n WHERE order_id = p_order_id\r\n GROUP BY product_id, variant_id\r\n LOOP\r\n v_sku := NULL;\r\n v_current_quantity := 0;\r\n\r\n IF v_item.variant_id IS NOT NULL THEN\r\n SELECT\r\n sku,\r\n GREATEST(COALESCE(stock_quantity, 0), 0)\r\n INTO v_sku,\r\n v_current_quantity\r\n FROM public.product_variants\r\n WHERE id = v_item.variant_id\r\n LIMIT 1;\r\n ELSIF v_item.product_id IS NOT NULL THEN\r\n SELECT\r\n sku,\r\n GREATEST(COALESCE(stock, 0), 0)\r\n INTO v_sku,\r\n v_current_quantity\r\n FROM public.products\r\n WHERE id = v_item.product_id\r\n LIMIT 1;\r\n END IF;\r\n\r\n IF NULLIF(trim(v_sku), '') IS NULL THEN\r\n CONTINUE;\r\n END IF;\r\n\r\n INSERT INTO public.inventory_items (sku, quantity)\r\n VALUES (v_sku, v_current_quantity)\r\n ON CONFLICT (sku) DO NOTHING;\r\n\r\n UPDATE public.inventory_items\r\n SET\r\n quantity = GREATEST(COALESCE(quantity, 0) - v_item.quantity, 0),\r\n updated_at = now()\r\n WHERE sku = v_sku;\r\n END LOOP;\r\n\r\n UPDATE public.orders\r\n SET inventory_deducted_at = now()\r\n WHERE id = p_order_id;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone DEFAULT now()) RETURNS TABLE(invoice_number text, paid_at timestamp with time zone)\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_order public.orders%ROWTYPE;\r\n v_effective_paid_at timestamptz;\r\nBEGIN\r\n SELECT *\r\n INTO v_order\r\n FROM public.orders\r\n WHERE id = p_order_id\r\n FOR UPDATE;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Order % not found', p_order_id;\r\n END IF;\r\n\r\n v_effective_paid_at := COALESCE(v_order.paid_at, p_paid_at, now(), v_order.created_at);\r\n\r\n UPDATE public.orders\r\n SET\r\n invoice_number = COALESCE(v_order.invoice_number, public.generate_order_invoice_number()),\r\n paid_at = v_effective_paid_at\r\n WHERE id = p_order_id\r\n RETURNING orders.invoice_number, orders.paid_at\r\n INTO invoice_number, paid_at;\r\n\r\n RETURN NEXT;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.clear_currency_price_overrides(target_currency text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_target_currency text := upper(trim(target_currency));\r\nBEGIN\r\n IF v_target_currency = '' THEN\r\n RETURN;\r\n END IF;\r\n\r\n UPDATE public.products\r\n SET\r\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\r\n sale_prices = CASE\r\n WHEN sale_prices IS NULL THEN NULL\r\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\r\n ELSE sale_prices - v_target_currency\r\n END,\r\n updated_at = now()\r\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\r\n\r\n UPDATE public.product_variants\r\n SET\r\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\r\n sale_prices = CASE\r\n WHEN sale_prices IS NULL THEN NULL\r\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\r\n ELSE sale_prices - v_target_currency\r\n END,\r\n updated_at = now()\r\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN jsonb_typeof(candidate) <> 'array' THEN false\r\n ELSE\r\n NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_array_elements(candidate) AS field(value)\r\n WHERE jsonb_typeof(field.value) <> 'object'\r\n OR jsonb_typeof(field.value -> 'key') IS DISTINCT FROM 'string'\r\n OR jsonb_typeof(field.value -> 'label') IS DISTINCT FROM 'string'\r\n OR jsonb_typeof(field.value -> 'type') IS DISTINCT FROM 'string'\r\n OR field.value ->> 'key' !~ '^[a-z][a-z0-9_]*$'\r\n OR field.value ->> 'type' NOT IN ('text', 'rich-text', 'image_r2', 'db_relation')\r\n )\r\n AND (\r\n SELECT COUNT(*) = COUNT(DISTINCT field.value ->> 'key')\r\n FROM jsonb_array_elements(candidate) AS field(value)\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT CASE\r\n WHEN jsonb_typeof(candidate) <> 'object' THEN false\r\n ELSE candidate ->> 'type' IN ('container', 'field_render')\r\n END;\r\n$$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.custom_block_definitions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n slug text NOT NULL,\r\n name text NOT NULL,\r\n description text DEFAULT ''::text NOT NULL,\r\n fields jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n layout_schema jsonb NOT NULL,\r\n is_original boolean DEFAULT true NOT NULL,\r\n CONSTRAINT custom_block_definitions_fields_check CHECK (public.is_valid_custom_block_fields(fields)),\r\n CONSTRAINT custom_block_definitions_layout_schema_check CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),\r\n CONSTRAINT custom_block_definitions_name_check CHECK ((length(TRIM(BOTH FROM name)) > 0)),\r\n CONSTRAINT custom_block_definitions_slug_check CHECK ((slug ~ '^[a-z][a-z0-9-]*$'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.custom_block_definitions IS 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.fields IS 'Strict JSONB field declarations for data-rendered custom blocks.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.layout_schema IS 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.is_original IS 'False when a definition was created by duplicating an existing registry row.';\r\n\r\nCREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid) RETURNS public.custom_block_definitions\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $_$\r\nDECLARE\r\n source_definition public.custom_block_definitions%ROWTYPE;\r\n copied_definition public.custom_block_definitions%ROWTYPE;\r\n base_slug text;\r\n copy_slug text;\r\n copy_index integer := 1;\r\nBEGIN\r\n IF auth.role() <> 'service_role'\r\n AND COALESCE((SELECT public.get_current_user_role())::text, '') NOT IN ('ADMIN', 'WRITER') THEN\r\n RAISE EXCEPTION 'Not authorized to duplicate custom block definitions.'\r\n USING ERRCODE = '42501';\r\n END IF;\r\n\r\n SELECT *\r\n INTO source_definition\r\n FROM public.custom_block_definitions\r\n WHERE id = target_id;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Custom block definition % not found.', target_id\r\n USING ERRCODE = 'P0002';\r\n END IF;\r\n\r\n base_slug := regexp_replace(source_definition.slug, '-copy(-[0-9]+)?$', '');\r\n copy_slug := base_slug || '-copy';\r\n\r\n WHILE EXISTS (\r\n SELECT 1\r\n FROM public.custom_block_definitions\r\n WHERE slug = copy_slug\r\n ) LOOP\r\n copy_index := copy_index + 1;\r\n copy_slug := base_slug || '-copy-' || copy_index;\r\n END LOOP;\r\n\r\n INSERT INTO public.custom_block_definitions (\r\n id,\r\n slug,\r\n name,\r\n description,\r\n fields,\r\n layout_schema,\r\n is_original\r\n )\r\n VALUES (\r\n gen_random_uuid(),\r\n copy_slug,\r\n source_definition.name || ' Copy',\r\n source_definition.description,\r\n source_definition.fields,\r\n source_definition.layout_schema,\r\n false\r\n )\r\n RETURNING *\r\n INTO copied_definition;\r\n\r\n RETURN copied_definition;\r\nEND;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.format_order_invoice_number(p_value bigint) RETURNS text\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT 'INV-' || lpad(p_value::text, 6, '0');\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.generate_order_invoice_number() RETURNS text\r\n LANGUAGE sql\r\n SET search_path TO ''\r\n AS $$\r\n SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_current_user_role() RETURNS public.user_role\r\n LANGUAGE sql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\n SELECT role FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_default_currency_code() RETURNS text\r\n LANGUAGE sql STABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT COALESCE(\r\n (\r\n SELECT upper(code)\r\n FROM public.currencies\r\n WHERE is_default = true\r\n ORDER BY updated_at DESC, created_at DESC, code ASC\r\n LIMIT 1\r\n ),\r\n 'USD'\r\n );\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities() RETURNS boolean\r\n LANGUAGE plpgsql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_value jsonb;\r\n v_raw text;\r\nBEGIN\r\n SELECT value\r\n INTO v_value\r\n FROM public.site_settings\r\n WHERE key = 'ecommerce_inventory_settings';\r\n\r\n IF v_value IS NULL THEN\r\n RETURN true;\r\n END IF;\r\n\r\n IF jsonb_typeof(v_value) = 'object' THEN\r\n v_raw := NULLIF(v_value->>'track_quantities', '');\r\n ELSE\r\n v_raw := NULLIF(trim(BOTH '\"' FROM v_value::text), '');\r\n END IF;\r\n\r\n IF v_raw IS NULL THEN\r\n RETURN true;\r\n END IF;\r\n\r\n IF lower(v_raw) IN ('false', 'f', '0', 'no', 'off') THEN\r\n RETURN false;\r\n END IF;\r\n\r\n RETURN true;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_my_claim(claim text) RETURNS jsonb\r\n LANGUAGE sql STABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_blocks_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_coupon_freemius_mappings_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\r\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\r\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_coupons_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.code := upper(regexp_replace(btrim(NEW.code), '\\s+', '', 'g'));\r\n NEW.name := btrim(NEW.name);\r\n NEW.provider_scope := lower(btrim(NEW.provider_scope));\r\n NEW.discount_type := lower(btrim(NEW.discount_type));\r\n NEW.freemius_sync_status := lower(btrim(COALESCE(NEW.freemius_sync_status, 'not_synced')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_default_currency_change() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n IF TG_OP = 'INSERT' THEN\r\n IF NEW.is_default THEN\r\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\r\n END IF;\r\n ELSIF NEW.is_default\r\n AND (\r\n OLD.is_default IS DISTINCT FROM NEW.is_default\r\n OR OLD.code IS DISTINCT FROM NEW.code\r\n ) THEN\r\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_inventory_item_change() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_sku text := COALESCE(NEW.sku, OLD.sku);\r\nBEGIN\r\n PERFORM public.sync_inventory_cache_for_sku(v_sku);\r\n RETURN COALESCE(NEW, OLD);\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_inventory_items_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_languages_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_media_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_navigation_items_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n admin_flag_set boolean := false;\r\n user_role public.user_role;\r\n v_full_name text;\r\n v_avatar_url text;\r\n v_github_username text;\r\n v_provider text;\r\nBEGIN\r\n INSERT INTO public.site_settings (key, value)\r\n VALUES ('is_admin_created', 'false'::jsonb)\r\n ON CONFLICT (key) DO NOTHING;\r\n\r\n SELECT COALESCE(value::jsonb::boolean, false)\r\n INTO admin_flag_set\r\n FROM public.site_settings\r\n WHERE key = 'is_admin_created'\r\n FOR UPDATE;\r\n\r\n IF admin_flag_set = false THEN\r\n user_role := 'ADMIN'::public.user_role;\r\n\r\n UPDATE public.site_settings\r\n SET value = 'true'::jsonb\r\n WHERE key = 'is_admin_created';\r\n ELSE\r\n user_role := 'USER'::public.user_role;\r\n END IF;\r\n\r\n v_full_name := NEW.raw_user_meta_data->>'full_name';\r\n v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';\r\n v_provider := NEW.raw_app_meta_data->>'provider';\r\n\r\n IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN\r\n v_github_username := COALESCE(\r\n NEW.raw_user_meta_data->>'user_name',\r\n NEW.raw_user_meta_data->>'preferred_username'\r\n );\r\n ELSE\r\n v_github_username := NULL;\r\n END IF;\r\n\r\n INSERT INTO public.profiles (\r\n id,\r\n role,\r\n full_name,\r\n avatar_url,\r\n github_username\r\n )\r\n VALUES (\r\n NEW.id,\r\n user_role,\r\n v_full_name,\r\n v_avatar_url,\r\n v_github_username\r\n )\r\n ON CONFLICT (id) DO UPDATE SET\r\n full_name = EXCLUDED.full_name,\r\n avatar_url = EXCLUDED.avatar_url,\r\n github_username = EXCLUDED.github_username;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_pages_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_posts_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_product_freemius_sale_coupons_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\r\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\r\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.country_code = upper(btrim(NEW.country_code));\r\n NEW.state_code = CASE\r\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.state_code))\r\n END;\r\n NEW.postal_code = CASE\r\n WHEN NEW.postal_code IS NULL OR btrim(NEW.postal_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.postal_code))\r\n END;\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_tax_rates_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.country_code = upper(btrim(NEW.country_code));\r\n NEW.state_code = CASE\r\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.state_code))\r\n END;\r\n NEW.tax_name = btrim(NEW.tax_name);\r\n NEW.updated_at = now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at = now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_ucp_cart_sessions_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_admin() RETURNS boolean\r\n LANGUAGE sql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\n SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_currency_amount_map(amounts jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN amounts IS NULL THEN false\r\n WHEN jsonb_typeof(amounts) <> 'object' THEN false\r\n WHEN amounts = '{}'::jsonb THEN false\r\n ELSE NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_each(amounts) AS entry\r\n WHERE entry.key !~ '^[A-Z]{3}$'\r\n OR jsonb_typeof(entry.value) <> 'number'\r\n OR entry.value::text !~ '^[0-9]+$'\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN sale_prices IS NULL THEN true\r\n WHEN jsonb_typeof(sale_prices) <> 'object' THEN false\r\n WHEN sale_prices = '{}'::jsonb THEN true\r\n WHEN prices IS NULL OR jsonb_typeof(prices) <> 'object' THEN false\r\n ELSE NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_each(sale_prices) AS entry\r\n WHERE entry.key !~ '^[A-Z]{3}$'\r\n OR NOT (prices ? entry.key)\r\n OR jsonb_typeof(entry.value) <> 'number'\r\n OR entry.value::text !~ '^[0-9]+$'\r\n OR entry.value::text::numeric > (prices ->> entry.key)::numeric\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb) RETURNS jsonb\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN amounts IS NULL THEN '{}'::jsonb\r\n WHEN jsonb_typeof(amounts) <> 'object' THEN amounts\r\n ELSE COALESCE(\r\n (\r\n SELECT jsonb_object_agg(\r\n upper(trim(entry.key)),\r\n CASE\r\n WHEN jsonb_typeof(entry.value) = 'number'\r\n AND entry.value::text ~ '^[0-9]+$' THEN\r\n to_jsonb((entry.value::text)::bigint)\r\n ELSE\r\n entry.value\r\n END\r\n )\r\n FROM jsonb_each(amounts) AS entry\r\n ),\r\n '{}'::jsonb\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.set_currency_defaults() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.code := upper(trim(NEW.code));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.is_default THEN\r\n NEW.is_active := true;\r\n NEW.exchange_rate := 1;\r\n NEW.auto_update_exchange_rate := false;\r\n NEW.auto_sync_product_prices := false;\r\n NEW.exchange_rate_source := COALESCE(NULLIF(NEW.exchange_rate_source, ''), 'store-default');\r\n NEW.exchange_rate_updated_at := COALESCE(NEW.exchange_rate_updated_at, now());\r\n\r\n UPDATE public.currencies\r\n SET is_default = false,\r\n updated_at = now()\r\n WHERE id IS DISTINCT FROM NEW.id\r\n AND is_default = true;\r\n ELSIF NULLIF(NEW.exchange_rate_source, '') IS NULL THEN\r\n NEW.exchange_rate_source := NULL;\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n _new record;\r\nBEGIN\r\n _new := NEW;\r\n _new.updated_at = now();\r\n RETURN _new;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_currency_price_maps() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_default_currency text := public.get_default_currency_code();\r\n v_price_map_changed boolean := false;\r\n v_legacy_changed boolean := false;\r\nBEGIN\r\n NEW.prices := public.normalize_currency_amount_map(COALESCE(NEW.prices, '{}'::jsonb));\r\n NEW.sale_prices := public.normalize_currency_amount_map(COALESCE(NEW.sale_prices, '{}'::jsonb));\r\n\r\n IF NEW.sale_prices = '{}'::jsonb THEN\r\n NEW.sale_prices := NULL;\r\n END IF;\r\n\r\n IF TG_OP = 'UPDATE' THEN\r\n v_price_map_changed :=\r\n NEW.prices IS DISTINCT FROM OLD.prices\r\n OR NEW.sale_prices IS DISTINCT FROM OLD.sale_prices;\r\n v_legacy_changed :=\r\n NEW.price IS DISTINCT FROM OLD.price\r\n OR NEW.sale_price IS DISTINCT FROM OLD.sale_price;\r\n END IF;\r\n\r\n IF TG_OP = 'INSERT' THEN\r\n IF NEW.prices ? v_default_currency THEN\r\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\r\n ELSE\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(NEW.price, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\r\n NEW.sale_price := (NEW.sale_prices ->> v_default_currency)::integer;\r\n ELSIF NEW.sale_price IS NOT NULL THEN\r\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\r\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\r\n END IF;\r\n\r\n RETURN NEW;\r\n END IF;\r\n\r\n IF v_price_map_changed AND NOT v_legacy_changed THEN\r\n IF NOT (NEW.prices ? v_default_currency) THEN\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(OLD.price, NEW.price, 0), 0)\r\n );\r\n END IF;\r\n\r\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\r\n NEW.sale_price := CASE\r\n WHEN NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\r\n (NEW.sale_prices ->> v_default_currency)::integer\r\n ELSE\r\n NULL\r\n END;\r\n\r\n RETURN NEW;\r\n END IF;\r\n\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(NEW.price, 0), 0)\r\n );\r\n\r\n IF NEW.sale_price IS NULL THEN\r\n IF NEW.sale_prices IS NOT NULL THEN\r\n NEW.sale_prices := NEW.sale_prices - v_default_currency;\r\n\r\n IF NEW.sale_prices = '{}'::jsonb THEN\r\n NEW.sale_prices := NULL;\r\n END IF;\r\n END IF;\r\n ELSE\r\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\r\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_quantity integer := 0;\r\nBEGIN\r\n IF NULLIF(trim(p_sku), '') IS NULL THEN\r\n RETURN;\r\n END IF;\r\n\r\n SELECT quantity\r\n INTO v_quantity\r\n FROM public.inventory_items\r\n WHERE sku = p_sku\r\n LIMIT 1;\r\n\r\n v_quantity := COALESCE(v_quantity, 0);\r\n\r\n UPDATE public.product_variants\r\n SET\r\n stock_quantity = v_quantity,\r\n updated_at = now()\r\n WHERE sku = p_sku;\r\n\r\n UPDATE public.products AS products\r\n SET\r\n stock = v_quantity,\r\n updated_at = now()\r\n WHERE products.sku = p_sku\r\n AND NOT EXISTS (\r\n SELECT 1\r\n FROM public.product_variants\r\n WHERE product_id = products.id\r\n );\r\n\r\n UPDATE public.products AS products\r\n SET\r\n stock = COALESCE((\r\n SELECT SUM(COALESCE(inventory.quantity, 0))\r\n FROM public.product_variants AS variants\r\n LEFT JOIN public.inventory_items AS inventory\r\n ON inventory.sku = variants.sku\r\n WHERE variants.product_id = products.id\r\n ), 0),\r\n updated_at = now()\r\n WHERE EXISTS (\r\n SELECT 1\r\n FROM public.product_variants AS variants\r\n WHERE variants.product_id = products.id\r\n AND variants.sku = p_sku\r\n );\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_target_currency text := upper(trim(target_currency));\r\nBEGIN\r\n UPDATE public.products\r\n SET\r\n prices = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\r\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\r\n END,\r\n sale_prices = CASE\r\n WHEN sale_price IS NULL THEN sale_prices\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\r\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\r\n END,\r\n price = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\r\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n ELSE\r\n price\r\n END,\r\n sale_price = CASE\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\r\n (sale_prices ->> v_target_currency)::integer\r\n ELSE\r\n sale_price\r\n END,\r\n updated_at = now()\r\n WHERE\r\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\r\n OR (\r\n sale_price IS NOT NULL\r\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\r\n )\r\n OR (\r\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n )\r\n OR (\r\n sale_prices IS NOT NULL\r\n AND sale_prices ? v_target_currency\r\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\r\n );\r\n\r\n UPDATE public.product_variants\r\n SET\r\n prices = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\r\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\r\n END,\r\n sale_prices = CASE\r\n WHEN sale_price IS NULL THEN sale_prices\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\r\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\r\n END,\r\n price = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\r\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n ELSE\r\n price\r\n END,\r\n sale_price = CASE\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\r\n (sale_prices ->> v_target_currency)::integer\r\n ELSE\r\n sale_price\r\n END,\r\n updated_at = now()\r\n WHERE\r\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\r\n OR (\r\n sale_price IS NOT NULL\r\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\r\n )\r\n OR (\r\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n )\r\n OR (\r\n sale_prices IS NOT NULL\r\n AND sale_prices ? v_target_currency\r\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\r\n );\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_shipping_method_currency_maps() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_source_currency text;\r\nBEGIN\r\n v_source_currency := upper(trim(COALESCE(NULLIF(NEW.cost_currency, ''), public.get_default_currency_code())));\r\n\r\n NEW.cost_currency := v_source_currency;\r\n NEW.currency_pricing_mode := COALESCE(NULLIF(lower(trim(NEW.currency_pricing_mode)), ''), 'auto');\r\n NEW.cost_amounts := public.normalize_currency_amount_map(COALESCE(NEW.cost_amounts, '{}'::jsonb));\r\n NEW.min_order_amounts := public.normalize_currency_amount_map(COALESCE(NEW.min_order_amounts, '{}'::jsonb));\r\n\r\n IF NEW.currency_pricing_mode NOT IN ('auto', 'manual') THEN\r\n RAISE EXCEPTION 'Unsupported shipping currency pricing mode: %', NEW.currency_pricing_mode;\r\n END IF;\r\n\r\n IF NEW.cost_amounts = '{}'::jsonb THEN\r\n NEW.cost_amounts := jsonb_build_object(v_source_currency, GREATEST(COALESCE(NEW.cost_amount, 0), 0));\r\n ELSIF NOT (NEW.cost_amounts ? v_source_currency) THEN\r\n NEW.cost_amounts := NEW.cost_amounts || jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.cost_amount, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.min_order_amounts = '{}'::jsonb THEN\r\n NEW.min_order_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\r\n );\r\n ELSIF NOT (NEW.min_order_amounts ? v_source_currency) THEN\r\n NEW.min_order_amounts := NEW.min_order_amounts || jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.currency_pricing_mode = 'auto' THEN\r\n NEW.cost_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0)\r\n );\r\n NEW.min_order_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0)\r\n );\r\n END IF;\r\n\r\n NEW.cost_amount := GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0);\r\n NEW.min_order_amount := GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0);\r\n NEW.updated_at := now();\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.update_product_ratings() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_product_id uuid;\r\n v_avg numeric(3,2);\r\n v_count integer;\r\nBEGIN\r\n -- Determine which product_id we need to update\r\n IF TG_OP = 'DELETE' THEN\r\n v_product_id := OLD.product_id;\r\n ELSE\r\n v_product_id := NEW.product_id;\r\n END IF;\r\n\r\n IF v_product_id IS NOT NULL THEN\r\n -- Calculate average and count of approved reviews for this product\r\n SELECT COALESCE(avg(rating), 0.00), count(*)\r\n INTO v_avg, v_count\r\n FROM public.cms_interactions\r\n WHERE product_id = v_product_id\r\n AND type = 'review'\r\n AND status = 'approved';\r\n\r\n -- Update products table\r\n UPDATE public.products\r\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\r\n total_reviews = v_count\r\n WHERE id = v_product_id;\r\n END IF;\r\n\r\n -- Handle old product_id if it changed on UPDATE (e.g. transfer of reviews)\r\n IF TG_OP = 'UPDATE' AND OLD.product_id IS NOT NULL AND OLD.product_id <> NEW.product_id THEN\r\n SELECT COALESCE(avg(rating), 0.00), count(*)\r\n INTO v_avg, v_count\r\n FROM public.cms_interactions\r\n WHERE product_id = OLD.product_id\r\n AND type = 'review'\r\n AND status = 'approved';\r\n\r\n UPDATE public.products\r\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\r\n total_reviews = v_count\r\n WHERE id = OLD.product_id;\r\n END IF;\r\n\r\n RETURN NULL;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb) RETURNS uuid\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_product_id uuid := NULLIF(product_payload->>'id', '')::uuid;\r\n v_translation_group_id uuid := NULLIF(product_payload->>'translation_group_id', '')::uuid;\r\n v_product_type text := CASE\r\n WHEN product_payload->>'product_type' IN ('physical', 'digital') THEN\r\n product_payload->>'product_type'\r\n WHEN NULLIF(product_payload->>'freemius_product_id', '') IS NOT NULL\r\n OR NULLIF(product_payload->>'freemius_plan_id', '') IS NOT NULL THEN\r\n 'digital'\r\n ELSE\r\n 'physical'\r\n END;\r\n v_payment_provider text := CASE\r\n WHEN v_product_type = 'digital' THEN 'freemius'\r\n ELSE 'stripe'\r\n END;\r\n v_variants jsonb := COALESCE(product_payload->'variants', '[]'::jsonb);\r\n v_variant jsonb;\r\n v_variant_id uuid;\r\n v_term_id text;\r\n v_has_variants boolean := jsonb_typeof(v_variants) = 'array' AND jsonb_array_length(v_variants) > 0;\r\n v_total_variant_stock integer := 0;\r\nBEGIN\r\n IF NOT public.is_admin() THEN\r\n RAISE EXCEPTION 'Admin access required';\r\n END IF;\r\n\r\n IF v_has_variants THEN\r\n SELECT COALESCE(SUM(COALESCE((value->>'stock_quantity')::integer, 0)), 0)\r\n INTO v_total_variant_stock\r\n FROM jsonb_array_elements(v_variants);\r\n END IF;\r\n\r\n IF v_product_id IS NULL THEN\r\n INSERT INTO public.products (\r\n title,\r\n slug,\r\n sku,\r\n product_type,\r\n payment_provider,\r\n upc,\r\n stock,\r\n status,\r\n short_description,\r\n description_json,\r\n metadata,\r\n price,\r\n prices,\r\n sale_price,\r\n sale_prices,\r\n sale_start_at,\r\n sale_end_at,\r\n freemius_plan_id,\r\n freemius_product_id,\r\n trial_period_days,\r\n trial_requires_payment_method,\r\n language_id,\r\n translation_group_id\r\n )\r\n VALUES (\r\n product_payload->>'title',\r\n product_payload->>'slug',\r\n product_payload->>'sku',\r\n v_product_type,\r\n v_payment_provider,\r\n NULLIF(product_payload->>'upc', ''),\r\n CASE\r\n WHEN v_has_variants THEN v_total_variant_stock\r\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\r\n END,\r\n COALESCE(product_payload->>'status', 'draft'),\r\n NULLIF(product_payload->>'short_description', ''),\r\n product_payload->'description_json',\r\n COALESCE(product_payload->'metadata', '{}'::jsonb),\r\n COALESCE((product_payload->>'price')::integer, 0),\r\n COALESCE(product_payload->'prices', '{}'::jsonb),\r\n CASE\r\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\r\n (product_payload->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\r\n (product_payload->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\r\n (product_payload->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n NULLIF(product_payload->>'freemius_plan_id', ''),\r\n NULLIF(product_payload->>'freemius_product_id', ''),\r\n COALESCE((product_payload->>'trial_period_days')::integer, 0),\r\n COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\r\n (product_payload->>'language_id')::bigint,\r\n COALESCE(v_translation_group_id, gen_random_uuid())\r\n )\r\n RETURNING id INTO v_product_id;\r\n ELSE\r\n UPDATE public.products\r\n SET\r\n title = product_payload->>'title',\r\n slug = product_payload->>'slug',\r\n sku = product_payload->>'sku',\r\n product_type = v_product_type,\r\n payment_provider = v_payment_provider,\r\n upc = NULLIF(product_payload->>'upc', ''),\r\n stock = CASE\r\n WHEN v_has_variants THEN v_total_variant_stock\r\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\r\n END,\r\n status = COALESCE(product_payload->>'status', status),\r\n short_description = NULLIF(product_payload->>'short_description', ''),\r\n description_json = product_payload->'description_json',\r\n metadata = COALESCE(product_payload->'metadata', '{}'::jsonb),\r\n price = COALESCE((product_payload->>'price')::integer, 0),\r\n prices = COALESCE(product_payload->'prices', '{}'::jsonb),\r\n sale_price = CASE\r\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\r\n (product_payload->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n sale_prices = CASE\r\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n sale_start_at = CASE\r\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\r\n (product_payload->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n sale_end_at = CASE\r\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\r\n (product_payload->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n freemius_plan_id = NULLIF(product_payload->>'freemius_plan_id', ''),\r\n freemius_product_id = NULLIF(product_payload->>'freemius_product_id', ''),\r\n trial_period_days = COALESCE((product_payload->>'trial_period_days')::integer, 0),\r\n trial_requires_payment_method = COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\r\n language_id = COALESCE((product_payload->>'language_id')::bigint, language_id),\r\n translation_group_id = COALESCE(v_translation_group_id, translation_group_id),\r\n updated_at = now()\r\n WHERE id = v_product_id;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Product not found';\r\n END IF;\r\n END IF;\r\n\r\n DELETE FROM public.variant_attribute_mapping\r\n WHERE variant_id IN (\r\n SELECT id\r\n FROM public.product_variants\r\n WHERE product_id = v_product_id\r\n );\r\n\r\n DELETE FROM public.product_variants\r\n WHERE product_id = v_product_id;\r\n\r\n IF v_has_variants THEN\r\n FOR v_variant IN\r\n SELECT value FROM jsonb_array_elements(v_variants)\r\n LOOP\r\n INSERT INTO public.product_variants (\r\n product_id,\r\n sku,\r\n upc,\r\n price,\r\n prices,\r\n sale_price,\r\n sale_prices,\r\n sale_start_at,\r\n sale_end_at,\r\n stock_quantity,\r\n main_media_id\r\n )\r\n VALUES (\r\n v_product_id,\r\n v_variant->>'sku',\r\n NULLIF(v_variant->>'upc', ''),\r\n COALESCE((v_variant->>'price')::integer, 0),\r\n COALESCE(v_variant->'prices', '{}'::jsonb),\r\n CASE\r\n WHEN v_variant ? 'sale_price' AND v_variant->>'sale_price' <> '' THEN\r\n (v_variant->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_prices' THEN COALESCE(v_variant->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_start_at' AND v_variant->>'sale_start_at' <> '' THEN\r\n (v_variant->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_end_at' AND v_variant->>'sale_end_at' <> '' THEN\r\n (v_variant->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n COALESCE((v_variant->>'stock_quantity')::integer, 0),\r\n NULLIF(v_variant->>'main_media_id', '')::uuid\r\n )\r\n RETURNING id INTO v_variant_id;\r\n\r\n FOR v_term_id IN\r\n SELECT jsonb_array_elements_text(COALESCE(v_variant->'attribute_term_ids', '[]'::jsonb))\r\n LOOP\r\n INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)\r\n VALUES (v_variant_id, v_term_id::uuid);\r\n END LOOP;\r\n END LOOP;\r\n END IF;\r\n\r\n RETURN v_product_id;\r\nEND;\r\n$$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.blocks (\r\n id bigint NOT NULL,\r\n page_id bigint,\r\n post_id bigint,\r\n language_id bigint NOT NULL,\r\n block_type text NOT NULL,\r\n content jsonb,\r\n \"order\" integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n product_id uuid,\r\n CONSTRAINT check_exactly_one_parent CHECK ((((page_id IS NOT NULL) AND (post_id IS NULL) AND (product_id IS NULL)) OR ((post_id IS NOT NULL) AND (page_id IS NULL) AND (product_id IS NULL)) OR ((product_id IS NOT NULL) AND (page_id IS NULL) AND (post_id IS NULL))))\r\n);\r\n\r\nCOMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';\r\n\r\nCOMMENT ON COLUMN public.blocks.block_type IS 'Type of the block, e.g., \"text\", \"image\".';\r\n\r\nCOMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';\r\n\r\nCOMMENT ON COLUMN public.blocks.\"order\" IS 'Sort order of the block.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.blocks'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.blocks ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.blocks_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.categories (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n slug text NOT NULL,\r\n description text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n description_translations jsonb DEFAULT '{}'::jsonb NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';\r\n\r\nCOMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {\"fr\": \"Numérique\"}).';\r\n\r\nCOMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.cms_interactions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n type public.interaction_type NOT NULL,\r\n status public.approval_status DEFAULT 'pending'::public.approval_status NOT NULL,\r\n content text NOT NULL,\r\n rating integer,\r\n user_id uuid NOT NULL,\r\n product_id uuid,\r\n post_id bigint,\r\n reactions jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT check_product_or_post CHECK ((((product_id IS NOT NULL) AND (post_id IS NULL)) OR ((post_id IS NOT NULL) AND (product_id IS NULL)))),\r\n CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL))))\r\n);\r\n\r\nCOMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.rating IS 'Star rating 1-5, only populated for product reviews.';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles.id';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';\r\n\r\nCREATE TABLE IF NOT EXISTS public.content_drafts (\r\n id bigint NOT NULL,\r\n parent_type text NOT NULL,\r\n parent_id bigint NOT NULL,\r\n author_id uuid,\r\n base_version integer DEFAULT 1 NOT NULL,\r\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT content_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\r\n CONSTRAINT content_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text)),\r\n CONSTRAINT content_drafts_parent_type_check CHECK ((parent_type = ANY (ARRAY['page'::text, 'post'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.content_drafts IS 'Draft snapshots used by Draft Mode and front-end visual editing before publishing to live page/post rows.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.parent_type IS 'The content table this draft belongs to: page or post.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.parent_id IS 'ID of the page or post being drafted.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.base_version IS 'Published page/post version the draft was created from.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.meta IS 'Draft page/post metadata snapshot.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.blocks IS 'Ordered draft block snapshot, including block ids, block types, content, language ids, and order values.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.content_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.content_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.content_drafts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.cortex_ai_db_mutation_audit (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n actor_user_id uuid,\r\n tool_name text NOT NULL,\r\n action_name text NOT NULL,\r\n target_tables text[] DEFAULT '{}'::text[] NOT NULL,\r\n operation_summary text NOT NULL,\r\n payload_hash text NOT NULL,\r\n payload jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n preview jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n status text NOT NULL,\r\n error_message text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT cortex_ai_db_mutation_audit_status_check CHECK ((status = ANY (ARRAY['success'::text, 'failure'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.cortex_ai_db_mutation_audit IS 'Audit trail for confirmed Cortex AI database mutation attempts.';\r\n\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.payload IS 'Redacted tool input payload for the confirmed mutation attempt.';\r\n\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.preview IS 'Redacted confirmation preview shown before the mutation was confirmed.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_freemius_mappings (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n coupon_id uuid NOT NULL,\r\n product_id uuid,\r\n freemius_product_id text NOT NULL,\r\n freemius_coupon_id text,\r\n freemius_coupon_code text NOT NULL,\r\n sync_status text DEFAULT 'pending'::text NOT NULL,\r\n sync_error text,\r\n remote_payload jsonb,\r\n last_synced_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupon_freemius_mappings_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\r\n CONSTRAINT coupon_freemius_mappings_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\r\n CONSTRAINT coupon_freemius_mappings_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text])))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_products (\r\n coupon_id uuid NOT NULL,\r\n product_id uuid NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.coupon_products IS 'Optional product allow-list for coupons. No rows means all products in the provider scope are eligible.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_redemptions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n coupon_id uuid,\r\n order_id uuid,\r\n coupon_code text NOT NULL,\r\n provider text NOT NULL,\r\n discount_total integer DEFAULT 0 NOT NULL,\r\n user_id uuid,\r\n customer_email text,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n redeemed_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupon_redemptions_code_not_blank CHECK ((char_length(btrim(coupon_code)) > 0)),\r\n CONSTRAINT coupon_redemptions_discount_total_check CHECK ((discount_total >= 0)),\r\n CONSTRAINT coupon_redemptions_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text])))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupons (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n code text NOT NULL,\r\n name text NOT NULL,\r\n internal_note text,\r\n provider_scope text DEFAULT 'all'::text NOT NULL,\r\n discount_type text NOT NULL,\r\n discount_amount integer NOT NULL,\r\n is_active boolean DEFAULT true NOT NULL,\r\n starts_at timestamp with time zone,\r\n ends_at timestamp with time zone,\r\n redemption_limit integer,\r\n redemptions_count integer DEFAULT 0 NOT NULL,\r\n freemius_sync_status text DEFAULT 'not_synced'::text NOT NULL,\r\n freemius_sync_error text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupons_code_not_blank CHECK ((char_length(btrim(code)) > 0)),\r\n CONSTRAINT coupons_date_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at))),\r\n CONSTRAINT coupons_discount_amount_positive CHECK ((discount_amount > 0)),\r\n CONSTRAINT coupons_discount_type_valid CHECK ((discount_type = ANY (ARRAY['percent'::text, 'fixed'::text]))),\r\n CONSTRAINT coupons_freemius_sync_status_valid CHECK ((freemius_sync_status = ANY (ARRAY['not_synced'::text, 'pending'::text, 'synced'::text, 'failed'::text, 'not_required'::text]))),\r\n CONSTRAINT coupons_name_not_blank CHECK ((char_length(btrim(name)) > 0)),\r\n CONSTRAINT coupons_percent_amount_valid CHECK (((discount_type <> 'percent'::text) OR (discount_amount <= 100))),\r\n CONSTRAINT coupons_provider_scope_valid CHECK ((provider_scope = ANY (ARRAY['all'::text, 'stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT coupons_redemption_limit_positive CHECK (((redemption_limit IS NULL) OR (redemption_limit > 0))),\r\n CONSTRAINT coupons_redemptions_count_nonnegative CHECK ((redemptions_count >= 0))\r\n);\r\n\r\nCOMMENT ON TABLE public.coupons IS 'Unified commerce coupons managed by NextBlock CMS and applied through provider-aware checkout.';\r\n\r\nCOMMENT ON COLUMN public.coupons.provider_scope IS 'Provider eligibility: all, stripe, or freemius.';\r\n\r\nCOMMENT ON COLUMN public.coupons.discount_amount IS 'Percent value for percent coupons, or fixed minor-unit amount for fixed coupons.';\r\n\r\nCOMMENT ON COLUMN public.coupons.freemius_sync_status IS 'Aggregate status for syncing this coupon to Freemius product coupon records.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.currencies (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n code text NOT NULL,\r\n symbol text NOT NULL,\r\n exchange_rate numeric(20,10) NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n is_active boolean DEFAULT true NOT NULL,\r\n rounding_mode text DEFAULT 'none'::text NOT NULL,\r\n rounding_increment integer DEFAULT 1 NOT NULL,\r\n rounding_charm_amount integer,\r\n auto_update_exchange_rate boolean DEFAULT true NOT NULL,\r\n exchange_rate_updated_at timestamp with time zone,\r\n exchange_rate_source text,\r\n auto_sync_product_prices boolean DEFAULT false NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT currencies_charm_requires_amount CHECK (((rounding_mode <> 'charm'::text) OR (rounding_charm_amount IS NOT NULL))),\r\n CONSTRAINT currencies_code_check CHECK ((code ~ '^[A-Z]{3}$'::text)),\r\n CONSTRAINT currencies_default_auto_update_disabled CHECK (((NOT is_default) OR (auto_update_exchange_rate = false))),\r\n CONSTRAINT currencies_default_exchange_rate_is_one CHECK (((NOT is_default) OR (exchange_rate = (1)::numeric))),\r\n CONSTRAINT currencies_default_must_be_active CHECK (((NOT is_default) OR is_active)),\r\n CONSTRAINT currencies_default_product_price_sync_disabled CHECK (((NOT is_default) OR (auto_sync_product_prices = false))),\r\n CONSTRAINT currencies_exchange_rate_check CHECK ((exchange_rate > (0)::numeric)),\r\n CONSTRAINT currencies_rounding_charm_nonnegative CHECK (((rounding_charm_amount IS NULL) OR (rounding_charm_amount >= 0))),\r\n CONSTRAINT currencies_rounding_increment_positive CHECK ((rounding_increment > 0)),\r\n CONSTRAINT currencies_rounding_mode_valid CHECK ((rounding_mode = ANY (ARRAY['none'::text, 'nearest'::text, 'up'::text, 'down'::text, 'charm'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.currencies IS 'Store currencies available for storefront display and conversion.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate IS 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_mode IS 'Rounding strategy applied when prices are auto-converted into this currency.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_increment IS 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_charm_amount IS 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';\r\n\r\nCOMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS 'Whether scheduled FX sync jobs should refresh this currency.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS 'When this currency exchange rate was last refreshed or manually set.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_source IS 'Human-readable source for the current exchange rate, such as a provider host or manual override.';\r\n\r\nCOMMENT ON COLUMN public.currencies.auto_sync_product_prices IS 'Whether storefront product and variant prices in this currency are derived automatically from the store default currency using FX and rounding rules.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.email_2fa_challenges (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n token_hash text NOT NULL,\r\n expires_at timestamp with time zone NOT NULL,\r\n consumed_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.email_2fa_challenges IS 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.freemius_plans (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n name text NOT NULL,\r\n title text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.freemius_pricing (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n plan_id uuid NOT NULL,\r\n api_monthly_price numeric,\r\n api_annual_price numeric,\r\n api_lifetime_price numeric,\r\n override_monthly_price numeric,\r\n override_annual_price numeric,\r\n override_lifetime_price numeric,\r\n license_quota integer,\r\n is_active boolean DEFAULT true NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.inventory_items (\r\n sku text NOT NULL,\r\n quantity integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT inventory_items_quantity_check CHECK ((quantity >= 0))\r\n);\r\n\r\nCOMMENT ON TABLE public.inventory_items IS 'Source-of-truth inventory records keyed by sellable SKU.';\r\n\r\nCOMMENT ON COLUMN public.inventory_items.sku IS 'Global sellable SKU. Matching products or variants share inventory.';\r\n\r\nCOMMENT ON COLUMN public.inventory_items.quantity IS 'Available quantity for this SKU.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.languages (\r\n id bigint NOT NULL,\r\n code text NOT NULL,\r\n name text NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n is_active boolean DEFAULT true,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';\r\n\r\nCOMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.languages'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.languages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.languages_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.logos (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n media_id uuid,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.logos IS 'Stores company and brand logos.';\r\n\r\nCOMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';\r\n\r\nCOMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.media (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n uploader_id uuid,\r\n file_name text NOT NULL,\r\n object_key text NOT NULL,\r\n file_type text,\r\n size_bytes bigint,\r\n description text,\r\n width integer,\r\n height integer,\r\n blur_data_url text,\r\n variants jsonb,\r\n file_path text,\r\n folder text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';\r\n\r\nCOMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';\r\n\r\nCOMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';\r\n\r\nCOMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';\r\n\r\nCOMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';\r\n\r\nCOMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';\r\n\r\nCOMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';\r\n\r\nCOMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.navigation_items (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n menu_key public.menu_location NOT NULL,\r\n label text NOT NULL,\r\n url text NOT NULL,\r\n parent_id bigint,\r\n \"order\" integer DEFAULT 0 NOT NULL,\r\n page_id bigint,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';\r\n\r\nCOMMENT ON COLUMN public.navigation_items.menu_key IS 'Identifies the menu this item belongs to.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.navigation_items'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.navigation_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.navigation_items_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE SEQUENCE IF NOT EXISTS public.order_invoice_number_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1;\r\n\r\nCREATE TABLE IF NOT EXISTS public.order_items (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n order_id uuid NOT NULL,\r\n product_id uuid,\r\n variant_id uuid,\r\n quantity integer NOT NULL,\r\n price_at_purchase integer NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.orders (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid,\r\n status text DEFAULT 'pending'::text NOT NULL,\r\n total integer NOT NULL,\r\n stripe_session_id text,\r\n payment_intent_id text,\r\n customer_details jsonb,\r\n provider text DEFAULT 'stripe'::text,\r\n freemius_product_id text,\r\n freemius_plan_id text,\r\n freemius_license_id text,\r\n freemius_subscription_id text,\r\n freemius_trial_id text,\r\n freemius_user_id text,\r\n freemius_trial_ends_at timestamp with time zone,\r\n freemius_last_event_type text,\r\n freemius_last_synced_at timestamp with time zone,\r\n currency text DEFAULT 'USD'::text NOT NULL,\r\n subtotal integer,\r\n shipping_total integer,\r\n tax_total integer DEFAULT 0 NOT NULL,\r\n tax_details jsonb,\r\n exchange_rate_at_purchase numeric(20,10) DEFAULT 1 NOT NULL,\r\n inventory_deducted_at timestamp with time zone,\r\n invoice_number text,\r\n paid_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now(),\r\n coupon_id uuid,\r\n coupon_code text,\r\n discount_total integer DEFAULT 0 NOT NULL,\r\n discount_details jsonb,\r\n CONSTRAINT orders_discount_total_check CHECK ((discount_total >= 0)),\r\n CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK ((exchange_rate_at_purchase > (0)::numeric)),\r\n CONSTRAINT orders_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT orders_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'trial'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text, 'refunded'::text])))\r\n);\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_license_id IS 'Freemius license ID used to reconcile checkout callbacks and webhooks.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_subscription_id IS 'Freemius subscription ID when the order is associated with recurring billing.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_trial_id IS 'Freemius trial ID when checkout starts in trial mode.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_trial_ends_at IS 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_last_event_type IS 'Last Freemius checkout callback or webhook event applied to the order.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_last_synced_at IS 'Timestamp when Freemius metadata was last reconciled locally.';\r\n\r\nCOMMENT ON COLUMN public.orders.currency IS 'ISO currency code used for the order totals.';\r\n\r\nCOMMENT ON COLUMN public.orders.subtotal IS 'Subtotal before shipping and tax, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.shipping_total IS 'Shipping amount before tax, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.tax_total IS 'Total tax amount collected for the order, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.tax_details IS 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';\r\n\r\nCOMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS 'Exchange rate locked at purchase time relative to the store default currency.';\r\n\r\nCOMMENT ON COLUMN public.orders.invoice_number IS 'Stable printable invoice number assigned once when the order first becomes paid.';\r\n\r\nCOMMENT ON COLUMN public.orders.paid_at IS 'Timestamp when the order was first marked as paid.';\r\n\r\nCOMMENT ON COLUMN public.orders.coupon_code IS 'Coupon code applied to the order at checkout time.';\r\n\r\nCOMMENT ON COLUMN public.orders.discount_total IS 'Total discount applied to this order in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.discount_details IS 'Provider-aware coupon quote details captured at checkout.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.package_activations (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n license_key text NOT NULL,\r\n instance_name text NOT NULL,\r\n package_id text NOT NULL,\r\n status text DEFAULT 'active'::text NOT NULL,\r\n meta jsonb DEFAULT '{}'::jsonb,\r\n last_validated_at timestamp with time zone DEFAULT now(),\r\n created_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.page_revisions (\r\n id bigint NOT NULL,\r\n page_id bigint NOT NULL,\r\n author_id uuid,\r\n version integer NOT NULL,\r\n revision_type public.revision_type NOT NULL,\r\n content jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';\r\n\r\nCOMMENT ON COLUMN public.page_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.page_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.page_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.page_revisions_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.pages (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n author_id uuid,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\r\n meta_title text,\r\n meta_description text,\r\n version integer DEFAULT 1 NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n feature_image_id uuid\r\n);\r\n\r\nCOMMENT ON TABLE public.pages IS 'Stores static pages for the website.';\r\n\r\nCOMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';\r\n\r\nCOMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';\r\n\r\nCOMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different language versions of the same conceptual page.';\r\n\r\nCOMMENT ON COLUMN public.pages.feature_image_id IS 'ID of the media item to be used as the page feature image.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.pages'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.pages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.pages_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.post_revisions (\r\n id bigint NOT NULL,\r\n post_id bigint NOT NULL,\r\n author_id uuid,\r\n version integer NOT NULL,\r\n revision_type public.revision_type NOT NULL,\r\n content jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';\r\n\r\nCOMMENT ON COLUMN public.post_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.post_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.post_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.post_revisions_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.posts (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n author_id uuid,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n label text,\r\n excerpt text,\r\n subtitle text,\r\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\r\n published_at timestamp with time zone,\r\n meta_title text,\r\n meta_description text,\r\n feature_image_id uuid,\r\n version integer DEFAULT 1 NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';\r\n\r\nCOMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';\r\n\r\nCOMMENT ON COLUMN public.posts.label IS 'Short editorial label rendered as a pill on post hero and article cards.';\r\n\r\nCOMMENT ON COLUMN public.posts.excerpt IS 'Short editorial summary used in post metadata rows and post cards.';\r\n\r\nCOMMENT ON COLUMN public.posts.subtitle IS 'Longer deck shown under the post title.';\r\n\r\nCOMMENT ON COLUMN public.posts.feature_image_id IS 'ID of the media item to be used as the feature image.';\r\n\r\nCOMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';\r\n\r\nCOMMENT ON COLUMN public.posts.translation_group_id IS 'Groups different language versions of the same conceptual post.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.posts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.posts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.posts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.privacy_consent_logs (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n consent_token text NOT NULL,\r\n categories jsonb DEFAULT '{\"analytics\": false, \"marketing\": false, \"necessary\": true}'::jsonb NOT NULL,\r\n ip_masked text,\r\n user_agent text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT privacy_consent_logs_categories_check CHECK ((jsonb_typeof(categories) = 'object'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.privacy_consent_logs IS 'Immutable audit log of visitor consent decisions for Quebec Law 25 / PIPEDA accountability.';\r\n\r\nCOMMENT ON COLUMN public.privacy_consent_logs.consent_token IS 'Opaque token also stored in the nb_consent_preference cookie to correlate a decision with its record.';\r\n\r\nCOMMENT ON COLUMN public.privacy_consent_logs.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address for an analytics/marketing consent log.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_attribute_terms (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n attribute_id uuid NOT NULL,\r\n value text NOT NULL,\r\n slug text NOT NULL,\r\n sort_order integer DEFAULT 0 NOT NULL,\r\n value_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_attributes (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n slug text NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_categories (\r\n product_id uuid NOT NULL,\r\n category_id uuid NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_drafts (\r\n id bigint NOT NULL,\r\n product_id uuid NOT NULL,\r\n author_id uuid,\r\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n CONSTRAINT product_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\r\n CONSTRAINT product_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.product_drafts IS 'Draft product metadata snapshots used by Draft Mode and front-end visual editing before publishing to live product rows.';\r\n\r\nCOMMENT ON COLUMN public.product_drafts.product_id IS 'ID of the product being drafted.';\r\n\r\nCOMMENT ON COLUMN public.product_drafts.meta IS 'Draft product metadata snapshot. Front-end visual editing currently mutates visible title, short_description, and description_json fields.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.product_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.product_drafts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_freemius_sale_coupons (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n freemius_product_id text NOT NULL,\r\n freemius_plan_id text,\r\n freemius_coupon_id text,\r\n freemius_coupon_code text NOT NULL,\r\n discount_percent integer,\r\n starts_at timestamp with time zone,\r\n ends_at timestamp with time zone,\r\n is_active boolean DEFAULT false NOT NULL,\r\n sync_status text DEFAULT 'pending'::text NOT NULL,\r\n sync_error text,\r\n remote_payload jsonb,\r\n last_synced_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT product_freemius_sale_coupons_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\r\n CONSTRAINT product_freemius_sale_coupons_discount_valid CHECK (((discount_percent IS NULL) OR ((discount_percent > 0) AND (discount_percent <= 100)))),\r\n CONSTRAINT product_freemius_sale_coupons_fm_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\r\n CONSTRAINT product_freemius_sale_coupons_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text]))),\r\n CONSTRAINT product_freemius_sale_coupons_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at)))\r\n);\r\n\r\nCOMMENT ON TABLE public.product_freemius_sale_coupons IS 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_media (\r\n product_id uuid NOT NULL,\r\n media_id uuid NOT NULL,\r\n sort_order integer DEFAULT 0\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_variants (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n sku text NOT NULL,\r\n price_adjustment integer DEFAULT 0 NOT NULL,\r\n price integer DEFAULT 0 NOT NULL,\r\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock_quantity integer DEFAULT 0 NOT NULL,\r\n upc text,\r\n main_media_id uuid,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n sale_start_at timestamp with time zone,\r\n sale_end_at timestamp with time zone,\r\n scheduled_price integer,\r\n scheduled_prices jsonb,\r\n scheduled_price_at timestamp with time zone,\r\n CONSTRAINT product_variants_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\r\n CONSTRAINT product_variants_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\r\n CONSTRAINT product_variants_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at)))\r\n);\r\n\r\nCOMMENT ON COLUMN public.product_variants.prices IS 'Variant regular prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_prices IS 'Variant sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC) for this variant. NULL means no lower bound.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC) for this variant. NULL means no upper bound.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.products (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n language_id bigint NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n sku text NOT NULL,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n product_type text NOT NULL,\r\n payment_provider text NOT NULL,\r\n price integer NOT NULL,\r\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock integer DEFAULT 0,\r\n status text DEFAULT 'draft'::text NOT NULL,\r\n meta_title text,\r\n meta_description text,\r\n short_description text,\r\n description_json jsonb,\r\n metadata jsonb,\r\n freemius_plan_id text,\r\n freemius_product_id text,\r\n trial_period_days integer DEFAULT 0 NOT NULL,\r\n trial_requires_payment_method boolean DEFAULT false NOT NULL,\r\n upc text,\r\n is_taxable boolean DEFAULT true NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n sale_start_at timestamp with time zone,\r\n sale_end_at timestamp with time zone,\r\n scheduled_price integer,\r\n scheduled_prices jsonb,\r\n scheduled_price_at timestamp with time zone,\r\n average_rating numeric(3,2) DEFAULT 0.00 NOT NULL,\r\n total_reviews integer DEFAULT 0 NOT NULL,\r\n CONSTRAINT products_payment_provider_check CHECK ((payment_provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT products_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\r\n CONSTRAINT products_product_type_check CHECK ((product_type = ANY (ARRAY['physical'::text, 'digital'::text]))),\r\n CONSTRAINT products_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\r\n CONSTRAINT products_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at))),\r\n CONSTRAINT products_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text]))),\r\n CONSTRAINT products_trial_period_days_check CHECK ((trial_period_days >= 0)),\r\n CONSTRAINT products_type_provider_consistency_check CHECK ((((product_type = 'physical'::text) AND (payment_provider = 'stripe'::text)) OR ((product_type = 'digital'::text) AND (payment_provider = 'freemius'::text))))\r\n);\r\n\r\nCOMMENT ON COLUMN public.products.prices IS 'Regular prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_prices IS 'Sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.products.is_taxable IS 'When true, this product participates in Stripe tax calculation.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC). NULL means no lower bound.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC). NULL means no upper bound. Both NULL = always-on sale.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\r\n\r\nCOMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rating of approved reviews.';\r\n\r\nCOMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.profiles (\r\n id uuid NOT NULL,\r\n updated_at timestamp with time zone,\r\n full_name text,\r\n avatar_url text,\r\n website text,\r\n github_username text,\r\n phone text,\r\n role public.user_role DEFAULT 'USER'::public.user_role NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';\r\n\r\nCOMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';\r\n\r\nCOMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zone_locations (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n zone_id uuid NOT NULL,\r\n country_code text NOT NULL,\r\n state_code text,\r\n postal_code text,\r\n created_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.country_code IS 'ISO 3166-1 alpha-2 country code.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.state_code IS 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.postal_code IS 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zone_methods (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n zone_id uuid NOT NULL,\r\n method_type text NOT NULL,\r\n cost_amount integer DEFAULT 0 NOT NULL,\r\n cost_currency text DEFAULT 'USD'::text NOT NULL,\r\n min_order_amount integer DEFAULT 0 NOT NULL,\r\n name text NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n currency_pricing_mode text DEFAULT 'auto'::text NOT NULL,\r\n cost_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n min_order_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n CONSTRAINT shipping_zone_methods_cost_amounts_include_source CHECK ((cost_amounts ? upper(cost_currency))),\r\n CONSTRAINT shipping_zone_methods_cost_amounts_valid CHECK (public.is_valid_currency_amount_map(cost_amounts)),\r\n CONSTRAINT shipping_zone_methods_cost_currency_format CHECK ((cost_currency ~ '^[A-Z]{3}$'::text)),\r\n CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid CHECK ((currency_pricing_mode = ANY (ARRAY['auto'::text, 'manual'::text]))),\r\n CONSTRAINT shipping_zone_methods_method_type_check CHECK ((method_type = ANY (ARRAY['flat_rate'::text, 'free_shipping'::text]))),\r\n CONSTRAINT shipping_zone_methods_min_order_amounts_include_source CHECK ((min_order_amounts ? upper(cost_currency))),\r\n CONSTRAINT shipping_zone_methods_min_order_amounts_valid CHECK (public.is_valid_currency_amount_map(min_order_amounts))\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.name_translations IS 'Localized shipping method labels keyed by language code. Example: {\"fr\": \"Livraison standard\"}.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS 'Shipping costs by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zones (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n priority_order integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.site_settings (\r\n key text NOT NULL,\r\n value jsonb\r\n);\r\n\r\nCOMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys (Cortex AI BYOK, Bot Protection Secret, Email secret, Payment secret) hold encrypted envelopes and are restricted to ADMIN via row-level policies.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.system_alerts (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n alert_type text NOT NULL,\r\n title text NOT NULL,\r\n message text NOT NULL,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n is_resolved boolean DEFAULT false NOT NULL,\r\n resolved_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT system_alerts_alert_type_check CHECK ((alert_type = ANY (ARRAY['merge_conflict'::text, 'runtime_update_available'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.system_alerts IS 'System notifications for the automated upstream-update architecture (merge conflicts, runtime updates available). Written by service-role; read by ADMINs.';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.alert_type IS 'One of: merge_conflict, runtime_update_available.';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.metadata IS 'Structured deep-link context for the dashboard banner CTA (repo/branch/action_url or latest_version/download_url).';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.is_resolved IS 'When true the alert is hidden from the dashboard banner.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.system_configuration (\r\n id integer DEFAULT 1 NOT NULL,\r\n auto_accept_signups boolean DEFAULT false NOT NULL,\r\n settings jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT system_configuration_singleton CHECK ((id = 1))\r\n);\r\n\r\nCOMMENT ON TABLE public.system_configuration IS 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.tax_rates (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n country_code text NOT NULL,\r\n state_code text,\r\n tax_name text NOT NULL,\r\n tax_rate numeric(7,4) NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT tax_rates_tax_name_check CHECK ((char_length(btrim(tax_name)) > 0)),\r\n CONSTRAINT tax_rates_tax_rate_check CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))\r\n);\r\n\r\nCOMMENT ON TABLE public.tax_rates IS 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.country_code IS 'ISO 3166-1 alpha-2 country code.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.state_code IS 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.tax_name IS 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.tax_rate IS 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.translations (\r\n key text NOT NULL,\r\n translations jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON COLUMN public.translations.key IS 'A unique, slugified identifier (e.g., \"sign_in_button_text\").';\r\n\r\nCOMMENT ON COLUMN public.translations.translations IS 'Stores translations as key-value pairs (e.g., {\"en\": \"Sign In\", \"fr\": \"S''inscrire\"}).';\r\n\r\nCREATE TABLE IF NOT EXISTS public.ucp_cart_sessions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n status text DEFAULT 'active'::text NOT NULL,\r\n currency text DEFAULT 'USD'::text NOT NULL,\r\n locale text,\r\n buyer_identity jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n context jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n signals jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n attribution jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n line_items jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n totals jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n checkout_url text,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,\r\n CONSTRAINT ucp_cart_sessions_attribution_object CHECK ((jsonb_typeof(attribution) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_buyer_identity_object CHECK ((jsonb_typeof(buyer_identity) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_context_object CHECK ((jsonb_typeof(context) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_line_items_array CHECK ((jsonb_typeof(line_items) = 'array'::text)),\r\n CONSTRAINT ucp_cart_sessions_metadata_object CHECK ((jsonb_typeof(metadata) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_signals_object CHECK ((jsonb_typeof(signals) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'cancelled'::text, 'completed'::text]))),\r\n CONSTRAINT ucp_cart_sessions_totals_array CHECK ((jsonb_typeof(totals) = 'array'::text))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_addresses (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n address_type text NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n recipient_name text,\r\n company_name text,\r\n line1 text,\r\n line2 text,\r\n city text,\r\n state text,\r\n postal_code text,\r\n country_code text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT user_addresses_address_type_check CHECK ((address_type = ANY (ARRAY['billing'::text, 'shipping'::text])))\r\n);\r\n\r\nCOMMENT ON COLUMN public.user_addresses.company_name IS 'Optional company or organization name for the address.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_security_settings (\r\n user_id uuid NOT NULL,\r\n mfa_enabled boolean DEFAULT false NOT NULL,\r\n mfa_type text,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT user_security_settings_mfa_type_check CHECK ((mfa_type = ANY (ARRAY['totp'::text, 'email'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.user_security_settings IS 'Per-user multi-factor configuration. mfa_type is NULL until a factor is enrolled.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_trusted_devices (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n device_hash text NOT NULL,\r\n browser_metadata text,\r\n expires_at timestamp with time zone NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.user_trusted_devices IS 'SHA-256 hashes of trusted-device tokens. A 2FA bypass is only honoured when a non-expired row matches the cookie token, so deleting a row instantly revokes trust.';\r\n\r\nCOMMENT ON COLUMN public.user_trusted_devices.device_hash IS 'SHA-256 of the raw token held in the nb_trusted_device cookie. The raw token is never stored.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.variant_attribute_mapping (\r\n variant_id uuid NOT NULL,\r\n attribute_term_id uuid NOT NULL\r\n);\r\n"
20
+ "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\n-- 00 · schema: enums, functions, tables, sequences, defaults\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\n\nSET check_function_bodies = false;\n\nCREATE SCHEMA IF NOT EXISTS public;\n\nCOMMENT ON SCHEMA public IS 'standard public schema';\n\nDO $rb$ BEGIN\nCREATE TYPE public.approval_status AS ENUM (\n 'pending',\n 'approved',\n 'denied'\n);\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\n\nDO $rb$ BEGIN\nCREATE TYPE public.interaction_type AS ENUM (\n 'review',\n 'comment'\n);\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\n\nDO $rb$ BEGIN\nCREATE TYPE public.menu_location AS ENUM (\n 'HEADER',\n 'FOOTER',\n 'SIDEBAR'\n);\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\n\nDO $rb$ BEGIN\nCREATE TYPE public.page_status AS ENUM (\n 'draft',\n 'published',\n 'archived'\n);\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\n\nDO $rb$ BEGIN\nCREATE TYPE public.revision_type AS ENUM (\n 'snapshot',\n 'diff'\n);\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\n\nDO $rb$ BEGIN\nCREATE TYPE public.user_role AS ENUM (\n 'ADMIN',\n 'WRITER',\n 'USER'\n);\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\n\nCREATE OR REPLACE FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) RETURNS void\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_track_quantities boolean := public.get_ecommerce_track_quantities();\n v_item record;\n v_inventory_deducted_at timestamptz;\n v_sku text;\n v_current_quantity integer;\nBEGIN\n SELECT inventory_deducted_at\n INTO v_inventory_deducted_at\n FROM public.orders\n WHERE id = p_order_id\n FOR UPDATE;\n\n IF NOT FOUND OR v_inventory_deducted_at IS NOT NULL THEN\n RETURN;\n END IF;\n\n IF NOT v_track_quantities THEN\n UPDATE public.orders\n SET inventory_deducted_at = now()\n WHERE id = p_order_id;\n\n RETURN;\n END IF;\n\n FOR v_item IN\n SELECT\n product_id,\n variant_id,\n SUM(quantity)::integer AS quantity\n FROM public.order_items\n WHERE order_id = p_order_id\n GROUP BY product_id, variant_id\n LOOP\n v_sku := NULL;\n v_current_quantity := 0;\n\n IF v_item.variant_id IS NOT NULL THEN\n SELECT\n sku,\n GREATEST(COALESCE(stock_quantity, 0), 0)\n INTO v_sku,\n v_current_quantity\n FROM public.product_variants\n WHERE id = v_item.variant_id\n LIMIT 1;\n ELSIF v_item.product_id IS NOT NULL THEN\n SELECT\n sku,\n GREATEST(COALESCE(stock, 0), 0)\n INTO v_sku,\n v_current_quantity\n FROM public.products\n WHERE id = v_item.product_id\n LIMIT 1;\n END IF;\n\n IF NULLIF(trim(v_sku), '') IS NULL THEN\n CONTINUE;\n END IF;\n\n INSERT INTO public.inventory_items (sku, quantity)\n VALUES (v_sku, v_current_quantity)\n ON CONFLICT (sku) DO NOTHING;\n\n UPDATE public.inventory_items\n SET\n quantity = GREATEST(COALESCE(quantity, 0) - v_item.quantity, 0),\n updated_at = now()\n WHERE sku = v_sku;\n END LOOP;\n\n UPDATE public.orders\n SET inventory_deducted_at = now()\n WHERE id = p_order_id;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone DEFAULT now()) RETURNS TABLE(invoice_number text, paid_at timestamp with time zone)\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_effective_paid_at timestamptz;\nBEGIN\n SELECT *\n INTO v_order\n FROM public.orders\n WHERE id = p_order_id\n FOR UPDATE;\n\n IF NOT FOUND THEN\n RAISE EXCEPTION 'Order % not found', p_order_id;\n END IF;\n\n v_effective_paid_at := COALESCE(v_order.paid_at, p_paid_at, now(), v_order.created_at);\n\n UPDATE public.orders\n SET\n invoice_number = COALESCE(v_order.invoice_number, public.generate_order_invoice_number()),\n paid_at = v_effective_paid_at\n WHERE id = p_order_id\n RETURNING orders.invoice_number, orders.paid_at\n INTO invoice_number, paid_at;\n\n RETURN NEXT;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.clear_currency_price_overrides(target_currency text) RETURNS void\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nDECLARE\n v_target_currency text := upper(trim(target_currency));\nBEGIN\n IF v_target_currency = '' THEN\n RETURN;\n END IF;\n\n UPDATE public.products\n SET\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\n sale_prices = CASE\n WHEN sale_prices IS NULL THEN NULL\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\n ELSE sale_prices - v_target_currency\n END,\n updated_at = now()\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\n\n UPDATE public.product_variants\n SET\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\n sale_prices = CASE\n WHEN sale_prices IS NULL THEN NULL\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\n ELSE sale_prices - v_target_currency\n END,\n updated_at = now()\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb) RETURNS boolean\n LANGUAGE sql IMMUTABLE\n SET search_path TO ''\n AS $_$\n SELECT CASE\n WHEN jsonb_typeof(candidate) <> 'array' THEN false\n ELSE\n NOT EXISTS (\n SELECT 1\n FROM jsonb_array_elements(candidate) AS field(value)\n WHERE jsonb_typeof(field.value) <> 'object'\n OR jsonb_typeof(field.value -> 'key') IS DISTINCT FROM 'string'\n OR jsonb_typeof(field.value -> 'label') IS DISTINCT FROM 'string'\n OR jsonb_typeof(field.value -> 'type') IS DISTINCT FROM 'string'\n OR field.value ->> 'key' !~ '^[a-z][a-z0-9_]*$'\n OR field.value ->> 'type' NOT IN ('text', 'rich-text', 'image_r2', 'db_relation')\n )\n AND (\n SELECT COUNT(*) = COUNT(DISTINCT field.value ->> 'key')\n FROM jsonb_array_elements(candidate) AS field(value)\n )\n END;\n$_$;\n\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) RETURNS boolean\n LANGUAGE sql IMMUTABLE\n SET search_path TO ''\n AS $$\n SELECT CASE\n WHEN jsonb_typeof(candidate) <> 'object' THEN false\n ELSE candidate ->> 'type' IN ('container', 'field_render')\n END;\n$$;\n\nCREATE TABLE IF NOT EXISTS public.custom_block_definitions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n slug text NOT NULL,\n name text NOT NULL,\n description text DEFAULT ''::text NOT NULL,\n fields jsonb DEFAULT '[]'::jsonb NOT NULL,\n layout_schema jsonb NOT NULL,\n is_original boolean DEFAULT true NOT NULL,\n CONSTRAINT custom_block_definitions_fields_check CHECK (public.is_valid_custom_block_fields(fields)),\n CONSTRAINT custom_block_definitions_layout_schema_check CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),\n CONSTRAINT custom_block_definitions_name_check CHECK ((length(TRIM(BOTH FROM name)) > 0)),\n CONSTRAINT custom_block_definitions_slug_check CHECK ((slug ~ '^[a-z][a-z0-9-]*$'::text))\n);\n\nCOMMENT ON TABLE public.custom_block_definitions IS 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';\n\nCOMMENT ON COLUMN public.custom_block_definitions.fields IS 'Strict JSONB field declarations for data-rendered custom blocks.';\n\nCOMMENT ON COLUMN public.custom_block_definitions.layout_schema IS 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';\n\nCOMMENT ON COLUMN public.custom_block_definitions.is_original IS 'False when a definition was created by duplicating an existing registry row.';\n\nCREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid) RETURNS public.custom_block_definitions\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $_$\nDECLARE\n source_definition public.custom_block_definitions%ROWTYPE;\n copied_definition public.custom_block_definitions%ROWTYPE;\n base_slug text;\n copy_slug text;\n copy_index integer := 1;\nBEGIN\n IF auth.role() <> 'service_role'\n AND COALESCE((SELECT public.get_current_user_role())::text, '') NOT IN ('ADMIN', 'WRITER') THEN\n RAISE EXCEPTION 'Not authorized to duplicate custom block definitions.'\n USING ERRCODE = '42501';\n END IF;\n\n SELECT *\n INTO source_definition\n FROM public.custom_block_definitions\n WHERE id = target_id;\n\n IF NOT FOUND THEN\n RAISE EXCEPTION 'Custom block definition % not found.', target_id\n USING ERRCODE = 'P0002';\n END IF;\n\n base_slug := regexp_replace(source_definition.slug, '-copy(-[0-9]+)?$', '');\n copy_slug := base_slug || '-copy';\n\n WHILE EXISTS (\n SELECT 1\n FROM public.custom_block_definitions\n WHERE slug = copy_slug\n ) LOOP\n copy_index := copy_index + 1;\n copy_slug := base_slug || '-copy-' || copy_index;\n END LOOP;\n\n INSERT INTO public.custom_block_definitions (\n id,\n slug,\n name,\n description,\n fields,\n layout_schema,\n is_original\n )\n VALUES (\n gen_random_uuid(),\n copy_slug,\n source_definition.name || ' Copy',\n source_definition.description,\n source_definition.fields,\n source_definition.layout_schema,\n false\n )\n RETURNING *\n INTO copied_definition;\n\n RETURN copied_definition;\nEND;\n$_$;\n\nCREATE OR REPLACE FUNCTION public.format_order_invoice_number(p_value bigint) RETURNS text\n LANGUAGE sql IMMUTABLE\n SET search_path TO ''\n AS $$\n SELECT 'INV-' || lpad(p_value::text, 6, '0');\n$$;\n\nCREATE OR REPLACE FUNCTION public.generate_order_invoice_number() RETURNS text\n LANGUAGE sql\n SET search_path TO ''\n AS $$\n SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));\n$$;\n\nCREATE OR REPLACE FUNCTION public.get_current_user_role() RETURNS public.user_role\n LANGUAGE sql STABLE\n SET search_path TO 'public'\n AS $$\n SELECT role FROM public.profiles WHERE id = auth.uid();\n$$;\n\nCREATE OR REPLACE FUNCTION public.get_default_currency_code() RETURNS text\n LANGUAGE sql STABLE\n SET search_path TO ''\n AS $$\n SELECT COALESCE(\n (\n SELECT upper(code)\n FROM public.currencies\n WHERE is_default = true\n ORDER BY updated_at DESC, created_at DESC, code ASC\n LIMIT 1\n ),\n 'USD'\n );\n$$;\n\nCREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities() RETURNS boolean\n LANGUAGE plpgsql STABLE\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_value jsonb;\n v_raw text;\nBEGIN\n SELECT value\n INTO v_value\n FROM public.site_settings\n WHERE key = 'ecommerce_inventory_settings';\n\n IF v_value IS NULL THEN\n RETURN true;\n END IF;\n\n IF jsonb_typeof(v_value) = 'object' THEN\n v_raw := NULLIF(v_value->>'track_quantities', '');\n ELSE\n v_raw := NULLIF(trim(BOTH '\"' FROM v_value::text), '');\n END IF;\n\n IF v_raw IS NULL THEN\n RETURN true;\n END IF;\n\n IF lower(v_raw) IN ('false', 'f', '0', 'no', 'off') THEN\n RETURN false;\n END IF;\n\n RETURN true;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.get_my_claim(claim text) RETURNS jsonb\n LANGUAGE sql STABLE\n SET search_path TO ''\n AS $$\n SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_blocks_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_coupon_freemius_mappings_write() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\n NEW.updated_at := now();\n\n IF NEW.created_at IS NULL THEN\n NEW.created_at := now();\n END IF;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_coupons_write() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.code := upper(regexp_replace(btrim(NEW.code), '\\s+', '', 'g'));\n NEW.name := btrim(NEW.name);\n NEW.provider_scope := lower(btrim(NEW.provider_scope));\n NEW.discount_type := lower(btrim(NEW.discount_type));\n NEW.freemius_sync_status := lower(btrim(COALESCE(NEW.freemius_sync_status, 'not_synced')));\n NEW.updated_at := now();\n\n IF NEW.created_at IS NULL THEN\n NEW.created_at := now();\n END IF;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_default_currency_change() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n IF TG_OP = 'INSERT' THEN\n IF NEW.is_default THEN\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\n END IF;\n ELSIF NEW.is_default\n AND (\n OLD.is_default IS DISTINCT FROM NEW.is_default\n OR OLD.code IS DISTINCT FROM NEW.code\n ) THEN\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\n END IF;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_inventory_item_change() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_sku text := COALESCE(NEW.sku, OLD.sku);\nBEGIN\n PERFORM public.sync_inventory_cache_for_sku(v_sku);\n RETURN COALESCE(NEW, OLD);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_inventory_items_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_languages_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_media_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_navigation_items_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n admin_flag_set boolean := false;\n user_role public.user_role;\n v_full_name text;\n v_avatar_url text;\n v_github_username text;\n v_provider text;\nBEGIN\n INSERT INTO public.site_settings (key, value)\n VALUES ('is_admin_created', 'false'::jsonb)\n ON CONFLICT (key) DO NOTHING;\n\n SELECT COALESCE(value::jsonb::boolean, false)\n INTO admin_flag_set\n FROM public.site_settings\n WHERE key = 'is_admin_created'\n FOR UPDATE;\n\n IF admin_flag_set = false THEN\n user_role := 'ADMIN'::public.user_role;\n\n UPDATE public.site_settings\n SET value = 'true'::jsonb\n WHERE key = 'is_admin_created';\n ELSE\n user_role := 'USER'::public.user_role;\n END IF;\n\n v_full_name := NEW.raw_user_meta_data->>'full_name';\n v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';\n v_provider := NEW.raw_app_meta_data->>'provider';\n\n IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN\n v_github_username := COALESCE(\n NEW.raw_user_meta_data->>'user_name',\n NEW.raw_user_meta_data->>'preferred_username'\n );\n ELSE\n v_github_username := NULL;\n END IF;\n\n INSERT INTO public.profiles (\n id,\n role,\n full_name,\n avatar_url,\n github_username\n )\n VALUES (\n NEW.id,\n user_role,\n v_full_name,\n v_avatar_url,\n v_github_username\n )\n ON CONFLICT (id) DO UPDATE SET\n full_name = EXCLUDED.full_name,\n avatar_url = EXCLUDED.avatar_url,\n github_username = EXCLUDED.github_username;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_pages_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_posts_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_product_freemius_sale_coupons_write() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\n NEW.updated_at := now();\n\n IF NEW.created_at IS NULL THEN\n NEW.created_at := now();\n END IF;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.country_code = upper(btrim(NEW.country_code));\n NEW.state_code = CASE\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\n ELSE upper(btrim(NEW.state_code))\n END;\n NEW.postal_code = CASE\n WHEN NEW.postal_code IS NULL OR btrim(NEW.postal_code) = '' THEN NULL\n ELSE upper(btrim(NEW.postal_code))\n END;\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_tax_rates_write() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.country_code = upper(btrim(NEW.country_code));\n NEW.state_code = CASE\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\n ELSE upper(btrim(NEW.state_code))\n END;\n NEW.tax_name = btrim(NEW.tax_name);\n NEW.updated_at = now();\n\n IF NEW.created_at IS NULL THEN\n NEW.created_at = now();\n END IF;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.handle_ucp_cart_sessions_update() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.is_admin() RETURNS boolean\n LANGUAGE sql STABLE\n SET search_path TO 'public'\n AS $$\n SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();\n$$;\n\nCREATE OR REPLACE FUNCTION public.is_valid_currency_amount_map(amounts jsonb) RETURNS boolean\n LANGUAGE sql IMMUTABLE\n SET search_path TO ''\n AS $_$\n SELECT CASE\n WHEN amounts IS NULL THEN false\n WHEN jsonb_typeof(amounts) <> 'object' THEN false\n WHEN amounts = '{}'::jsonb THEN false\n ELSE NOT EXISTS (\n SELECT 1\n FROM jsonb_each(amounts) AS entry\n WHERE entry.key !~ '^[A-Z]{3}$'\n OR jsonb_typeof(entry.value) <> 'number'\n OR entry.value::text !~ '^[0-9]+$'\n )\n END;\n$_$;\n\nCREATE OR REPLACE FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) RETURNS boolean\n LANGUAGE sql IMMUTABLE\n SET search_path TO ''\n AS $_$\n SELECT CASE\n WHEN sale_prices IS NULL THEN true\n WHEN jsonb_typeof(sale_prices) <> 'object' THEN false\n WHEN sale_prices = '{}'::jsonb THEN true\n WHEN prices IS NULL OR jsonb_typeof(prices) <> 'object' THEN false\n ELSE NOT EXISTS (\n SELECT 1\n FROM jsonb_each(sale_prices) AS entry\n WHERE entry.key !~ '^[A-Z]{3}$'\n OR NOT (prices ? entry.key)\n OR jsonb_typeof(entry.value) <> 'number'\n OR entry.value::text !~ '^[0-9]+$'\n OR entry.value::text::numeric > (prices ->> entry.key)::numeric\n )\n END;\n$_$;\n\nCREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb) RETURNS jsonb\n LANGUAGE sql IMMUTABLE\n SET search_path TO ''\n AS $_$\n SELECT CASE\n WHEN amounts IS NULL THEN '{}'::jsonb\n WHEN jsonb_typeof(amounts) <> 'object' THEN amounts\n ELSE COALESCE(\n (\n SELECT jsonb_object_agg(\n upper(trim(entry.key)),\n CASE\n WHEN jsonb_typeof(entry.value) = 'number'\n AND entry.value::text ~ '^[0-9]+$' THEN\n to_jsonb((entry.value::text)::bigint)\n ELSE\n entry.value\n END\n )\n FROM jsonb_each(amounts) AS entry\n ),\n '{}'::jsonb\n )\n END;\n$_$;\n\nCREATE OR REPLACE FUNCTION public.set_currency_defaults() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nBEGIN\n NEW.code := upper(trim(NEW.code));\n NEW.updated_at := now();\n\n IF NEW.is_default THEN\n NEW.is_active := true;\n NEW.exchange_rate := 1;\n NEW.auto_update_exchange_rate := false;\n NEW.auto_sync_product_prices := false;\n NEW.exchange_rate_source := COALESCE(NULLIF(NEW.exchange_rate_source, ''), 'store-default');\n NEW.exchange_rate_updated_at := COALESCE(NEW.exchange_rate_updated_at, now());\n\n UPDATE public.currencies\n SET is_default = false,\n updated_at = now()\n WHERE id IS DISTINCT FROM NEW.id\n AND is_default = true;\n ELSIF NULLIF(NEW.exchange_rate_source, '') IS NULL THEN\n NEW.exchange_rate_source := NULL;\n END IF;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nDECLARE\n _new record;\nBEGIN\n _new := NEW;\n _new.updated_at = now();\n RETURN _new;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.sync_currency_price_maps() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nDECLARE\n v_default_currency text := public.get_default_currency_code();\n v_price_map_changed boolean := false;\n v_legacy_changed boolean := false;\nBEGIN\n NEW.prices := public.normalize_currency_amount_map(COALESCE(NEW.prices, '{}'::jsonb));\n NEW.sale_prices := public.normalize_currency_amount_map(COALESCE(NEW.sale_prices, '{}'::jsonb));\n\n IF NEW.sale_prices = '{}'::jsonb THEN\n NEW.sale_prices := NULL;\n END IF;\n\n IF TG_OP = 'UPDATE' THEN\n v_price_map_changed :=\n NEW.prices IS DISTINCT FROM OLD.prices\n OR NEW.sale_prices IS DISTINCT FROM OLD.sale_prices;\n v_legacy_changed :=\n NEW.price IS DISTINCT FROM OLD.price\n OR NEW.sale_price IS DISTINCT FROM OLD.sale_price;\n END IF;\n\n IF TG_OP = 'INSERT' THEN\n IF NEW.prices ? v_default_currency THEN\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\n ELSE\n NEW.prices := NEW.prices || jsonb_build_object(\n v_default_currency,\n GREATEST(COALESCE(NEW.price, 0), 0)\n );\n END IF;\n\n IF NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\n NEW.sale_price := (NEW.sale_prices ->> v_default_currency)::integer;\n ELSIF NEW.sale_price IS NOT NULL THEN\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\n END IF;\n\n RETURN NEW;\n END IF;\n\n IF v_price_map_changed AND NOT v_legacy_changed THEN\n IF NOT (NEW.prices ? v_default_currency) THEN\n NEW.prices := NEW.prices || jsonb_build_object(\n v_default_currency,\n GREATEST(COALESCE(OLD.price, NEW.price, 0), 0)\n );\n END IF;\n\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\n NEW.sale_price := CASE\n WHEN NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\n (NEW.sale_prices ->> v_default_currency)::integer\n ELSE\n NULL\n END;\n\n RETURN NEW;\n END IF;\n\n NEW.prices := NEW.prices || jsonb_build_object(\n v_default_currency,\n GREATEST(COALESCE(NEW.price, 0), 0)\n );\n\n IF NEW.sale_price IS NULL THEN\n IF NEW.sale_prices IS NOT NULL THEN\n NEW.sale_prices := NEW.sale_prices - v_default_currency;\n\n IF NEW.sale_prices = '{}'::jsonb THEN\n NEW.sale_prices := NULL;\n END IF;\n END IF;\n ELSE\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\n END IF;\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text) RETURNS void\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_quantity integer := 0;\nBEGIN\n IF NULLIF(trim(p_sku), '') IS NULL THEN\n RETURN;\n END IF;\n\n SELECT quantity\n INTO v_quantity\n FROM public.inventory_items\n WHERE sku = p_sku\n LIMIT 1;\n\n v_quantity := COALESCE(v_quantity, 0);\n\n UPDATE public.product_variants\n SET\n stock_quantity = v_quantity,\n updated_at = now()\n WHERE sku = p_sku;\n\n UPDATE public.products AS products\n SET\n stock = v_quantity,\n updated_at = now()\n WHERE products.sku = p_sku\n AND NOT EXISTS (\n SELECT 1\n FROM public.product_variants\n WHERE product_id = products.id\n );\n\n UPDATE public.products AS products\n SET\n stock = COALESCE((\n SELECT SUM(COALESCE(inventory.quantity, 0))\n FROM public.product_variants AS variants\n LEFT JOIN public.inventory_items AS inventory\n ON inventory.sku = variants.sku\n WHERE variants.product_id = products.id\n ), 0),\n updated_at = now()\n WHERE EXISTS (\n SELECT 1\n FROM public.product_variants AS variants\n WHERE variants.product_id = products.id\n AND variants.sku = p_sku\n );\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) RETURNS void\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nDECLARE\n v_target_currency text := upper(trim(target_currency));\nBEGIN\n UPDATE public.products\n SET\n prices = CASE\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\n END,\n sale_prices = CASE\n WHEN sale_price IS NULL THEN sale_prices\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\n END,\n price = CASE\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\n ELSE\n price\n END,\n sale_price = CASE\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\n (sale_prices ->> v_target_currency)::integer\n ELSE\n sale_price\n END,\n updated_at = now()\n WHERE\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\n OR (\n sale_price IS NOT NULL\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\n )\n OR (\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\n )\n OR (\n sale_prices IS NOT NULL\n AND sale_prices ? v_target_currency\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\n );\n\n UPDATE public.product_variants\n SET\n prices = CASE\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\n END,\n sale_prices = CASE\n WHEN sale_price IS NULL THEN sale_prices\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\n END,\n price = CASE\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\n ELSE\n price\n END,\n sale_price = CASE\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\n (sale_prices ->> v_target_currency)::integer\n ELSE\n sale_price\n END,\n updated_at = now()\n WHERE\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\n OR (\n sale_price IS NOT NULL\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\n )\n OR (\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\n )\n OR (\n sale_prices IS NOT NULL\n AND sale_prices ? v_target_currency\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\n );\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.sync_shipping_method_currency_maps() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path TO ''\n AS $$\nDECLARE\n v_source_currency text;\nBEGIN\n v_source_currency := upper(trim(COALESCE(NULLIF(NEW.cost_currency, ''), public.get_default_currency_code())));\n\n NEW.cost_currency := v_source_currency;\n NEW.currency_pricing_mode := COALESCE(NULLIF(lower(trim(NEW.currency_pricing_mode)), ''), 'auto');\n NEW.cost_amounts := public.normalize_currency_amount_map(COALESCE(NEW.cost_amounts, '{}'::jsonb));\n NEW.min_order_amounts := public.normalize_currency_amount_map(COALESCE(NEW.min_order_amounts, '{}'::jsonb));\n\n IF NEW.currency_pricing_mode NOT IN ('auto', 'manual') THEN\n RAISE EXCEPTION 'Unsupported shipping currency pricing mode: %', NEW.currency_pricing_mode;\n END IF;\n\n IF NEW.cost_amounts = '{}'::jsonb THEN\n NEW.cost_amounts := jsonb_build_object(v_source_currency, GREATEST(COALESCE(NEW.cost_amount, 0), 0));\n ELSIF NOT (NEW.cost_amounts ? v_source_currency) THEN\n NEW.cost_amounts := NEW.cost_amounts || jsonb_build_object(\n v_source_currency,\n GREATEST(COALESCE(NEW.cost_amount, 0), 0)\n );\n END IF;\n\n IF NEW.min_order_amounts = '{}'::jsonb THEN\n NEW.min_order_amounts := jsonb_build_object(\n v_source_currency,\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\n );\n ELSIF NOT (NEW.min_order_amounts ? v_source_currency) THEN\n NEW.min_order_amounts := NEW.min_order_amounts || jsonb_build_object(\n v_source_currency,\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\n );\n END IF;\n\n IF NEW.currency_pricing_mode = 'auto' THEN\n NEW.cost_amounts := jsonb_build_object(\n v_source_currency,\n GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0)\n );\n NEW.min_order_amounts := jsonb_build_object(\n v_source_currency,\n GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0)\n );\n END IF;\n\n NEW.cost_amount := GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0);\n NEW.min_order_amount := GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0);\n NEW.updated_at := now();\n\n RETURN NEW;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.update_product_ratings() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_product_id uuid;\n v_avg numeric(3,2);\n v_count integer;\nBEGIN\n -- Determine which product_id we need to update\n IF TG_OP = 'DELETE' THEN\n v_product_id := OLD.product_id;\n ELSE\n v_product_id := NEW.product_id;\n END IF;\n\n IF v_product_id IS NOT NULL THEN\n -- Calculate average and count of approved reviews for this product\n SELECT COALESCE(avg(rating), 0.00), count(*)\n INTO v_avg, v_count\n FROM public.cms_interactions\n WHERE product_id = v_product_id\n AND type = 'review'\n AND status = 'approved';\n\n -- Update products table\n UPDATE public.products\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\n total_reviews = v_count\n WHERE id = v_product_id;\n END IF;\n\n -- Handle old product_id if it changed on UPDATE (e.g. transfer of reviews)\n IF TG_OP = 'UPDATE' AND OLD.product_id IS NOT NULL AND OLD.product_id <> NEW.product_id THEN\n SELECT COALESCE(avg(rating), 0.00), count(*)\n INTO v_avg, v_count\n FROM public.cms_interactions\n WHERE product_id = OLD.product_id\n AND type = 'review'\n AND status = 'approved';\n\n UPDATE public.products\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\n total_reviews = v_count\n WHERE id = OLD.product_id;\n END IF;\n\n RETURN NULL;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb) RETURNS uuid\n LANGUAGE plpgsql\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_product_id uuid := NULLIF(product_payload->>'id', '')::uuid;\n v_translation_group_id uuid := NULLIF(product_payload->>'translation_group_id', '')::uuid;\n v_product_type text := CASE\n WHEN product_payload->>'product_type' IN ('physical', 'digital') THEN\n product_payload->>'product_type'\n WHEN NULLIF(product_payload->>'freemius_product_id', '') IS NOT NULL\n OR NULLIF(product_payload->>'freemius_plan_id', '') IS NOT NULL THEN\n 'digital'\n ELSE\n 'physical'\n END;\n v_payment_provider text := CASE\n WHEN v_product_type = 'digital' THEN 'freemius'\n ELSE 'stripe'\n END;\n v_variants jsonb := COALESCE(product_payload->'variants', '[]'::jsonb);\n v_variant jsonb;\n v_variant_id uuid;\n v_term_id text;\n v_has_variants boolean := jsonb_typeof(v_variants) = 'array' AND jsonb_array_length(v_variants) > 0;\n v_total_variant_stock integer := 0;\nBEGIN\n IF NOT public.is_admin() THEN\n RAISE EXCEPTION 'Admin access required';\n END IF;\n\n IF v_has_variants THEN\n SELECT COALESCE(SUM(COALESCE((value->>'stock_quantity')::integer, 0)), 0)\n INTO v_total_variant_stock\n FROM jsonb_array_elements(v_variants);\n END IF;\n\n IF v_product_id IS NULL THEN\n INSERT INTO public.products (\n title,\n slug,\n sku,\n product_type,\n payment_provider,\n upc,\n stock,\n status,\n short_description,\n description_json,\n metadata,\n price,\n prices,\n sale_price,\n sale_prices,\n sale_start_at,\n sale_end_at,\n freemius_plan_id,\n freemius_product_id,\n trial_period_days,\n trial_requires_payment_method,\n language_id,\n translation_group_id\n )\n VALUES (\n product_payload->>'title',\n product_payload->>'slug',\n product_payload->>'sku',\n v_product_type,\n v_payment_provider,\n NULLIF(product_payload->>'upc', ''),\n CASE\n WHEN v_has_variants THEN v_total_variant_stock\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\n END,\n COALESCE(product_payload->>'status', 'draft'),\n NULLIF(product_payload->>'short_description', ''),\n product_payload->'description_json',\n COALESCE(product_payload->'metadata', '{}'::jsonb),\n COALESCE((product_payload->>'price')::integer, 0),\n COALESCE(product_payload->'prices', '{}'::jsonb),\n CASE\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\n (product_payload->>'sale_price')::integer\n ELSE\n NULL\n END,\n CASE\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\n ELSE NULL\n END,\n CASE\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\n (product_payload->>'sale_start_at')::timestamptz\n ELSE\n NULL\n END,\n CASE\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\n (product_payload->>'sale_end_at')::timestamptz\n ELSE\n NULL\n END,\n NULLIF(product_payload->>'freemius_plan_id', ''),\n NULLIF(product_payload->>'freemius_product_id', ''),\n COALESCE((product_payload->>'trial_period_days')::integer, 0),\n COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\n (product_payload->>'language_id')::bigint,\n COALESCE(v_translation_group_id, gen_random_uuid())\n )\n RETURNING id INTO v_product_id;\n ELSE\n UPDATE public.products\n SET\n title = product_payload->>'title',\n slug = product_payload->>'slug',\n sku = product_payload->>'sku',\n product_type = v_product_type,\n payment_provider = v_payment_provider,\n upc = NULLIF(product_payload->>'upc', ''),\n stock = CASE\n WHEN v_has_variants THEN v_total_variant_stock\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\n END,\n status = COALESCE(product_payload->>'status', status),\n short_description = NULLIF(product_payload->>'short_description', ''),\n description_json = product_payload->'description_json',\n metadata = COALESCE(product_payload->'metadata', '{}'::jsonb),\n price = COALESCE((product_payload->>'price')::integer, 0),\n prices = COALESCE(product_payload->'prices', '{}'::jsonb),\n sale_price = CASE\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\n (product_payload->>'sale_price')::integer\n ELSE\n NULL\n END,\n sale_prices = CASE\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\n ELSE NULL\n END,\n sale_start_at = CASE\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\n (product_payload->>'sale_start_at')::timestamptz\n ELSE\n NULL\n END,\n sale_end_at = CASE\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\n (product_payload->>'sale_end_at')::timestamptz\n ELSE\n NULL\n END,\n freemius_plan_id = NULLIF(product_payload->>'freemius_plan_id', ''),\n freemius_product_id = NULLIF(product_payload->>'freemius_product_id', ''),\n trial_period_days = COALESCE((product_payload->>'trial_period_days')::integer, 0),\n trial_requires_payment_method = COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\n language_id = COALESCE((product_payload->>'language_id')::bigint, language_id),\n translation_group_id = COALESCE(v_translation_group_id, translation_group_id),\n updated_at = now()\n WHERE id = v_product_id;\n\n IF NOT FOUND THEN\n RAISE EXCEPTION 'Product not found';\n END IF;\n END IF;\n\n DELETE FROM public.variant_attribute_mapping\n WHERE variant_id IN (\n SELECT id\n FROM public.product_variants\n WHERE product_id = v_product_id\n );\n\n DELETE FROM public.product_variants\n WHERE product_id = v_product_id;\n\n IF v_has_variants THEN\n FOR v_variant IN\n SELECT value FROM jsonb_array_elements(v_variants)\n LOOP\n INSERT INTO public.product_variants (\n product_id,\n sku,\n upc,\n price,\n prices,\n sale_price,\n sale_prices,\n sale_start_at,\n sale_end_at,\n stock_quantity,\n main_media_id\n )\n VALUES (\n v_product_id,\n v_variant->>'sku',\n NULLIF(v_variant->>'upc', ''),\n COALESCE((v_variant->>'price')::integer, 0),\n COALESCE(v_variant->'prices', '{}'::jsonb),\n CASE\n WHEN v_variant ? 'sale_price' AND v_variant->>'sale_price' <> '' THEN\n (v_variant->>'sale_price')::integer\n ELSE\n NULL\n END,\n CASE\n WHEN v_variant ? 'sale_prices' THEN COALESCE(v_variant->'sale_prices', '{}'::jsonb)\n ELSE NULL\n END,\n CASE\n WHEN v_variant ? 'sale_start_at' AND v_variant->>'sale_start_at' <> '' THEN\n (v_variant->>'sale_start_at')::timestamptz\n ELSE\n NULL\n END,\n CASE\n WHEN v_variant ? 'sale_end_at' AND v_variant->>'sale_end_at' <> '' THEN\n (v_variant->>'sale_end_at')::timestamptz\n ELSE\n NULL\n END,\n COALESCE((v_variant->>'stock_quantity')::integer, 0),\n NULLIF(v_variant->>'main_media_id', '')::uuid\n )\n RETURNING id INTO v_variant_id;\n\n FOR v_term_id IN\n SELECT jsonb_array_elements_text(COALESCE(v_variant->'attribute_term_ids', '[]'::jsonb))\n LOOP\n INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)\n VALUES (v_variant_id, v_term_id::uuid);\n END LOOP;\n END LOOP;\n END IF;\n\n RETURN v_product_id;\nEND;\n$$;\n\nCREATE TABLE IF NOT EXISTS public.blocks (\n id bigint NOT NULL,\n page_id bigint,\n post_id bigint,\n language_id bigint NOT NULL,\n block_type text NOT NULL,\n content jsonb,\n \"order\" integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n product_id uuid,\n CONSTRAINT check_exactly_one_parent CHECK ((((page_id IS NOT NULL) AND (post_id IS NULL) AND (product_id IS NULL)) OR ((post_id IS NOT NULL) AND (page_id IS NULL) AND (product_id IS NULL)) OR ((product_id IS NOT NULL) AND (page_id IS NULL) AND (post_id IS NULL))))\n);\n\nCOMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';\n\nCOMMENT ON COLUMN public.blocks.block_type IS 'Type of the block, e.g., \"text\", \"image\".';\n\nCOMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';\n\nCOMMENT ON COLUMN public.blocks.\"order\" IS 'Sort order of the block.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.blocks'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.blocks ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.blocks_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.categories (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n name text NOT NULL,\n slug text NOT NULL,\n description text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\n description_translations jsonb DEFAULT '{}'::jsonb NOT NULL\n);\n\nCOMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';\n\nCOMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {\"fr\": \"Numérique\"}).';\n\nCOMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';\n\nCREATE TABLE IF NOT EXISTS public.cms_interactions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n type public.interaction_type NOT NULL,\n status public.approval_status DEFAULT 'pending'::public.approval_status NOT NULL,\n content text NOT NULL,\n rating integer,\n user_id uuid NOT NULL,\n product_id uuid,\n post_id bigint,\n reactions jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT check_product_or_post CHECK ((((product_id IS NOT NULL) AND (post_id IS NULL)) OR ((post_id IS NOT NULL) AND (product_id IS NULL)))),\n CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL))))\n);\n\nCOMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';\n\nCOMMENT ON COLUMN public.cms_interactions.rating IS 'Star rating 1-5, only populated for product reviews.';\n\nCOMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles.id';\n\nCOMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';\n\nCREATE TABLE IF NOT EXISTS public.content_drafts (\n id bigint NOT NULL,\n parent_type text NOT NULL,\n parent_id bigint NOT NULL,\n author_id uuid,\n base_version integer DEFAULT 1 NOT NULL,\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT content_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\n CONSTRAINT content_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text)),\n CONSTRAINT content_drafts_parent_type_check CHECK ((parent_type = ANY (ARRAY['page'::text, 'post'::text])))\n);\n\nCOMMENT ON TABLE public.content_drafts IS 'Draft snapshots used by Draft Mode and front-end visual editing before publishing to live page/post rows.';\n\nCOMMENT ON COLUMN public.content_drafts.parent_type IS 'The content table this draft belongs to: page or post.';\n\nCOMMENT ON COLUMN public.content_drafts.parent_id IS 'ID of the page or post being drafted.';\n\nCOMMENT ON COLUMN public.content_drafts.base_version IS 'Published page/post version the draft was created from.';\n\nCOMMENT ON COLUMN public.content_drafts.meta IS 'Draft page/post metadata snapshot.';\n\nCOMMENT ON COLUMN public.content_drafts.blocks IS 'Ordered draft block snapshot, including block ids, block types, content, language ids, and order values.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.content_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.content_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.content_drafts_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.cortex_ai_db_mutation_audit (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n actor_user_id uuid,\n tool_name text NOT NULL,\n action_name text NOT NULL,\n target_tables text[] DEFAULT '{}'::text[] NOT NULL,\n operation_summary text NOT NULL,\n payload_hash text NOT NULL,\n payload jsonb DEFAULT '{}'::jsonb NOT NULL,\n preview jsonb DEFAULT '{}'::jsonb NOT NULL,\n status text NOT NULL,\n error_message text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT cortex_ai_db_mutation_audit_status_check CHECK ((status = ANY (ARRAY['success'::text, 'failure'::text])))\n);\n\nCOMMENT ON TABLE public.cortex_ai_db_mutation_audit IS 'Audit trail for confirmed Cortex AI database mutation attempts.';\n\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.payload IS 'Redacted tool input payload for the confirmed mutation attempt.';\n\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.preview IS 'Redacted confirmation preview shown before the mutation was confirmed.';\n\nCREATE TABLE IF NOT EXISTS public.coupon_freemius_mappings (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n coupon_id uuid NOT NULL,\n product_id uuid,\n freemius_product_id text NOT NULL,\n freemius_coupon_id text,\n freemius_coupon_code text NOT NULL,\n sync_status text DEFAULT 'pending'::text NOT NULL,\n sync_error text,\n remote_payload jsonb,\n last_synced_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT coupon_freemius_mappings_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\n CONSTRAINT coupon_freemius_mappings_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\n CONSTRAINT coupon_freemius_mappings_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text])))\n);\n\nCREATE TABLE IF NOT EXISTS public.coupon_products (\n coupon_id uuid NOT NULL,\n product_id uuid NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.coupon_products IS 'Optional product allow-list for coupons. No rows means all products in the provider scope are eligible.';\n\nCREATE TABLE IF NOT EXISTS public.coupon_redemptions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n coupon_id uuid,\n order_id uuid,\n coupon_code text NOT NULL,\n provider text NOT NULL,\n discount_total integer DEFAULT 0 NOT NULL,\n user_id uuid,\n customer_email text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n redeemed_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT coupon_redemptions_code_not_blank CHECK ((char_length(btrim(coupon_code)) > 0)),\n CONSTRAINT coupon_redemptions_discount_total_check CHECK ((discount_total >= 0)),\n CONSTRAINT coupon_redemptions_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text])))\n);\n\nCREATE TABLE IF NOT EXISTS public.coupons (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n code text NOT NULL,\n name text NOT NULL,\n internal_note text,\n provider_scope text DEFAULT 'all'::text NOT NULL,\n discount_type text NOT NULL,\n discount_amount integer NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n starts_at timestamp with time zone,\n ends_at timestamp with time zone,\n redemption_limit integer,\n redemptions_count integer DEFAULT 0 NOT NULL,\n freemius_sync_status text DEFAULT 'not_synced'::text NOT NULL,\n freemius_sync_error text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT coupons_code_not_blank CHECK ((char_length(btrim(code)) > 0)),\n CONSTRAINT coupons_date_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at))),\n CONSTRAINT coupons_discount_amount_positive CHECK ((discount_amount > 0)),\n CONSTRAINT coupons_discount_type_valid CHECK ((discount_type = ANY (ARRAY['percent'::text, 'fixed'::text]))),\n CONSTRAINT coupons_freemius_sync_status_valid CHECK ((freemius_sync_status = ANY (ARRAY['not_synced'::text, 'pending'::text, 'synced'::text, 'failed'::text, 'not_required'::text]))),\n CONSTRAINT coupons_name_not_blank CHECK ((char_length(btrim(name)) > 0)),\n CONSTRAINT coupons_percent_amount_valid CHECK (((discount_type <> 'percent'::text) OR (discount_amount <= 100))),\n CONSTRAINT coupons_provider_scope_valid CHECK ((provider_scope = ANY (ARRAY['all'::text, 'stripe'::text, 'freemius'::text]))),\n CONSTRAINT coupons_redemption_limit_positive CHECK (((redemption_limit IS NULL) OR (redemption_limit > 0))),\n CONSTRAINT coupons_redemptions_count_nonnegative CHECK ((redemptions_count >= 0))\n);\n\nCOMMENT ON TABLE public.coupons IS 'Unified commerce coupons managed by NextBlock CMS and applied through provider-aware checkout.';\n\nCOMMENT ON COLUMN public.coupons.provider_scope IS 'Provider eligibility: all, stripe, or freemius.';\n\nCOMMENT ON COLUMN public.coupons.discount_amount IS 'Percent value for percent coupons, or fixed minor-unit amount for fixed coupons.';\n\nCOMMENT ON COLUMN public.coupons.freemius_sync_status IS 'Aggregate status for syncing this coupon to Freemius product coupon records.';\n\nCREATE TABLE IF NOT EXISTS public.currencies (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n code text NOT NULL,\n symbol text NOT NULL,\n exchange_rate numeric(20,10) NOT NULL,\n is_default boolean DEFAULT false NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n rounding_mode text DEFAULT 'none'::text NOT NULL,\n rounding_increment integer DEFAULT 1 NOT NULL,\n rounding_charm_amount integer,\n auto_update_exchange_rate boolean DEFAULT true NOT NULL,\n exchange_rate_updated_at timestamp with time zone,\n exchange_rate_source text,\n auto_sync_product_prices boolean DEFAULT false NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT currencies_charm_requires_amount CHECK (((rounding_mode <> 'charm'::text) OR (rounding_charm_amount IS NOT NULL))),\n CONSTRAINT currencies_code_check CHECK ((code ~ '^[A-Z]{3}$'::text)),\n CONSTRAINT currencies_default_auto_update_disabled CHECK (((NOT is_default) OR (auto_update_exchange_rate = false))),\n CONSTRAINT currencies_default_exchange_rate_is_one CHECK (((NOT is_default) OR (exchange_rate = (1)::numeric))),\n CONSTRAINT currencies_default_must_be_active CHECK (((NOT is_default) OR is_active)),\n CONSTRAINT currencies_default_product_price_sync_disabled CHECK (((NOT is_default) OR (auto_sync_product_prices = false))),\n CONSTRAINT currencies_exchange_rate_check CHECK ((exchange_rate > (0)::numeric)),\n CONSTRAINT currencies_rounding_charm_nonnegative CHECK (((rounding_charm_amount IS NULL) OR (rounding_charm_amount >= 0))),\n CONSTRAINT currencies_rounding_increment_positive CHECK ((rounding_increment > 0)),\n CONSTRAINT currencies_rounding_mode_valid CHECK ((rounding_mode = ANY (ARRAY['none'::text, 'nearest'::text, 'up'::text, 'down'::text, 'charm'::text])))\n);\n\nCOMMENT ON TABLE public.currencies IS 'Store currencies available for storefront display and conversion.';\n\nCOMMENT ON COLUMN public.currencies.exchange_rate IS 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';\n\nCOMMENT ON COLUMN public.currencies.rounding_mode IS 'Rounding strategy applied when prices are auto-converted into this currency.';\n\nCOMMENT ON COLUMN public.currencies.rounding_increment IS 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';\n\nCOMMENT ON COLUMN public.currencies.rounding_charm_amount IS 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';\n\nCOMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS 'Whether scheduled FX sync jobs should refresh this currency.';\n\nCOMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS 'When this currency exchange rate was last refreshed or manually set.';\n\nCOMMENT ON COLUMN public.currencies.exchange_rate_source IS 'Human-readable source for the current exchange rate, such as a provider host or manual override.';\n\nCOMMENT ON COLUMN public.currencies.auto_sync_product_prices IS 'Whether storefront product and variant prices in this currency are derived automatically from the store default currency using FX and rounding rules.';\n\nCREATE TABLE IF NOT EXISTS public.email_2fa_challenges (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n user_id uuid NOT NULL,\n token_hash text NOT NULL,\n expires_at timestamp with time zone NOT NULL,\n consumed_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.email_2fa_challenges IS 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';\n\nCREATE TABLE IF NOT EXISTS public.freemius_plans (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n product_id uuid NOT NULL,\n name text NOT NULL,\n title text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.freemius_pricing (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n plan_id uuid NOT NULL,\n api_monthly_price numeric,\n api_annual_price numeric,\n api_lifetime_price numeric,\n override_monthly_price numeric,\n override_annual_price numeric,\n override_lifetime_price numeric,\n license_quota integer,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.inventory_items (\n sku text NOT NULL,\n quantity integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT inventory_items_quantity_check CHECK ((quantity >= 0))\n);\n\nCOMMENT ON TABLE public.inventory_items IS 'Source-of-truth inventory records keyed by sellable SKU.';\n\nCOMMENT ON COLUMN public.inventory_items.sku IS 'Global sellable SKU. Matching products or variants share inventory.';\n\nCOMMENT ON COLUMN public.inventory_items.quantity IS 'Available quantity for this SKU.';\n\nCREATE TABLE IF NOT EXISTS public.languages (\n id bigint NOT NULL,\n code text NOT NULL,\n name text NOT NULL,\n is_default boolean DEFAULT false NOT NULL,\n is_active boolean DEFAULT true,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';\n\nCOMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.languages'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.languages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.languages_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.logos (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n name text NOT NULL,\n media_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.logos IS 'Stores company and brand logos.';\n\nCOMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';\n\nCOMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';\n\nCREATE TABLE IF NOT EXISTS public.media (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n uploader_id uuid,\n file_name text NOT NULL,\n object_key text NOT NULL,\n file_type text,\n size_bytes bigint,\n description text,\n width integer,\n height integer,\n blur_data_url text,\n variants jsonb,\n file_path text,\n folder text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';\n\nCOMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';\n\nCOMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';\n\nCOMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';\n\nCOMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';\n\nCOMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';\n\nCOMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';\n\nCOMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';\n\nCREATE TABLE IF NOT EXISTS public.navigation_items (\n id bigint NOT NULL,\n language_id bigint NOT NULL,\n menu_key public.menu_location NOT NULL,\n label text NOT NULL,\n url text NOT NULL,\n parent_id bigint,\n \"order\" integer DEFAULT 0 NOT NULL,\n page_id bigint,\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';\n\nCOMMENT ON COLUMN public.navigation_items.menu_key IS 'Identifies the menu this item belongs to.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.navigation_items'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.navigation_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.navigation_items_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE SEQUENCE IF NOT EXISTS public.order_invoice_number_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1;\n\nCREATE TABLE IF NOT EXISTS public.order_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n order_id uuid NOT NULL,\n product_id uuid,\n variant_id uuid,\n quantity integer NOT NULL,\n price_at_purchase integer NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.orders (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n user_id uuid,\n status text DEFAULT 'pending'::text NOT NULL,\n total integer NOT NULL,\n stripe_session_id text,\n payment_intent_id text,\n customer_details jsonb,\n provider text DEFAULT 'stripe'::text,\n freemius_product_id text,\n freemius_plan_id text,\n freemius_license_id text,\n freemius_subscription_id text,\n freemius_trial_id text,\n freemius_user_id text,\n freemius_trial_ends_at timestamp with time zone,\n freemius_last_event_type text,\n freemius_last_synced_at timestamp with time zone,\n currency text DEFAULT 'USD'::text NOT NULL,\n subtotal integer,\n shipping_total integer,\n tax_total integer DEFAULT 0 NOT NULL,\n tax_details jsonb,\n exchange_rate_at_purchase numeric(20,10) DEFAULT 1 NOT NULL,\n inventory_deducted_at timestamp with time zone,\n invoice_number text,\n paid_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now(),\n coupon_id uuid,\n coupon_code text,\n discount_total integer DEFAULT 0 NOT NULL,\n discount_details jsonb,\n CONSTRAINT orders_discount_total_check CHECK ((discount_total >= 0)),\n CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK ((exchange_rate_at_purchase > (0)::numeric)),\n CONSTRAINT orders_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\n CONSTRAINT orders_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'trial'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text, 'refunded'::text])))\n);\n\nCOMMENT ON COLUMN public.orders.freemius_license_id IS 'Freemius license ID used to reconcile checkout callbacks and webhooks.';\n\nCOMMENT ON COLUMN public.orders.freemius_subscription_id IS 'Freemius subscription ID when the order is associated with recurring billing.';\n\nCOMMENT ON COLUMN public.orders.freemius_trial_id IS 'Freemius trial ID when checkout starts in trial mode.';\n\nCOMMENT ON COLUMN public.orders.freemius_trial_ends_at IS 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';\n\nCOMMENT ON COLUMN public.orders.freemius_last_event_type IS 'Last Freemius checkout callback or webhook event applied to the order.';\n\nCOMMENT ON COLUMN public.orders.freemius_last_synced_at IS 'Timestamp when Freemius metadata was last reconciled locally.';\n\nCOMMENT ON COLUMN public.orders.currency IS 'ISO currency code used for the order totals.';\n\nCOMMENT ON COLUMN public.orders.subtotal IS 'Subtotal before shipping and tax, in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.orders.shipping_total IS 'Shipping amount before tax, in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.orders.tax_total IS 'Total tax amount collected for the order, in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.orders.tax_details IS 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';\n\nCOMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS 'Exchange rate locked at purchase time relative to the store default currency.';\n\nCOMMENT ON COLUMN public.orders.invoice_number IS 'Stable printable invoice number assigned once when the order first becomes paid.';\n\nCOMMENT ON COLUMN public.orders.paid_at IS 'Timestamp when the order was first marked as paid.';\n\nCOMMENT ON COLUMN public.orders.coupon_code IS 'Coupon code applied to the order at checkout time.';\n\nCOMMENT ON COLUMN public.orders.discount_total IS 'Total discount applied to this order in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.orders.discount_details IS 'Provider-aware coupon quote details captured at checkout.';\n\nCREATE TABLE IF NOT EXISTS public.package_activations (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n license_key text NOT NULL,\n instance_name text NOT NULL,\n package_id text NOT NULL,\n status text DEFAULT 'active'::text NOT NULL,\n meta jsonb DEFAULT '{}'::jsonb,\n last_validated_at timestamp with time zone DEFAULT now(),\n created_at timestamp with time zone DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS public.page_revisions (\n id bigint NOT NULL,\n page_id bigint NOT NULL,\n author_id uuid,\n version integer NOT NULL,\n revision_type public.revision_type NOT NULL,\n content jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';\n\nCOMMENT ON COLUMN public.page_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.page_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.page_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.page_revisions_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.pages (\n id bigint NOT NULL,\n language_id bigint NOT NULL,\n author_id uuid,\n title text NOT NULL,\n slug text NOT NULL,\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\n meta_title text,\n meta_description text,\n version integer DEFAULT 1 NOT NULL,\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n feature_image_id uuid\n);\n\nCOMMENT ON TABLE public.pages IS 'Stores static pages for the website.';\n\nCOMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';\n\nCOMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';\n\nCOMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different language versions of the same conceptual page.';\n\nCOMMENT ON COLUMN public.pages.feature_image_id IS 'ID of the media item to be used as the page feature image.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.pages'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.pages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.pages_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.post_revisions (\n id bigint NOT NULL,\n post_id bigint NOT NULL,\n author_id uuid,\n version integer NOT NULL,\n revision_type public.revision_type NOT NULL,\n content jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';\n\nCOMMENT ON COLUMN public.post_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.post_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.post_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.post_revisions_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.posts (\n id bigint NOT NULL,\n language_id bigint NOT NULL,\n author_id uuid,\n title text NOT NULL,\n slug text NOT NULL,\n label text,\n excerpt text,\n subtitle text,\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\n published_at timestamp with time zone,\n meta_title text,\n meta_description text,\n feature_image_id uuid,\n version integer DEFAULT 1 NOT NULL,\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';\n\nCOMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';\n\nCOMMENT ON COLUMN public.posts.label IS 'Short editorial label rendered as a pill on post hero and article cards.';\n\nCOMMENT ON COLUMN public.posts.excerpt IS 'Short editorial summary used in post metadata rows and post cards.';\n\nCOMMENT ON COLUMN public.posts.subtitle IS 'Longer deck shown under the post title.';\n\nCOMMENT ON COLUMN public.posts.feature_image_id IS 'ID of the media item to be used as the feature image.';\n\nCOMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';\n\nCOMMENT ON COLUMN public.posts.translation_group_id IS 'Groups different language versions of the same conceptual post.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.posts'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.posts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.posts_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.privacy_consent_logs (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n consent_token text NOT NULL,\n categories jsonb DEFAULT '{\"analytics\": false, \"marketing\": false, \"necessary\": true}'::jsonb NOT NULL,\n ip_masked text,\n user_agent text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT privacy_consent_logs_categories_check CHECK ((jsonb_typeof(categories) = 'object'::text))\n);\n\nCOMMENT ON TABLE public.privacy_consent_logs IS 'Immutable audit log of visitor consent decisions for Quebec Law 25 / PIPEDA accountability.';\n\nCOMMENT ON COLUMN public.privacy_consent_logs.consent_token IS 'Opaque token also stored in the nb_consent_preference cookie to correlate a decision with its record.';\n\nCOMMENT ON COLUMN public.privacy_consent_logs.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address for an analytics/marketing consent log.';\n\nCREATE TABLE IF NOT EXISTS public.product_attribute_terms (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n attribute_id uuid NOT NULL,\n value text NOT NULL,\n slug text NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n value_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now(),\n updated_at timestamp with time zone DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS public.product_attributes (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n name text NOT NULL,\n slug text NOT NULL,\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now(),\n updated_at timestamp with time zone DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS public.product_categories (\n product_id uuid NOT NULL,\n category_id uuid NOT NULL\n);\n\nCOMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';\n\nCREATE TABLE IF NOT EXISTS public.product_drafts (\n id bigint NOT NULL,\n product_id uuid NOT NULL,\n author_id uuid,\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\n CONSTRAINT product_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\n CONSTRAINT product_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text))\n);\n\nCOMMENT ON TABLE public.product_drafts IS 'Draft product metadata snapshots used by Draft Mode and front-end visual editing before publishing to live product rows.';\n\nCOMMENT ON COLUMN public.product_drafts.product_id IS 'ID of the product being drafted.';\n\nCOMMENT ON COLUMN public.product_drafts.meta IS 'Draft product metadata snapshot. Front-end visual editing currently mutates visible title, short_description, and description_json fields.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\n ALTER TABLE public.product_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.product_drafts_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nCREATE TABLE IF NOT EXISTS public.product_freemius_sale_coupons (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n product_id uuid NOT NULL,\n freemius_product_id text NOT NULL,\n freemius_plan_id text,\n freemius_coupon_id text,\n freemius_coupon_code text NOT NULL,\n discount_percent integer,\n starts_at timestamp with time zone,\n ends_at timestamp with time zone,\n is_active boolean DEFAULT false NOT NULL,\n sync_status text DEFAULT 'pending'::text NOT NULL,\n sync_error text,\n remote_payload jsonb,\n last_synced_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT product_freemius_sale_coupons_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\n CONSTRAINT product_freemius_sale_coupons_discount_valid CHECK (((discount_percent IS NULL) OR ((discount_percent > 0) AND (discount_percent <= 100)))),\n CONSTRAINT product_freemius_sale_coupons_fm_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\n CONSTRAINT product_freemius_sale_coupons_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text]))),\n CONSTRAINT product_freemius_sale_coupons_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at)))\n);\n\nCOMMENT ON TABLE public.product_freemius_sale_coupons IS 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';\n\nCREATE TABLE IF NOT EXISTS public.product_media (\n product_id uuid NOT NULL,\n media_id uuid NOT NULL,\n sort_order integer DEFAULT 0\n);\n\nCREATE TABLE IF NOT EXISTS public.product_variants (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n product_id uuid NOT NULL,\n sku text NOT NULL,\n price_adjustment integer DEFAULT 0 NOT NULL,\n price integer DEFAULT 0 NOT NULL,\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\n sale_price integer,\n sale_prices jsonb,\n stock_quantity integer DEFAULT 0 NOT NULL,\n upc text,\n main_media_id uuid,\n created_at timestamp with time zone DEFAULT now(),\n updated_at timestamp with time zone DEFAULT now(),\n sale_start_at timestamp with time zone,\n sale_end_at timestamp with time zone,\n scheduled_price integer,\n scheduled_prices jsonb,\n scheduled_price_at timestamp with time zone,\n CONSTRAINT product_variants_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\n CONSTRAINT product_variants_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\n CONSTRAINT product_variants_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at)))\n);\n\nCOMMENT ON COLUMN public.product_variants.prices IS 'Variant regular prices by ISO 4217 code in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.product_variants.sale_prices IS 'Variant sale prices by ISO 4217 code in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.product_variants.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC) for this variant. NULL means no lower bound.';\n\nCOMMENT ON COLUMN public.product_variants.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC) for this variant. NULL means no upper bound.';\n\nCOMMENT ON COLUMN public.product_variants.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\n\nCOMMENT ON COLUMN public.product_variants.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\n\nCOMMENT ON COLUMN public.product_variants.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\n\nCREATE TABLE IF NOT EXISTS public.products (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n language_id bigint NOT NULL,\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\n sku text NOT NULL,\n title text NOT NULL,\n slug text NOT NULL,\n product_type text NOT NULL,\n payment_provider text NOT NULL,\n price integer NOT NULL,\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\n sale_price integer,\n sale_prices jsonb,\n stock integer DEFAULT 0,\n status text DEFAULT 'draft'::text NOT NULL,\n meta_title text,\n meta_description text,\n short_description text,\n description_json jsonb,\n metadata jsonb,\n freemius_plan_id text,\n freemius_product_id text,\n trial_period_days integer DEFAULT 0 NOT NULL,\n trial_requires_payment_method boolean DEFAULT false NOT NULL,\n upc text,\n is_taxable boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now(),\n updated_at timestamp with time zone DEFAULT now(),\n sale_start_at timestamp with time zone,\n sale_end_at timestamp with time zone,\n scheduled_price integer,\n scheduled_prices jsonb,\n scheduled_price_at timestamp with time zone,\n average_rating numeric(3,2) DEFAULT 0.00 NOT NULL,\n total_reviews integer DEFAULT 0 NOT NULL,\n CONSTRAINT products_payment_provider_check CHECK ((payment_provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\n CONSTRAINT products_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\n CONSTRAINT products_product_type_check CHECK ((product_type = ANY (ARRAY['physical'::text, 'digital'::text]))),\n CONSTRAINT products_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\n CONSTRAINT products_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at))),\n CONSTRAINT products_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text]))),\n CONSTRAINT products_trial_period_days_check CHECK ((trial_period_days >= 0)),\n CONSTRAINT products_type_provider_consistency_check CHECK ((((product_type = 'physical'::text) AND (payment_provider = 'stripe'::text)) OR ((product_type = 'digital'::text) AND (payment_provider = 'freemius'::text))))\n);\n\nCOMMENT ON COLUMN public.products.prices IS 'Regular prices by ISO 4217 code in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.products.sale_prices IS 'Sale prices by ISO 4217 code in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.products.is_taxable IS 'When true, this product participates in Stripe tax calculation.';\n\nCOMMENT ON COLUMN public.products.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC). NULL means no lower bound.';\n\nCOMMENT ON COLUMN public.products.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC). NULL means no upper bound. Both NULL = always-on sale.';\n\nCOMMENT ON COLUMN public.products.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\n\nCOMMENT ON COLUMN public.products.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\n\nCOMMENT ON COLUMN public.products.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\n\nCOMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rating of approved reviews.';\n\nCOMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';\n\nCREATE TABLE IF NOT EXISTS public.profiles (\n id uuid NOT NULL,\n updated_at timestamp with time zone,\n full_name text,\n avatar_url text,\n website text,\n github_username text,\n phone text,\n role public.user_role DEFAULT 'USER'::public.user_role NOT NULL\n);\n\nCOMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';\n\nCOMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';\n\nCOMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';\n\nCREATE TABLE IF NOT EXISTS public.shipping_zone_locations (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n zone_id uuid NOT NULL,\n country_code text NOT NULL,\n state_code text,\n postal_code text,\n created_at timestamp with time zone DEFAULT now()\n);\n\nCOMMENT ON COLUMN public.shipping_zone_locations.country_code IS 'ISO 3166-1 alpha-2 country code.';\n\nCOMMENT ON COLUMN public.shipping_zone_locations.state_code IS 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';\n\nCOMMENT ON COLUMN public.shipping_zone_locations.postal_code IS 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';\n\nCREATE TABLE IF NOT EXISTS public.shipping_zone_methods (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n zone_id uuid NOT NULL,\n method_type text NOT NULL,\n cost_amount integer DEFAULT 0 NOT NULL,\n cost_currency text DEFAULT 'USD'::text NOT NULL,\n min_order_amount integer DEFAULT 0 NOT NULL,\n name text NOT NULL,\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\n currency_pricing_mode text DEFAULT 'auto'::text NOT NULL,\n cost_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\n min_order_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now(),\n updated_at timestamp with time zone DEFAULT now(),\n CONSTRAINT shipping_zone_methods_cost_amounts_include_source CHECK ((cost_amounts ? upper(cost_currency))),\n CONSTRAINT shipping_zone_methods_cost_amounts_valid CHECK (public.is_valid_currency_amount_map(cost_amounts)),\n CONSTRAINT shipping_zone_methods_cost_currency_format CHECK ((cost_currency ~ '^[A-Z]{3}$'::text)),\n CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid CHECK ((currency_pricing_mode = ANY (ARRAY['auto'::text, 'manual'::text]))),\n CONSTRAINT shipping_zone_methods_method_type_check CHECK ((method_type = ANY (ARRAY['flat_rate'::text, 'free_shipping'::text]))),\n CONSTRAINT shipping_zone_methods_min_order_amounts_include_source CHECK ((min_order_amounts ? upper(cost_currency))),\n CONSTRAINT shipping_zone_methods_min_order_amounts_valid CHECK (public.is_valid_currency_amount_map(min_order_amounts))\n);\n\nCOMMENT ON COLUMN public.shipping_zone_methods.name_translations IS 'Localized shipping method labels keyed by language code. Example: {\"fr\": \"Livraison standard\"}.';\n\nCOMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';\n\nCOMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS 'Shipping costs by ISO 4217 code in the smallest currency unit.';\n\nCOMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';\n\nCREATE TABLE IF NOT EXISTS public.shipping_zones (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n name text NOT NULL,\n priority_order integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now(),\n updated_at timestamp with time zone DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS public.site_settings (\n key text NOT NULL,\n value jsonb\n);\n\nCOMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys (Cortex AI BYOK, Bot Protection Secret, Email secret, Payment secret) hold encrypted envelopes and are restricted to ADMIN via row-level policies.';\n\nCREATE TABLE IF NOT EXISTS public.system_alerts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n alert_type text NOT NULL,\n title text NOT NULL,\n message text NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n is_resolved boolean DEFAULT false NOT NULL,\n resolved_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT system_alerts_alert_type_check CHECK ((alert_type = ANY (ARRAY['merge_conflict'::text, 'runtime_update_available'::text])))\n);\n\nCOMMENT ON TABLE public.system_alerts IS 'System notifications for the automated upstream-update architecture (merge conflicts, runtime updates available). Written by service-role; read by ADMINs.';\n\nCOMMENT ON COLUMN public.system_alerts.alert_type IS 'One of: merge_conflict, runtime_update_available.';\n\nCOMMENT ON COLUMN public.system_alerts.metadata IS 'Structured deep-link context for the dashboard banner CTA (repo/branch/action_url or latest_version/download_url).';\n\nCOMMENT ON COLUMN public.system_alerts.is_resolved IS 'When true the alert is hidden from the dashboard banner.';\n\nCREATE TABLE IF NOT EXISTS public.system_configuration (\n id integer DEFAULT 1 NOT NULL,\n auto_accept_signups boolean DEFAULT false NOT NULL,\n settings jsonb DEFAULT '{}'::jsonb NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT system_configuration_singleton CHECK ((id = 1))\n);\n\nCOMMENT ON TABLE public.system_configuration IS 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';\n\nCREATE TABLE IF NOT EXISTS public.tax_rates (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n country_code text NOT NULL,\n state_code text,\n tax_name text NOT NULL,\n tax_rate numeric(7,4) NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT tax_rates_tax_name_check CHECK ((char_length(btrim(tax_name)) > 0)),\n CONSTRAINT tax_rates_tax_rate_check CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))\n);\n\nCOMMENT ON TABLE public.tax_rates IS 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';\n\nCOMMENT ON COLUMN public.tax_rates.country_code IS 'ISO 3166-1 alpha-2 country code.';\n\nCOMMENT ON COLUMN public.tax_rates.state_code IS 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';\n\nCOMMENT ON COLUMN public.tax_rates.tax_name IS 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';\n\nCOMMENT ON COLUMN public.tax_rates.tax_rate IS 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';\n\nCREATE TABLE IF NOT EXISTS public.translations (\n key text NOT NULL,\n translations jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON COLUMN public.translations.key IS 'A unique, slugified identifier (e.g., \"sign_in_button_text\").';\n\nCOMMENT ON COLUMN public.translations.translations IS 'Stores translations as key-value pairs (e.g., {\"en\": \"Sign In\", \"fr\": \"S''inscrire\"}).';\n\nCREATE TABLE IF NOT EXISTS public.ucp_cart_sessions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n status text DEFAULT 'active'::text NOT NULL,\n currency text DEFAULT 'USD'::text NOT NULL,\n locale text,\n buyer_identity jsonb DEFAULT '{}'::jsonb NOT NULL,\n context jsonb DEFAULT '{}'::jsonb NOT NULL,\n signals jsonb DEFAULT '{}'::jsonb NOT NULL,\n attribution jsonb DEFAULT '{}'::jsonb NOT NULL,\n line_items jsonb DEFAULT '[]'::jsonb NOT NULL,\n totals jsonb DEFAULT '[]'::jsonb NOT NULL,\n checkout_url text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,\n CONSTRAINT ucp_cart_sessions_attribution_object CHECK ((jsonb_typeof(attribution) = 'object'::text)),\n CONSTRAINT ucp_cart_sessions_buyer_identity_object CHECK ((jsonb_typeof(buyer_identity) = 'object'::text)),\n CONSTRAINT ucp_cart_sessions_context_object CHECK ((jsonb_typeof(context) = 'object'::text)),\n CONSTRAINT ucp_cart_sessions_line_items_array CHECK ((jsonb_typeof(line_items) = 'array'::text)),\n CONSTRAINT ucp_cart_sessions_metadata_object CHECK ((jsonb_typeof(metadata) = 'object'::text)),\n CONSTRAINT ucp_cart_sessions_signals_object CHECK ((jsonb_typeof(signals) = 'object'::text)),\n CONSTRAINT ucp_cart_sessions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'cancelled'::text, 'completed'::text]))),\n CONSTRAINT ucp_cart_sessions_totals_array CHECK ((jsonb_typeof(totals) = 'array'::text))\n);\n\nCREATE TABLE IF NOT EXISTS public.user_addresses (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n user_id uuid NOT NULL,\n address_type text NOT NULL,\n is_default boolean DEFAULT false NOT NULL,\n recipient_name text,\n company_name text,\n line1 text,\n line2 text,\n city text,\n state text,\n postal_code text,\n country_code text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT user_addresses_address_type_check CHECK ((address_type = ANY (ARRAY['billing'::text, 'shipping'::text])))\n);\n\nCOMMENT ON COLUMN public.user_addresses.company_name IS 'Optional company or organization name for the address.';\n\nCREATE TABLE IF NOT EXISTS public.user_security_settings (\n user_id uuid NOT NULL,\n mfa_enabled boolean DEFAULT false NOT NULL,\n mfa_type text,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT user_security_settings_mfa_type_check CHECK ((mfa_type = ANY (ARRAY['totp'::text, 'email'::text])))\n);\n\nCOMMENT ON TABLE public.user_security_settings IS 'Per-user multi-factor configuration. mfa_type is NULL until a factor is enrolled.';\n\nCREATE TABLE IF NOT EXISTS public.user_trusted_devices (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n user_id uuid NOT NULL,\n device_hash text NOT NULL,\n browser_metadata text,\n expires_at timestamp with time zone NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.user_trusted_devices IS 'SHA-256 hashes of trusted-device tokens. A 2FA bypass is only honoured when a non-expired row matches the cookie token, so deleting a row instantly revokes trust.';\n\nCOMMENT ON COLUMN public.user_trusted_devices.device_hash IS 'SHA-256 of the raw token held in the nb_trusted_device cookie. The raw token is never stored.';\n\nCREATE TABLE IF NOT EXISTS public.variant_attribute_mapping (\n variant_id uuid NOT NULL,\n attribute_term_id uuid NOT NULL\n);\n"
21
21
  },
22
22
  {
23
23
  "version": "00000000000001",
24
24
  "name": "00000000000001_baseline_constraints_and_indexes.sql",
25
- "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 01 · constraints (PK / unique / check / FK) + indexes\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_pkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_pkey' AND conrelid = 'public.categories'::regclass) THEN\r\n ALTER TABLE ONLY public.categories\r\n ADD CONSTRAINT categories_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_slug_key' AND conrelid = 'public.categories'::regclass) THEN\r\n ALTER TABLE ONLY public.categories\r\n ADD CONSTRAINT categories_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_pkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_parent_unique' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_parent_unique UNIQUE (parent_type, parent_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_pkey' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_pkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\r\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\r\n ADD CONSTRAINT cortex_ai_db_mutation_audit_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_pkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_pkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_pkey PRIMARY KEY (coupon_id, product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_pkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupons_pkey' AND conrelid = 'public.coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.coupons\r\n ADD CONSTRAINT coupons_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_code_key' AND conrelid = 'public.currencies'::regclass) THEN\r\n ALTER TABLE ONLY public.currencies\r\n ADD CONSTRAINT currencies_code_key UNIQUE (code);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_pkey' AND conrelid = 'public.currencies'::regclass) THEN\r\n ALTER TABLE ONLY public.currencies\r\n ADD CONSTRAINT currencies_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_pkey' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\r\n ALTER TABLE ONLY public.custom_block_definitions\r\n ADD CONSTRAINT custom_block_definitions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_slug_key' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\r\n ALTER TABLE ONLY public.custom_block_definitions\r\n ADD CONSTRAINT custom_block_definitions_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_pkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\r\n ALTER TABLE ONLY public.email_2fa_challenges\r\n ADD CONSTRAINT email_2fa_challenges_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_pkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_plans\r\n ADD CONSTRAINT freemius_plans_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_pkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_pricing\r\n ADD CONSTRAINT freemius_pricing_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'inventory_items_pkey' AND conrelid = 'public.inventory_items'::regclass) THEN\r\n ALTER TABLE ONLY public.inventory_items\r\n ADD CONSTRAINT inventory_items_pkey PRIMARY KEY (sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_code_key' AND conrelid = 'public.languages'::regclass) THEN\r\n ALTER TABLE ONLY public.languages\r\n ADD CONSTRAINT languages_code_key UNIQUE (code);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_pkey' AND conrelid = 'public.languages'::regclass) THEN\r\n ALTER TABLE ONLY public.languages\r\n ADD CONSTRAINT languages_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_pkey' AND conrelid = 'public.logos'::regclass) THEN\r\n ALTER TABLE ONLY public.logos\r\n ADD CONSTRAINT logos_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_object_key_key' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_object_key_key UNIQUE (object_key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_pkey' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_pkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_pkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_pkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_stripe_session_id_key' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_stripe_session_id_key UNIQUE (stripe_session_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_license_key_package_id_key' AND conrelid = 'public.package_activations'::regclass) THEN\r\n ALTER TABLE ONLY public.package_activations\r\n ADD CONSTRAINT package_activations_license_key_package_id_key UNIQUE (license_key, package_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_pkey' AND conrelid = 'public.package_activations'::regclass) THEN\r\n ALTER TABLE ONLY public.package_activations\r\n ADD CONSTRAINT package_activations_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_version_key' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_page_version_key UNIQUE (page_id, version);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_pkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_slug_key' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_pkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_pkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_version_key' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_post_version_key UNIQUE (post_id, version);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_slug_key' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_pkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_consent_token_key' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\r\n ALTER TABLE ONLY public.privacy_consent_logs\r\n ADD CONSTRAINT privacy_consent_logs_consent_token_key UNIQUE (consent_token);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_pkey' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\r\n ALTER TABLE ONLY public.privacy_consent_logs\r\n ADD CONSTRAINT privacy_consent_logs_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_slug_key' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_attribute_id_slug_key UNIQUE (attribute_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_pkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_pkey' AND conrelid = 'public.product_attributes'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attributes\r\n ADD CONSTRAINT product_attributes_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_slug_key' AND conrelid = 'public.product_attributes'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attributes\r\n ADD CONSTRAINT product_attributes_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_pkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_pkey PRIMARY KEY (product_id, category_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_pkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_unique' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_product_unique UNIQUE (product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_pkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_unique' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_product_unique UNIQUE (product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_pkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_pkey PRIMARY KEY (product_id, media_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_pkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_sku_key' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_product_id_sku_key UNIQUE (product_id, sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_sku_key' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_sku_key UNIQUE (language_id, sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_slug_key' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_pkey' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_pkey' AND conrelid = 'public.profiles'::regclass) THEN\r\n ALTER TABLE ONLY public.profiles\r\n ADD CONSTRAINT profiles_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_pkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_locations\r\n ADD CONSTRAINT shipping_zone_locations_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_pkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_methods\r\n ADD CONSTRAINT shipping_zone_methods_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zones_pkey' AND conrelid = 'public.shipping_zones'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zones\r\n ADD CONSTRAINT shipping_zones_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_settings_pkey' AND conrelid = 'public.site_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.site_settings\r\n ADD CONSTRAINT site_settings_pkey PRIMARY KEY (key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_alerts_pkey' AND conrelid = 'public.system_alerts'::regclass) THEN\r\n ALTER TABLE ONLY public.system_alerts\r\n ADD CONSTRAINT system_alerts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_configuration_pkey' AND conrelid = 'public.system_configuration'::regclass) THEN\r\n ALTER TABLE ONLY public.system_configuration\r\n ADD CONSTRAINT system_configuration_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tax_rates_pkey' AND conrelid = 'public.tax_rates'::regclass) THEN\r\n ALTER TABLE ONLY public.tax_rates\r\n ADD CONSTRAINT tax_rates_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'translations_pkey' AND conrelid = 'public.translations'::regclass) THEN\r\n ALTER TABLE ONLY public.translations\r\n ADD CONSTRAINT translations_pkey PRIMARY KEY (key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ucp_cart_sessions_pkey' AND conrelid = 'public.ucp_cart_sessions'::regclass) THEN\r\n ALTER TABLE ONLY public.ucp_cart_sessions\r\n ADD CONSTRAINT ucp_cart_sessions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_pkey' AND conrelid = 'public.user_addresses'::regclass) THEN\r\n ALTER TABLE ONLY public.user_addresses\r\n ADD CONSTRAINT user_addresses_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_pkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.user_security_settings\r\n ADD CONSTRAINT user_security_settings_pkey PRIMARY KEY (user_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_device_hash_key' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_device_hash_key UNIQUE (device_hash);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_pkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_pkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_pkey PRIMARY KEY (variant_id, attribute_term_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_author_id_idx ON public.content_drafts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_parent_idx ON public.content_drafts USING btree (parent_type, parent_id);\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_updated_at_idx ON public.content_drafts USING btree (updated_at DESC);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_freemius_mappings_coupon_product_unique ON public.coupon_freemius_mappings USING btree (coupon_id, freemius_product_id);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_redemptions_order_unique ON public.coupon_redemptions USING btree (order_id) WHERE (order_id IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupons_code_unique ON public.coupons USING btree (upper(code));\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS ensure_single_default_language_idx ON public.languages USING btree (is_default) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_language_id ON public.blocks USING btree (language_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_page_id ON public.blocks USING btree (page_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_post_id ON public.blocks USING btree (post_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_freemius_product_id ON public.coupon_freemius_mappings USING btree (freemius_product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_product_id ON public.coupon_freemius_mappings USING btree (product_id) WHERE (product_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_products_product_id ON public.coupon_products USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_coupon_id ON public.coupon_redemptions USING btree (coupon_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_user_id ON public.coupon_redemptions USING btree (user_id) WHERE (user_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupons_active_dates ON public.coupons USING btree (is_active, starts_at, ends_at);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_single_default ON public.currencies USING btree (is_default) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_custom_block_definitions_is_original ON public.custom_block_definitions USING btree (is_original);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_expires_at ON public.email_2fa_challenges USING btree (expires_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_user_id ON public.email_2fa_challenges USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_freemius_plans_product_id ON public.freemius_plans USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_freemius_pricing_plan_id ON public.freemius_pricing USING btree (plan_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_inventory_items_updated_at ON public.inventory_items USING btree (updated_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_logos_media_id ON public.logos USING btree (media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_media_uploader_id ON public.media USING btree (uploader_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_language_id ON public.navigation_items USING btree (language_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_menu_lang_order ON public.navigation_items USING btree (menu_key, language_id, \"order\");\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_page_id ON public.navigation_items USING btree (page_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_parent_id ON public.navigation_items USING btree (parent_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_order_id ON public.order_items USING btree (order_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_product_id ON public.order_items USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_variant_id ON public.order_items USING btree (variant_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_coupon_id ON public.orders USING btree (coupon_id) WHERE (coupon_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_license_id ON public.orders USING btree (freemius_license_id) WHERE (freemius_license_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_subscription_id ON public.orders USING btree (freemius_subscription_id) WHERE (freemius_subscription_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_trial_id ON public.orders USING btree (freemius_trial_id) WHERE (freemius_trial_id IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_orders_invoice_number_unique ON public.orders USING btree (invoice_number) WHERE (invoice_number IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_user_id ON public.orders USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_package_activations_license_key ON public.package_activations USING btree (license_key);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_package_activations_package_id ON public.package_activations USING btree (package_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_page_revisions_author_id ON public.page_revisions USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_page_revisions_page_id_version ON public.page_revisions USING btree (page_id, version);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_author_id ON public.pages USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_feature_image_id ON public.pages USING btree (feature_image_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_translation_group_id ON public.pages USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_post_revisions_author_id ON public.post_revisions USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_post_revisions_post_id_version ON public.post_revisions USING btree (post_id, version);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_author_id ON public.posts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_feature_image_id ON public.posts USING btree (feature_image_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_translation_group_id ON public.posts USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_privacy_consent_logs_created_at ON public.privacy_consent_logs USING btree (created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_attribute_terms_attribute_id ON public.product_attribute_terms USING btree (attribute_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_categories_category_id ON public.product_categories USING btree (category_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_categories_product_id ON public.product_categories USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_freemius_sale_coupons_freemius_product_id ON public.product_freemius_sale_coupons USING btree (freemius_product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_media_media_id ON public.product_media USING btree (media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_media_product_id ON public.product_media USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_main_media_id ON public.product_variants USING btree (main_media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_prices_gin ON public.product_variants USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_product_id ON public.product_variants USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_prices_gin ON public.products USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_slug ON public.products USING btree (slug);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_translation_group_id ON public.products USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_country_state_postal ON public.shipping_zone_locations USING btree (country_code, state_code, postal_code);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_zone_id ON public.shipping_zone_locations USING btree (zone_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_name_translations ON public.shipping_zone_methods USING gin (name_translations);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_zone_id ON public.shipping_zone_methods USING btree (zone_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_system_alerts_unresolved ON public.system_alerts USING btree (is_resolved, created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_tax_rates_country_state ON public.tax_rates USING btree (country_code, state_code);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_created_at ON public.ucp_cart_sessions USING btree (created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_status_expires_at ON public.ucp_cart_sessions USING btree (status, expires_at);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_user_addresses_one_default_per_type ON public.user_addresses USING btree (user_id, address_type) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_addresses_type ON public.user_addresses USING btree (address_type);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_addresses_user_id ON public.user_addresses USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_expires_at ON public.user_trusted_devices USING btree (expires_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_user_id ON public.user_trusted_devices USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_variant_attribute_mapping_attribute_term_id ON public.variant_attribute_mapping USING btree (attribute_term_id);\r\n\r\nCREATE INDEX IF NOT EXISTS media_folder_idx ON public.media USING btree (folder);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_author_id_idx ON public.product_drafts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_product_id_idx ON public.product_drafts USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_updated_at_idx ON public.product_drafts USING btree (updated_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS product_variants_sale_window_idx ON public.product_variants USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\r\n\r\nCREATE INDEX IF NOT EXISTS product_variants_scheduled_price_idx ON public.product_variants USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS products_sale_window_idx ON public.products USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\r\n\r\nCREATE INDEX IF NOT EXISTS products_scheduled_price_idx ON public.products USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS tax_rates_country_state_name_key ON public.tax_rates USING btree (country_code, COALESCE(state_code, ''::text), lower(tax_name));\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_language_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_page_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_post_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_product_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_post_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_product_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_user_id_profiles_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_user_id_profiles_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_author_id_fkey' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_actor_user_id_fkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\r\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\r\n ADD CONSTRAINT cortex_ai_db_mutation_audit_actor_user_id_fkey FOREIGN KEY (actor_user_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_coupon_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_product_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_coupon_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_product_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_coupon_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_order_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_user_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_user_id_fkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\r\n ALTER TABLE ONLY public.email_2fa_challenges\r\n ADD CONSTRAINT email_2fa_challenges_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_product_id_fkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_plans\r\n ADD CONSTRAINT freemius_plans_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_plan_id_fkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_pricing\r\n ADD CONSTRAINT freemius_pricing_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public.freemius_plans(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_media_id_fkey' AND conrelid = 'public.logos'::regclass) THEN\r\n ALTER TABLE ONLY public.logos\r\n ADD CONSTRAINT logos_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_uploader_id_fkey' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_uploader_id_fkey FOREIGN KEY (uploader_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_language_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_page_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_parent_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.navigation_items(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_order_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_product_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_variant_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_coupon_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_user_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_author_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_author_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_feature_image_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_author_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_author_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_feature_image_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_fkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_attribute_id_fkey FOREIGN KEY (attribute_id) REFERENCES public.product_attributes(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_category_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.categories(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_product_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_author_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_id_fkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_media_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_product_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_main_media_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_main_media_id_fkey FOREIGN KEY (main_media_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_fkey' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_id_fkey' AND conrelid = 'public.profiles'::regclass) THEN\r\n ALTER TABLE ONLY public.profiles\r\n ADD CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_zone_id_fkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_locations\r\n ADD CONSTRAINT shipping_zone_locations_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_zone_id_fkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_methods\r\n ADD CONSTRAINT shipping_zone_methods_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_user_id_fkey' AND conrelid = 'public.user_addresses'::regclass) THEN\r\n ALTER TABLE ONLY public.user_addresses\r\n ADD CONSTRAINT user_addresses_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_user_id_fkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.user_security_settings\r\n ADD CONSTRAINT user_security_settings_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_user_id_fkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_attribute_term_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_attribute_term_id_fkey FOREIGN KEY (attribute_term_id) REFERENCES public.product_attribute_terms(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_variant_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n"
25
+ "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\n-- 01 · constraints (PK / unique / check / FK) + indexes\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_pkey' AND conrelid = 'public.blocks'::regclass) THEN\n ALTER TABLE ONLY public.blocks\n ADD CONSTRAINT blocks_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_pkey' AND conrelid = 'public.categories'::regclass) THEN\n ALTER TABLE ONLY public.categories\n ADD CONSTRAINT categories_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_slug_key' AND conrelid = 'public.categories'::regclass) THEN\n ALTER TABLE ONLY public.categories\n ADD CONSTRAINT categories_slug_key UNIQUE (slug);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_pkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE ONLY public.cms_interactions\n ADD CONSTRAINT cms_interactions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_parent_unique' AND conrelid = 'public.content_drafts'::regclass) THEN\n ALTER TABLE ONLY public.content_drafts\n ADD CONSTRAINT content_drafts_parent_unique UNIQUE (parent_type, parent_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_pkey' AND conrelid = 'public.content_drafts'::regclass) THEN\n ALTER TABLE ONLY public.content_drafts\n ADD CONSTRAINT content_drafts_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_pkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\n ADD CONSTRAINT cortex_ai_db_mutation_audit_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_pkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\n ALTER TABLE ONLY public.coupon_freemius_mappings\n ADD CONSTRAINT coupon_freemius_mappings_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_pkey' AND conrelid = 'public.coupon_products'::regclass) THEN\n ALTER TABLE ONLY public.coupon_products\n ADD CONSTRAINT coupon_products_pkey PRIMARY KEY (coupon_id, product_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_pkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\n ALTER TABLE ONLY public.coupon_redemptions\n ADD CONSTRAINT coupon_redemptions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupons_pkey' AND conrelid = 'public.coupons'::regclass) THEN\n ALTER TABLE ONLY public.coupons\n ADD CONSTRAINT coupons_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_code_key' AND conrelid = 'public.currencies'::regclass) THEN\n ALTER TABLE ONLY public.currencies\n ADD CONSTRAINT currencies_code_key UNIQUE (code);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_pkey' AND conrelid = 'public.currencies'::regclass) THEN\n ALTER TABLE ONLY public.currencies\n ADD CONSTRAINT currencies_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_pkey' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\n ALTER TABLE ONLY public.custom_block_definitions\n ADD CONSTRAINT custom_block_definitions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_slug_key' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\n ALTER TABLE ONLY public.custom_block_definitions\n ADD CONSTRAINT custom_block_definitions_slug_key UNIQUE (slug);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_pkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\n ALTER TABLE ONLY public.email_2fa_challenges\n ADD CONSTRAINT email_2fa_challenges_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_pkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\n ALTER TABLE ONLY public.freemius_plans\n ADD CONSTRAINT freemius_plans_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_pkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\n ALTER TABLE ONLY public.freemius_pricing\n ADD CONSTRAINT freemius_pricing_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'inventory_items_pkey' AND conrelid = 'public.inventory_items'::regclass) THEN\n ALTER TABLE ONLY public.inventory_items\n ADD CONSTRAINT inventory_items_pkey PRIMARY KEY (sku);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_code_key' AND conrelid = 'public.languages'::regclass) THEN\n ALTER TABLE ONLY public.languages\n ADD CONSTRAINT languages_code_key UNIQUE (code);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_pkey' AND conrelid = 'public.languages'::regclass) THEN\n ALTER TABLE ONLY public.languages\n ADD CONSTRAINT languages_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_pkey' AND conrelid = 'public.logos'::regclass) THEN\n ALTER TABLE ONLY public.logos\n ADD CONSTRAINT logos_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_object_key_key' AND conrelid = 'public.media'::regclass) THEN\n ALTER TABLE ONLY public.media\n ADD CONSTRAINT media_object_key_key UNIQUE (object_key);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_pkey' AND conrelid = 'public.media'::regclass) THEN\n ALTER TABLE ONLY public.media\n ADD CONSTRAINT media_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_pkey' AND conrelid = 'public.navigation_items'::regclass) THEN\n ALTER TABLE ONLY public.navigation_items\n ADD CONSTRAINT navigation_items_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_pkey' AND conrelid = 'public.order_items'::regclass) THEN\n ALTER TABLE ONLY public.order_items\n ADD CONSTRAINT order_items_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_pkey' AND conrelid = 'public.orders'::regclass) THEN\n ALTER TABLE ONLY public.orders\n ADD CONSTRAINT orders_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_stripe_session_id_key' AND conrelid = 'public.orders'::regclass) THEN\n ALTER TABLE ONLY public.orders\n ADD CONSTRAINT orders_stripe_session_id_key UNIQUE (stripe_session_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_license_key_package_id_key' AND conrelid = 'public.package_activations'::regclass) THEN\n ALTER TABLE ONLY public.package_activations\n ADD CONSTRAINT package_activations_license_key_package_id_key UNIQUE (license_key, package_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_pkey' AND conrelid = 'public.package_activations'::regclass) THEN\n ALTER TABLE ONLY public.package_activations\n ADD CONSTRAINT package_activations_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_version_key' AND conrelid = 'public.page_revisions'::regclass) THEN\n ALTER TABLE ONLY public.page_revisions\n ADD CONSTRAINT page_revisions_page_version_key UNIQUE (page_id, version);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_pkey' AND conrelid = 'public.page_revisions'::regclass) THEN\n ALTER TABLE ONLY public.page_revisions\n ADD CONSTRAINT page_revisions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_slug_key' AND conrelid = 'public.pages'::regclass) THEN\n ALTER TABLE ONLY public.pages\n ADD CONSTRAINT pages_language_id_slug_key UNIQUE (language_id, slug);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_pkey' AND conrelid = 'public.pages'::regclass) THEN\n ALTER TABLE ONLY public.pages\n ADD CONSTRAINT pages_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_pkey' AND conrelid = 'public.post_revisions'::regclass) THEN\n ALTER TABLE ONLY public.post_revisions\n ADD CONSTRAINT post_revisions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_version_key' AND conrelid = 'public.post_revisions'::regclass) THEN\n ALTER TABLE ONLY public.post_revisions\n ADD CONSTRAINT post_revisions_post_version_key UNIQUE (post_id, version);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_slug_key' AND conrelid = 'public.posts'::regclass) THEN\n ALTER TABLE ONLY public.posts\n ADD CONSTRAINT posts_language_id_slug_key UNIQUE (language_id, slug);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_pkey' AND conrelid = 'public.posts'::regclass) THEN\n ALTER TABLE ONLY public.posts\n ADD CONSTRAINT posts_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_consent_token_key' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\n ALTER TABLE ONLY public.privacy_consent_logs\n ADD CONSTRAINT privacy_consent_logs_consent_token_key UNIQUE (consent_token);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_pkey' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\n ALTER TABLE ONLY public.privacy_consent_logs\n ADD CONSTRAINT privacy_consent_logs_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_slug_key' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\n ALTER TABLE ONLY public.product_attribute_terms\n ADD CONSTRAINT product_attribute_terms_attribute_id_slug_key UNIQUE (attribute_id, slug);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_pkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\n ALTER TABLE ONLY public.product_attribute_terms\n ADD CONSTRAINT product_attribute_terms_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_pkey' AND conrelid = 'public.product_attributes'::regclass) THEN\n ALTER TABLE ONLY public.product_attributes\n ADD CONSTRAINT product_attributes_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_slug_key' AND conrelid = 'public.product_attributes'::regclass) THEN\n ALTER TABLE ONLY public.product_attributes\n ADD CONSTRAINT product_attributes_slug_key UNIQUE (slug);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_pkey' AND conrelid = 'public.product_categories'::regclass) THEN\n ALTER TABLE ONLY public.product_categories\n ADD CONSTRAINT product_categories_pkey PRIMARY KEY (product_id, category_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_pkey' AND conrelid = 'public.product_drafts'::regclass) THEN\n ALTER TABLE ONLY public.product_drafts\n ADD CONSTRAINT product_drafts_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_unique' AND conrelid = 'public.product_drafts'::regclass) THEN\n ALTER TABLE ONLY public.product_drafts\n ADD CONSTRAINT product_drafts_product_unique UNIQUE (product_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_pkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\n ALTER TABLE ONLY public.product_freemius_sale_coupons\n ADD CONSTRAINT product_freemius_sale_coupons_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_unique' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\n ALTER TABLE ONLY public.product_freemius_sale_coupons\n ADD CONSTRAINT product_freemius_sale_coupons_product_unique UNIQUE (product_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_pkey' AND conrelid = 'public.product_media'::regclass) THEN\n ALTER TABLE ONLY public.product_media\n ADD CONSTRAINT product_media_pkey PRIMARY KEY (product_id, media_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_pkey' AND conrelid = 'public.product_variants'::regclass) THEN\n ALTER TABLE ONLY public.product_variants\n ADD CONSTRAINT product_variants_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_sku_key' AND conrelid = 'public.product_variants'::regclass) THEN\n ALTER TABLE ONLY public.product_variants\n ADD CONSTRAINT product_variants_product_id_sku_key UNIQUE (product_id, sku);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_sku_key' AND conrelid = 'public.products'::regclass) THEN\n ALTER TABLE ONLY public.products\n ADD CONSTRAINT products_language_id_sku_key UNIQUE (language_id, sku);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_slug_key' AND conrelid = 'public.products'::regclass) THEN\n ALTER TABLE ONLY public.products\n ADD CONSTRAINT products_language_id_slug_key UNIQUE (language_id, slug);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_pkey' AND conrelid = 'public.products'::regclass) THEN\n ALTER TABLE ONLY public.products\n ADD CONSTRAINT products_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_pkey' AND conrelid = 'public.profiles'::regclass) THEN\n ALTER TABLE ONLY public.profiles\n ADD CONSTRAINT profiles_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_pkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\n ALTER TABLE ONLY public.shipping_zone_locations\n ADD CONSTRAINT shipping_zone_locations_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_pkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\n ALTER TABLE ONLY public.shipping_zone_methods\n ADD CONSTRAINT shipping_zone_methods_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zones_pkey' AND conrelid = 'public.shipping_zones'::regclass) THEN\n ALTER TABLE ONLY public.shipping_zones\n ADD CONSTRAINT shipping_zones_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_settings_pkey' AND conrelid = 'public.site_settings'::regclass) THEN\n ALTER TABLE ONLY public.site_settings\n ADD CONSTRAINT site_settings_pkey PRIMARY KEY (key);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_alerts_pkey' AND conrelid = 'public.system_alerts'::regclass) THEN\n ALTER TABLE ONLY public.system_alerts\n ADD CONSTRAINT system_alerts_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_configuration_pkey' AND conrelid = 'public.system_configuration'::regclass) THEN\n ALTER TABLE ONLY public.system_configuration\n ADD CONSTRAINT system_configuration_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tax_rates_pkey' AND conrelid = 'public.tax_rates'::regclass) THEN\n ALTER TABLE ONLY public.tax_rates\n ADD CONSTRAINT tax_rates_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'translations_pkey' AND conrelid = 'public.translations'::regclass) THEN\n ALTER TABLE ONLY public.translations\n ADD CONSTRAINT translations_pkey PRIMARY KEY (key);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ucp_cart_sessions_pkey' AND conrelid = 'public.ucp_cart_sessions'::regclass) THEN\n ALTER TABLE ONLY public.ucp_cart_sessions\n ADD CONSTRAINT ucp_cart_sessions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_pkey' AND conrelid = 'public.user_addresses'::regclass) THEN\n ALTER TABLE ONLY public.user_addresses\n ADD CONSTRAINT user_addresses_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_pkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\n ALTER TABLE ONLY public.user_security_settings\n ADD CONSTRAINT user_security_settings_pkey PRIMARY KEY (user_id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_device_hash_key' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\n ALTER TABLE ONLY public.user_trusted_devices\n ADD CONSTRAINT user_trusted_devices_device_hash_key UNIQUE (device_hash);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_pkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\n ALTER TABLE ONLY public.user_trusted_devices\n ADD CONSTRAINT user_trusted_devices_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_pkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\n ALTER TABLE ONLY public.variant_attribute_mapping\n ADD CONSTRAINT variant_attribute_mapping_pkey PRIMARY KEY (variant_id, attribute_term_id);\n END IF;\nEND $rb$;\n\nCREATE INDEX IF NOT EXISTS content_drafts_author_id_idx ON public.content_drafts USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS content_drafts_parent_idx ON public.content_drafts USING btree (parent_type, parent_id);\n\nCREATE INDEX IF NOT EXISTS content_drafts_updated_at_idx ON public.content_drafts USING btree (updated_at DESC);\n\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_freemius_mappings_coupon_product_unique ON public.coupon_freemius_mappings USING btree (coupon_id, freemius_product_id);\n\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_redemptions_order_unique ON public.coupon_redemptions USING btree (order_id) WHERE (order_id IS NOT NULL);\n\nCREATE UNIQUE INDEX IF NOT EXISTS coupons_code_unique ON public.coupons USING btree (upper(code));\n\nCREATE UNIQUE INDEX IF NOT EXISTS ensure_single_default_language_idx ON public.languages USING btree (is_default) WHERE (is_default = true);\n\nCREATE INDEX IF NOT EXISTS idx_blocks_language_id ON public.blocks USING btree (language_id);\n\nCREATE INDEX IF NOT EXISTS idx_blocks_page_id ON public.blocks USING btree (page_id);\n\nCREATE INDEX IF NOT EXISTS idx_blocks_post_id ON public.blocks USING btree (post_id);\n\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_freemius_product_id ON public.coupon_freemius_mappings USING btree (freemius_product_id);\n\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_product_id ON public.coupon_freemius_mappings USING btree (product_id) WHERE (product_id IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS idx_coupon_products_product_id ON public.coupon_products USING btree (product_id);\n\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_coupon_id ON public.coupon_redemptions USING btree (coupon_id);\n\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_user_id ON public.coupon_redemptions USING btree (user_id) WHERE (user_id IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS idx_coupons_active_dates ON public.coupons USING btree (is_active, starts_at, ends_at);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_single_default ON public.currencies USING btree (is_default) WHERE (is_default = true);\n\nCREATE INDEX IF NOT EXISTS idx_custom_block_definitions_is_original ON public.custom_block_definitions USING btree (is_original);\n\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_expires_at ON public.email_2fa_challenges USING btree (expires_at);\n\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_user_id ON public.email_2fa_challenges USING btree (user_id);\n\nCREATE INDEX IF NOT EXISTS idx_freemius_plans_product_id ON public.freemius_plans USING btree (product_id);\n\nCREATE INDEX IF NOT EXISTS idx_freemius_pricing_plan_id ON public.freemius_pricing USING btree (plan_id);\n\nCREATE INDEX IF NOT EXISTS idx_inventory_items_updated_at ON public.inventory_items USING btree (updated_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_logos_media_id ON public.logos USING btree (media_id);\n\nCREATE INDEX IF NOT EXISTS idx_media_uploader_id ON public.media USING btree (uploader_id);\n\nCREATE INDEX IF NOT EXISTS idx_navigation_items_language_id ON public.navigation_items USING btree (language_id);\n\nCREATE INDEX IF NOT EXISTS idx_navigation_items_menu_lang_order ON public.navigation_items USING btree (menu_key, language_id, \"order\");\n\nCREATE INDEX IF NOT EXISTS idx_navigation_items_page_id ON public.navigation_items USING btree (page_id);\n\nCREATE INDEX IF NOT EXISTS idx_navigation_items_parent_id ON public.navigation_items USING btree (parent_id);\n\nCREATE INDEX IF NOT EXISTS idx_order_items_order_id ON public.order_items USING btree (order_id);\n\nCREATE INDEX IF NOT EXISTS idx_order_items_product_id ON public.order_items USING btree (product_id);\n\nCREATE INDEX IF NOT EXISTS idx_order_items_variant_id ON public.order_items USING btree (variant_id);\n\nCREATE INDEX IF NOT EXISTS idx_orders_coupon_id ON public.orders USING btree (coupon_id) WHERE (coupon_id IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_license_id ON public.orders USING btree (freemius_license_id) WHERE (freemius_license_id IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_subscription_id ON public.orders USING btree (freemius_subscription_id) WHERE (freemius_subscription_id IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_trial_id ON public.orders USING btree (freemius_trial_id) WHERE (freemius_trial_id IS NOT NULL);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_orders_invoice_number_unique ON public.orders USING btree (invoice_number) WHERE (invoice_number IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS idx_orders_user_id ON public.orders USING btree (user_id);\n\nCREATE INDEX IF NOT EXISTS idx_package_activations_license_key ON public.package_activations USING btree (license_key);\n\nCREATE INDEX IF NOT EXISTS idx_package_activations_package_id ON public.package_activations USING btree (package_id);\n\nCREATE INDEX IF NOT EXISTS idx_page_revisions_author_id ON public.page_revisions USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS idx_page_revisions_page_id_version ON public.page_revisions USING btree (page_id, version);\n\nCREATE INDEX IF NOT EXISTS idx_pages_author_id ON public.pages USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS idx_pages_feature_image_id ON public.pages USING btree (feature_image_id);\n\nCREATE INDEX IF NOT EXISTS idx_pages_translation_group_id ON public.pages USING btree (translation_group_id);\n\nCREATE INDEX IF NOT EXISTS idx_post_revisions_author_id ON public.post_revisions USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS idx_post_revisions_post_id_version ON public.post_revisions USING btree (post_id, version);\n\nCREATE INDEX IF NOT EXISTS idx_posts_author_id ON public.posts USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS idx_posts_feature_image_id ON public.posts USING btree (feature_image_id);\n\nCREATE INDEX IF NOT EXISTS idx_posts_translation_group_id ON public.posts USING btree (translation_group_id);\n\nCREATE INDEX IF NOT EXISTS idx_privacy_consent_logs_created_at ON public.privacy_consent_logs USING btree (created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_product_attribute_terms_attribute_id ON public.product_attribute_terms USING btree (attribute_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_categories_category_id ON public.product_categories USING btree (category_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_categories_product_id ON public.product_categories USING btree (product_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_freemius_sale_coupons_freemius_product_id ON public.product_freemius_sale_coupons USING btree (freemius_product_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_media_media_id ON public.product_media USING btree (media_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_media_product_id ON public.product_media USING btree (product_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_variants_main_media_id ON public.product_variants USING btree (main_media_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_variants_prices_gin ON public.product_variants USING gin (prices jsonb_path_ops);\n\nCREATE INDEX IF NOT EXISTS idx_product_variants_product_id ON public.product_variants USING btree (product_id);\n\nCREATE INDEX IF NOT EXISTS idx_products_prices_gin ON public.products USING gin (prices jsonb_path_ops);\n\nCREATE INDEX IF NOT EXISTS idx_products_slug ON public.products USING btree (slug);\n\nCREATE INDEX IF NOT EXISTS idx_products_translation_group_id ON public.products USING btree (translation_group_id);\n\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_country_state_postal ON public.shipping_zone_locations USING btree (country_code, state_code, postal_code);\n\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_zone_id ON public.shipping_zone_locations USING btree (zone_id);\n\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_name_translations ON public.shipping_zone_methods USING gin (name_translations);\n\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_zone_id ON public.shipping_zone_methods USING btree (zone_id);\n\nCREATE INDEX IF NOT EXISTS idx_system_alerts_unresolved ON public.system_alerts USING btree (is_resolved, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_tax_rates_country_state ON public.tax_rates USING btree (country_code, state_code);\n\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_created_at ON public.ucp_cart_sessions USING btree (created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_status_expires_at ON public.ucp_cart_sessions USING btree (status, expires_at);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_user_addresses_one_default_per_type ON public.user_addresses USING btree (user_id, address_type) WHERE (is_default = true);\n\nCREATE INDEX IF NOT EXISTS idx_user_addresses_type ON public.user_addresses USING btree (address_type);\n\nCREATE INDEX IF NOT EXISTS idx_user_addresses_user_id ON public.user_addresses USING btree (user_id);\n\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_expires_at ON public.user_trusted_devices USING btree (expires_at);\n\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_user_id ON public.user_trusted_devices USING btree (user_id);\n\nCREATE INDEX IF NOT EXISTS idx_variant_attribute_mapping_attribute_term_id ON public.variant_attribute_mapping USING btree (attribute_term_id);\n\nCREATE INDEX IF NOT EXISTS media_folder_idx ON public.media USING btree (folder);\n\nCREATE INDEX IF NOT EXISTS product_drafts_author_id_idx ON public.product_drafts USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS product_drafts_product_id_idx ON public.product_drafts USING btree (product_id);\n\nCREATE INDEX IF NOT EXISTS product_drafts_updated_at_idx ON public.product_drafts USING btree (updated_at DESC);\n\nCREATE INDEX IF NOT EXISTS product_variants_sale_window_idx ON public.product_variants USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\n\nCREATE INDEX IF NOT EXISTS product_variants_scheduled_price_idx ON public.product_variants USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS products_sale_window_idx ON public.products USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\n\nCREATE INDEX IF NOT EXISTS products_scheduled_price_idx ON public.products USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\n\nCREATE UNIQUE INDEX IF NOT EXISTS tax_rates_country_state_name_key ON public.tax_rates USING btree (country_code, COALESCE(state_code, ''::text), lower(tax_name));\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_language_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\n ALTER TABLE ONLY public.blocks\n ADD CONSTRAINT blocks_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_page_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\n ALTER TABLE ONLY public.blocks\n ADD CONSTRAINT blocks_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_post_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\n ALTER TABLE ONLY public.blocks\n ADD CONSTRAINT blocks_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_product_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\n ALTER TABLE ONLY public.blocks\n ADD CONSTRAINT blocks_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_post_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE ONLY public.cms_interactions\n ADD CONSTRAINT cms_interactions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_product_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE ONLY public.cms_interactions\n ADD CONSTRAINT cms_interactions_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_user_id_profiles_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE ONLY public.cms_interactions\n ADD CONSTRAINT cms_interactions_user_id_profiles_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_author_id_fkey' AND conrelid = 'public.content_drafts'::regclass) THEN\n ALTER TABLE ONLY public.content_drafts\n ADD CONSTRAINT content_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_actor_user_id_fkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\n ADD CONSTRAINT cortex_ai_db_mutation_audit_actor_user_id_fkey FOREIGN KEY (actor_user_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_coupon_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\n ALTER TABLE ONLY public.coupon_freemius_mappings\n ADD CONSTRAINT coupon_freemius_mappings_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_product_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\n ALTER TABLE ONLY public.coupon_freemius_mappings\n ADD CONSTRAINT coupon_freemius_mappings_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_coupon_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\n ALTER TABLE ONLY public.coupon_products\n ADD CONSTRAINT coupon_products_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_product_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\n ALTER TABLE ONLY public.coupon_products\n ADD CONSTRAINT coupon_products_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_coupon_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\n ALTER TABLE ONLY public.coupon_redemptions\n ADD CONSTRAINT coupon_redemptions_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_order_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\n ALTER TABLE ONLY public.coupon_redemptions\n ADD CONSTRAINT coupon_redemptions_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_user_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\n ALTER TABLE ONLY public.coupon_redemptions\n ADD CONSTRAINT coupon_redemptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_user_id_fkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\n ALTER TABLE ONLY public.email_2fa_challenges\n ADD CONSTRAINT email_2fa_challenges_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_product_id_fkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\n ALTER TABLE ONLY public.freemius_plans\n ADD CONSTRAINT freemius_plans_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_plan_id_fkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\n ALTER TABLE ONLY public.freemius_pricing\n ADD CONSTRAINT freemius_pricing_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public.freemius_plans(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_media_id_fkey' AND conrelid = 'public.logos'::regclass) THEN\n ALTER TABLE ONLY public.logos\n ADD CONSTRAINT logos_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_uploader_id_fkey' AND conrelid = 'public.media'::regclass) THEN\n ALTER TABLE ONLY public.media\n ADD CONSTRAINT media_uploader_id_fkey FOREIGN KEY (uploader_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_language_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\n ALTER TABLE ONLY public.navigation_items\n ADD CONSTRAINT navigation_items_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_page_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\n ALTER TABLE ONLY public.navigation_items\n ADD CONSTRAINT navigation_items_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_parent_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\n ALTER TABLE ONLY public.navigation_items\n ADD CONSTRAINT navigation_items_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.navigation_items(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_order_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\n ALTER TABLE ONLY public.order_items\n ADD CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_product_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\n ALTER TABLE ONLY public.order_items\n ADD CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_variant_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\n ALTER TABLE ONLY public.order_items\n ADD CONSTRAINT order_items_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_coupon_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\n ALTER TABLE ONLY public.orders\n ADD CONSTRAINT orders_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_user_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\n ALTER TABLE ONLY public.orders\n ADD CONSTRAINT orders_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_author_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\n ALTER TABLE ONLY public.page_revisions\n ADD CONSTRAINT page_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\n ALTER TABLE ONLY public.page_revisions\n ADD CONSTRAINT page_revisions_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_author_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\n ALTER TABLE ONLY public.pages\n ADD CONSTRAINT pages_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_feature_image_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\n ALTER TABLE ONLY public.pages\n ADD CONSTRAINT pages_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\n ALTER TABLE ONLY public.pages\n ADD CONSTRAINT pages_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_author_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\n ALTER TABLE ONLY public.post_revisions\n ADD CONSTRAINT post_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\n ALTER TABLE ONLY public.post_revisions\n ADD CONSTRAINT post_revisions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_author_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\n ALTER TABLE ONLY public.posts\n ADD CONSTRAINT posts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_feature_image_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\n ALTER TABLE ONLY public.posts\n ADD CONSTRAINT posts_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\n ALTER TABLE ONLY public.posts\n ADD CONSTRAINT posts_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_fkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\n ALTER TABLE ONLY public.product_attribute_terms\n ADD CONSTRAINT product_attribute_terms_attribute_id_fkey FOREIGN KEY (attribute_id) REFERENCES public.product_attributes(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_category_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\n ALTER TABLE ONLY public.product_categories\n ADD CONSTRAINT product_categories_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.categories(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_product_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\n ALTER TABLE ONLY public.product_categories\n ADD CONSTRAINT product_categories_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_author_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\n ALTER TABLE ONLY public.product_drafts\n ADD CONSTRAINT product_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\n ALTER TABLE ONLY public.product_drafts\n ADD CONSTRAINT product_drafts_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_id_fkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\n ALTER TABLE ONLY public.product_freemius_sale_coupons\n ADD CONSTRAINT product_freemius_sale_coupons_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_media_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\n ALTER TABLE ONLY public.product_media\n ADD CONSTRAINT product_media_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_product_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\n ALTER TABLE ONLY public.product_media\n ADD CONSTRAINT product_media_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_main_media_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\n ALTER TABLE ONLY public.product_variants\n ADD CONSTRAINT product_variants_main_media_id_fkey FOREIGN KEY (main_media_id) REFERENCES public.media(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\n ALTER TABLE ONLY public.product_variants\n ADD CONSTRAINT product_variants_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_fkey' AND conrelid = 'public.products'::regclass) THEN\n ALTER TABLE ONLY public.products\n ADD CONSTRAINT products_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_id_fkey' AND conrelid = 'public.profiles'::regclass) THEN\n ALTER TABLE ONLY public.profiles\n ADD CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_zone_id_fkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\n ALTER TABLE ONLY public.shipping_zone_locations\n ADD CONSTRAINT shipping_zone_locations_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_zone_id_fkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\n ALTER TABLE ONLY public.shipping_zone_methods\n ADD CONSTRAINT shipping_zone_methods_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_user_id_fkey' AND conrelid = 'public.user_addresses'::regclass) THEN\n ALTER TABLE ONLY public.user_addresses\n ADD CONSTRAINT user_addresses_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_user_id_fkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\n ALTER TABLE ONLY public.user_security_settings\n ADD CONSTRAINT user_security_settings_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_user_id_fkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\n ALTER TABLE ONLY public.user_trusted_devices\n ADD CONSTRAINT user_trusted_devices_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_attribute_term_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\n ALTER TABLE ONLY public.variant_attribute_mapping\n ADD CONSTRAINT variant_attribute_mapping_attribute_term_id_fkey FOREIGN KEY (attribute_term_id) REFERENCES public.product_attribute_terms(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_variant_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\n ALTER TABLE ONLY public.variant_attribute_mapping\n ADD CONSTRAINT variant_attribute_mapping_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n"
26
26
  },
27
27
  {
28
28
  "version": "00000000000002",
29
29
  "name": "00000000000002_baseline_security_and_grants.sql",
30
- "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 02 · row-level security, policies, triggers, grants\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nDROP TRIGGER IF EXISTS on_blocks_update ON public.blocks;\r\nCREATE TRIGGER on_blocks_update BEFORE UPDATE ON public.blocks FOR EACH ROW EXECUTE FUNCTION public.handle_blocks_update();\r\n\r\nDROP TRIGGER IF EXISTS on_content_drafts_update ON public.content_drafts;\r\nCREATE TRIGGER on_content_drafts_update BEFORE UPDATE ON public.content_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS on_coupon_freemius_mappings_write ON public.coupon_freemius_mappings;\r\nCREATE TRIGGER on_coupon_freemius_mappings_write BEFORE INSERT OR UPDATE ON public.coupon_freemius_mappings FOR EACH ROW EXECUTE FUNCTION public.handle_coupon_freemius_mappings_write();\r\n\r\nDROP TRIGGER IF EXISTS on_coupons_write ON public.coupons;\r\nCREATE TRIGGER on_coupons_write BEFORE INSERT OR UPDATE ON public.coupons FOR EACH ROW EXECUTE FUNCTION public.handle_coupons_write();\r\n\r\nDROP TRIGGER IF EXISTS on_inventory_item_change ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_item_change AFTER INSERT OR DELETE OR UPDATE OF quantity ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_item_change();\r\n\r\nDROP TRIGGER IF EXISTS on_inventory_items_update ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_items_update BEFORE UPDATE ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_items_update();\r\n\r\nDROP TRIGGER IF EXISTS on_languages_update ON public.languages;\r\nCREATE TRIGGER on_languages_update BEFORE UPDATE ON public.languages FOR EACH ROW EXECUTE FUNCTION public.handle_languages_update();\r\n\r\nDROP TRIGGER IF EXISTS on_media_update ON public.media;\r\nCREATE TRIGGER on_media_update BEFORE UPDATE ON public.media FOR EACH ROW EXECUTE FUNCTION public.handle_media_update();\r\n\r\nDROP TRIGGER IF EXISTS on_navigation_items_update ON public.navigation_items;\r\nCREATE TRIGGER on_navigation_items_update BEFORE UPDATE ON public.navigation_items FOR EACH ROW EXECUTE FUNCTION public.handle_navigation_items_update();\r\n\r\nDROP TRIGGER IF EXISTS on_pages_update ON public.pages;\r\nCREATE TRIGGER on_pages_update BEFORE UPDATE ON public.pages FOR EACH ROW EXECUTE FUNCTION public.handle_pages_update();\r\n\r\nDROP TRIGGER IF EXISTS on_posts_update ON public.posts;\r\nCREATE TRIGGER on_posts_update BEFORE UPDATE ON public.posts FOR EACH ROW EXECUTE FUNCTION public.handle_posts_update();\r\n\r\nDROP TRIGGER IF EXISTS on_product_drafts_update ON public.product_drafts;\r\nCREATE TRIGGER on_product_drafts_update BEFORE UPDATE ON public.product_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS on_product_freemius_sale_coupons_write ON public.product_freemius_sale_coupons;\r\nCREATE TRIGGER on_product_freemius_sale_coupons_write BEFORE INSERT OR UPDATE ON public.product_freemius_sale_coupons FOR EACH ROW EXECUTE FUNCTION public.handle_product_freemius_sale_coupons_write();\r\n\r\nDROP TRIGGER IF EXISTS on_shipping_zone_locations_write ON public.shipping_zone_locations;\r\nCREATE TRIGGER on_shipping_zone_locations_write BEFORE INSERT OR UPDATE ON public.shipping_zone_locations FOR EACH ROW EXECUTE FUNCTION public.handle_shipping_zone_locations_write();\r\n\r\nDROP TRIGGER IF EXISTS on_tax_rates_write ON public.tax_rates;\r\nCREATE TRIGGER on_tax_rates_write BEFORE INSERT OR UPDATE ON public.tax_rates FOR EACH ROW EXECUTE FUNCTION public.handle_tax_rates_write();\r\n\r\nDROP TRIGGER IF EXISTS set_updated_at ON public.cms_interactions;\r\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS set_updated_at ON public.translations;\r\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.translations FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS trg_handle_default_currency_change ON public.currencies;\r\nCREATE TRIGGER trg_handle_default_currency_change AFTER INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.handle_default_currency_change();\r\n\r\nDROP TRIGGER IF EXISTS trg_handle_ucp_cart_sessions_update ON public.ucp_cart_sessions;\r\nCREATE TRIGGER trg_handle_ucp_cart_sessions_update BEFORE UPDATE ON public.ucp_cart_sessions FOR EACH ROW EXECUTE FUNCTION public.handle_ucp_cart_sessions_update();\r\n\r\nDROP TRIGGER IF EXISTS trg_set_currency_defaults ON public.currencies;\r\nCREATE TRIGGER trg_set_currency_defaults BEFORE INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.set_currency_defaults();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_product_variants_currency_prices ON public.product_variants;\r\nCREATE TRIGGER trg_sync_product_variants_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.product_variants FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_products_currency_prices ON public.products;\r\nCREATE TRIGGER trg_sync_products_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.products FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_shipping_method_currency_maps ON public.shipping_zone_methods;\r\nCREATE TRIGGER trg_sync_shipping_method_currency_maps BEFORE INSERT OR UPDATE OF cost_amount, cost_currency, min_order_amount, currency_pricing_mode, cost_amounts, min_order_amounts ON public.shipping_zone_methods FOR EACH ROW EXECUTE FUNCTION public.sync_shipping_method_currency_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_system_alerts_updated_at ON public.system_alerts;\r\nCREATE TRIGGER trg_system_alerts_updated_at BEFORE UPDATE ON public.system_alerts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS trigger_update_product_ratings ON public.cms_interactions;\r\nCREATE TRIGGER trigger_update_product_ratings AFTER INSERT OR DELETE OR UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.update_product_ratings();\r\n\r\nDROP POLICY IF EXISTS \"Admin can delete categories\" ON public.categories;\r\nCREATE POLICY \"Admin can delete categories\" ON public.categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can delete product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can delete product_categories\" ON public.product_categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can insert categories\" ON public.categories;\r\nCREATE POLICY \"Admin can insert categories\" ON public.categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can insert product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can insert product_categories\" ON public.product_categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can update categories\" ON public.categories;\r\nCREATE POLICY \"Admin can update categories\" ON public.categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can update product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can update product_categories\" ON public.product_categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Allow authenticated read access\" ON public.package_activations;\r\nCREATE POLICY \"Allow authenticated read access\" ON public.package_activations FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Allow service role full access\" ON public.package_activations;\r\nCREATE POLICY \"Allow service role full access\" ON public.package_activations TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view categories\" ON public.categories;\r\nCREATE POLICY \"Public can view categories\" ON public.categories FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view inventory items\" ON public.inventory_items;\r\nCREATE POLICY \"Public can view inventory items\" ON public.inventory_items FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view product media\" ON public.product_media;\r\nCREATE POLICY \"Public can view product media\" ON public.product_media FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Public can view product_categories\" ON public.product_categories FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view products\" ON public.products;\r\nCREATE POLICY \"Public can view products\" ON public.products FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read access for freemius_plans\" ON public.freemius_plans;\r\nCREATE POLICY \"Public read access for freemius_plans\" ON public.freemius_plans FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read access for freemius_pricing\" ON public.freemius_pricing;\r\nCREATE POLICY \"Public read access for freemius_pricing\" ON public.freemius_pricing FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read active currencies\" ON public.currencies;\r\nCREATE POLICY \"Public read active currencies\" ON public.currencies FOR SELECT TO authenticated, anon USING ((is_active = true));\r\n\r\nDROP POLICY IF EXISTS \"Public read product_attribute_terms\" ON public.product_attribute_terms;\r\nCREATE POLICY \"Public read product_attribute_terms\" ON public.product_attribute_terms FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read product_attributes\" ON public.product_attributes;\r\nCREATE POLICY \"Public read product_attributes\" ON public.product_attributes FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read product_variants\" ON public.product_variants;\r\nCREATE POLICY \"Public read product_variants\" ON public.product_variants FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zone_locations\" ON public.shipping_zone_locations;\r\nCREATE POLICY \"Public read shipping_zone_locations\" ON public.shipping_zone_locations FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zone_methods\" ON public.shipping_zone_methods;\r\nCREATE POLICY \"Public read shipping_zone_methods\" ON public.shipping_zone_methods FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zones\" ON public.shipping_zones;\r\nCREATE POLICY \"Public read shipping_zones\" ON public.shipping_zones FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read tax_rates\" ON public.tax_rates;\r\nCREATE POLICY \"Public read tax_rates\" ON public.tax_rates FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping;\r\nCREATE POLICY \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages inventory items\" ON public.inventory_items;\r\nCREATE POLICY \"Service Role manages inventory items\" ON public.inventory_items TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages order items\" ON public.order_items;\r\nCREATE POLICY \"Service Role manages order items\" ON public.order_items TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages orders\" ON public.orders;\r\nCREATE POLICY \"Service Role manages orders\" ON public.orders TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages tax_rates\" ON public.tax_rates;\r\nCREATE POLICY \"Service Role manages tax_rates\" ON public.tax_rates TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service role manages all addresses\" ON public.user_addresses;\r\nCREATE POLICY \"Service role manages all addresses\" ON public.user_addresses TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service role manages currencies\" ON public.currencies;\r\nCREATE POLICY \"Service role manages currencies\" ON public.currencies TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Users can manage own addresses\" ON public.user_addresses;\r\nCREATE POLICY \"Users can manage own addresses\" ON public.user_addresses TO authenticated USING ((user_id = ( SELECT auth.uid() AS uid))) WITH CHECK ((user_id = ( SELECT auth.uid() AS uid)));\r\n\r\nDROP POLICY IF EXISTS \"Users can view own order items\" ON public.order_items;\r\nCREATE POLICY \"Users can view own order items\" ON public.order_items FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (EXISTS ( SELECT 1\r\n FROM public.orders\r\n WHERE ((orders.id = order_items.order_id) AND (orders.user_id = ( SELECT auth.uid() AS uid)))))));\r\n\r\nDROP POLICY IF EXISTS \"Users can view own orders\" ON public.orders;\r\nCREATE POLICY \"Users can view own orders\" ON public.orders FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (user_id = ( SELECT auth.uid() AS uid))));\r\n\r\nALTER TABLE public.blocks ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS blocks_anon_read_policy ON public.blocks;\r\nCREATE POLICY blocks_anon_read_policy ON public.blocks FOR SELECT TO anon USING ((((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.pages p\r\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.posts pt\r\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.products pr\r\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text)))))));\r\n\r\nDROP POLICY IF EXISTS blocks_delete_policy ON public.blocks;\r\nCREATE POLICY blocks_delete_policy ON public.blocks FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS blocks_insert_policy ON public.blocks;\r\nCREATE POLICY blocks_insert_policy ON public.blocks FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS blocks_read_policy ON public.blocks;\r\nCREATE POLICY blocks_read_policy ON public.blocks FOR SELECT TO authenticated USING (((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])) OR (((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.pages p\r\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.posts pt\r\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.products pr\r\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text))))))));\r\n\r\nDROP POLICY IF EXISTS blocks_update_policy ON public.blocks;\r\nCREATE POLICY blocks_update_policy ON public.blocks FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.cms_interactions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS cms_interactions_delete_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_delete_policy ON public.cms_interactions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_insert_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_insert_policy ON public.cms_interactions FOR INSERT TO authenticated WITH CHECK (((auth.uid() = user_id) AND ((status = 'pending'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])))));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_read_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_read_policy ON public.cms_interactions FOR SELECT USING (((status = 'approved'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_update_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_update_policy ON public.cms_interactions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.content_drafts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS content_drafts_delete_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_delete_policy ON public.content_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_insert_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_insert_policy ON public.content_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_select_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_select_policy ON public.content_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_update_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_update_policy ON public.content_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.cortex_ai_db_mutation_audit ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit;\r\nCREATE POLICY cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit;\r\nCREATE POLICY cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_freemius_mappings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings;\r\nCREATE POLICY coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings;\r\nCREATE POLICY coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_products ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_products_admin_policy ON public.coupon_products;\r\nCREATE POLICY coupon_products_admin_policy ON public.coupon_products TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_products_service_role_policy ON public.coupon_products;\r\nCREATE POLICY coupon_products_service_role_policy ON public.coupon_products TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_redemptions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_redemptions_admin_select_policy ON public.coupon_redemptions;\r\nCREATE POLICY coupon_redemptions_admin_select_policy ON public.coupon_redemptions FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_redemptions_service_role_policy ON public.coupon_redemptions;\r\nCREATE POLICY coupon_redemptions_service_role_policy ON public.coupon_redemptions TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupons_admin_delete_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_delete_policy ON public.coupons FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_insert_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_insert_policy ON public.coupons FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_select_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_select_policy ON public.coupons FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_update_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_update_policy ON public.coupons FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_service_role_policy ON public.coupons;\r\nCREATE POLICY coupons_service_role_policy ON public.coupons TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.currencies ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS currencies_delete_policy ON public.currencies;\r\nCREATE POLICY currencies_delete_policy ON public.currencies FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS currencies_insert_policy ON public.currencies;\r\nCREATE POLICY currencies_insert_policy ON public.currencies FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS currencies_update_policy ON public.currencies;\r\nCREATE POLICY currencies_update_policy ON public.currencies FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.custom_block_definitions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_delete_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_delete_policy ON public.custom_block_definitions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_insert_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_insert_policy ON public.custom_block_definitions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_public_read_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_public_read_policy ON public.custom_block_definitions FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_service_role_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_service_role_policy ON public.custom_block_definitions TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_update_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_update_policy ON public.custom_block_definitions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.email_2fa_challenges ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS email_2fa_challenges_service_role_policy ON public.email_2fa_challenges;\r\nCREATE POLICY email_2fa_challenges_service_role_policy ON public.email_2fa_challenges TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.freemius_plans ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.freemius_pricing ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.inventory_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS inventory_items_delete_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_delete_policy ON public.inventory_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS inventory_items_insert_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_insert_policy ON public.inventory_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS inventory_items_update_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_update_policy ON public.inventory_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.languages ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS languages_delete_policy ON public.languages;\r\nCREATE POLICY languages_delete_policy ON public.languages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS languages_insert_policy ON public.languages;\r\nCREATE POLICY languages_insert_policy ON public.languages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS languages_read_policy ON public.languages;\r\nCREATE POLICY languages_read_policy ON public.languages FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS languages_update_policy ON public.languages;\r\nCREATE POLICY languages_update_policy ON public.languages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.logos ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS logos_delete_policy ON public.logos;\r\nCREATE POLICY logos_delete_policy ON public.logos FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS logos_insert_policy ON public.logos;\r\nCREATE POLICY logos_insert_policy ON public.logos FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS logos_read_policy ON public.logos;\r\nCREATE POLICY logos_read_policy ON public.logos FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS logos_update_policy ON public.logos;\r\nCREATE POLICY logos_update_policy ON public.logos FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.media ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS media_delete_policy ON public.media;\r\nCREATE POLICY media_delete_policy ON public.media FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS media_insert_policy ON public.media;\r\nCREATE POLICY media_insert_policy ON public.media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS media_read_policy ON public.media;\r\nCREATE POLICY media_read_policy ON public.media FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS media_service_role_policy ON public.media;\r\nCREATE POLICY media_service_role_policy ON public.media TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS media_update_policy ON public.media;\r\nCREATE POLICY media_update_policy ON public.media FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.navigation_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS navigation_items_delete_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_delete_policy ON public.navigation_items FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_items_insert_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_insert_policy ON public.navigation_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_items_update_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_update_policy ON public.navigation_items FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_read_policy ON public.navigation_items;\r\nCREATE POLICY navigation_read_policy ON public.navigation_items FOR SELECT USING (true);\r\n\r\nALTER TABLE public.order_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS order_items_delete_policy ON public.order_items;\r\nCREATE POLICY order_items_delete_policy ON public.order_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS order_items_insert_policy ON public.order_items;\r\nCREATE POLICY order_items_insert_policy ON public.order_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS order_items_update_policy ON public.order_items;\r\nCREATE POLICY order_items_update_policy ON public.order_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS orders_delete_policy ON public.orders;\r\nCREATE POLICY orders_delete_policy ON public.orders FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS orders_insert_policy ON public.orders;\r\nCREATE POLICY orders_insert_policy ON public.orders FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS orders_update_policy ON public.orders;\r\nCREATE POLICY orders_update_policy ON public.orders FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.package_activations ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.page_revisions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS page_revisions_delete_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_delete_policy ON public.page_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS page_revisions_insert_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_insert_policy ON public.page_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS page_revisions_read_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_read_policy ON public.page_revisions FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS page_revisions_update_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_update_policy ON public.page_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS pages_anon_read_policy ON public.pages;\r\nCREATE POLICY pages_anon_read_policy ON public.pages FOR SELECT TO anon USING ((status = 'published'::public.page_status));\r\n\r\nDROP POLICY IF EXISTS pages_delete_policy ON public.pages;\r\nCREATE POLICY pages_delete_policy ON public.pages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS pages_insert_policy ON public.pages;\r\nCREATE POLICY pages_insert_policy ON public.pages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS pages_read_policy ON public.pages;\r\nCREATE POLICY pages_read_policy ON public.pages FOR SELECT TO authenticated USING (((status = 'published'::public.page_status) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS pages_update_policy ON public.pages;\r\nCREATE POLICY pages_update_policy ON public.pages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.post_revisions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS post_revisions_delete_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_delete_policy ON public.post_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS post_revisions_insert_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_insert_policy ON public.post_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS post_revisions_read_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_read_policy ON public.post_revisions FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS post_revisions_update_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_update_policy ON public.post_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS posts_anon_read_policy ON public.posts;\r\nCREATE POLICY posts_anon_read_policy ON public.posts FOR SELECT TO anon USING (((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))));\r\n\r\nDROP POLICY IF EXISTS posts_delete_policy ON public.posts;\r\nCREATE POLICY posts_delete_policy ON public.posts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS posts_insert_policy ON public.posts;\r\nCREATE POLICY posts_insert_policy ON public.posts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS posts_read_policy ON public.posts;\r\nCREATE POLICY posts_read_policy ON public.posts FOR SELECT TO authenticated USING ((((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS posts_update_policy ON public.posts;\r\nCREATE POLICY posts_update_policy ON public.posts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.privacy_consent_logs ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs;\r\nCREATE POLICY privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS privacy_consent_logs_service_role_policy ON public.privacy_consent_logs;\r\nCREATE POLICY privacy_consent_logs_service_role_policy ON public.privacy_consent_logs TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.product_attribute_terms ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_delete_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_delete_policy ON public.product_attribute_terms FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_insert_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_insert_policy ON public.product_attribute_terms FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_update_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_update_policy ON public.product_attribute_terms FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_attributes ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_attributes_delete_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_delete_policy ON public.product_attributes FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attributes_insert_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_insert_policy ON public.product_attributes FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attributes_update_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_update_policy ON public.product_attributes FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_categories ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.product_drafts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_drafts_delete_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_delete_policy ON public.product_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_insert_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_insert_policy ON public.product_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_select_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_select_policy ON public.product_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_update_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_update_policy ON public.product_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.product_freemius_sale_coupons ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons;\r\nCREATE POLICY product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons;\r\nCREATE POLICY product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.product_media ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_media_delete_policy ON public.product_media;\r\nCREATE POLICY product_media_delete_policy ON public.product_media FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_media_insert_policy ON public.product_media;\r\nCREATE POLICY product_media_insert_policy ON public.product_media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_media_update_policy ON public.product_media;\r\nCREATE POLICY product_media_update_policy ON public.product_media FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_variants ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_variants_delete_policy ON public.product_variants;\r\nCREATE POLICY product_variants_delete_policy ON public.product_variants FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_variants_insert_policy ON public.product_variants;\r\nCREATE POLICY product_variants_insert_policy ON public.product_variants FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_variants_update_policy ON public.product_variants;\r\nCREATE POLICY product_variants_update_policy ON public.product_variants FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.products ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS products_delete_policy ON public.products;\r\nCREATE POLICY products_delete_policy ON public.products FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS products_insert_policy ON public.products;\r\nCREATE POLICY products_insert_policy ON public.products FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS products_update_policy ON public.products;\r\nCREATE POLICY products_update_policy ON public.products FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS profiles_insert_policy ON public.profiles;\r\nCREATE POLICY profiles_insert_policy ON public.profiles FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS profiles_read_policy ON public.profiles;\r\nCREATE POLICY profiles_read_policy ON public.profiles FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS profiles_service_role_policy ON public.profiles;\r\nCREATE POLICY profiles_service_role_policy ON public.profiles TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS profiles_update_policy ON public.profiles;\r\nCREATE POLICY profiles_update_policy ON public.profiles FOR UPDATE TO authenticated USING (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))) WITH CHECK (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)));\r\n\r\nALTER TABLE public.shipping_zone_locations ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_delete_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_delete_policy ON public.shipping_zone_locations FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_insert_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_insert_policy ON public.shipping_zone_locations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_update_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_update_policy ON public.shipping_zone_locations FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.shipping_zone_methods ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_delete_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_delete_policy ON public.shipping_zone_methods FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_insert_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_insert_policy ON public.shipping_zone_methods FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_update_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_update_policy ON public.shipping_zone_methods FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.shipping_zones ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zones_delete_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_delete_policy ON public.shipping_zones FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zones_insert_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_insert_policy ON public.shipping_zones FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zones_update_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_update_policy ON public.shipping_zones FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.site_settings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\r\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\r\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\r\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\r\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nALTER TABLE public.system_alerts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS system_alerts_select_admin ON public.system_alerts;\r\nCREATE POLICY system_alerts_select_admin ON public.system_alerts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_alerts_update_admin ON public.system_alerts;\r\nCREATE POLICY system_alerts_update_admin ON public.system_alerts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.system_configuration ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_delete ON public.system_configuration FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_insert ON public.system_configuration FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_select ON public.system_configuration FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_update ON public.system_configuration FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_service_role_all ON public.system_configuration;\r\nCREATE POLICY system_configuration_service_role_all ON public.system_configuration TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.tax_rates ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS tax_rates_delete_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_delete_policy ON public.tax_rates FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS tax_rates_insert_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_insert_policy ON public.tax_rates FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS tax_rates_update_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_update_policy ON public.tax_rates FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.translations ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS translations_delete_policy ON public.translations;\r\nCREATE POLICY translations_delete_policy ON public.translations FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS translations_insert_policy ON public.translations;\r\nCREATE POLICY translations_insert_policy ON public.translations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS translations_read_policy ON public.translations;\r\nCREATE POLICY translations_read_policy ON public.translations FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS translations_update_policy ON public.translations;\r\nCREATE POLICY translations_update_policy ON public.translations FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.ucp_cart_sessions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions;\r\nCREATE POLICY ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.user_addresses ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.user_security_settings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS user_security_settings_insert_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_insert_own_policy ON public.user_security_settings FOR INSERT TO authenticated WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_security_settings_select_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_select_own_policy ON public.user_security_settings FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_security_settings_service_role_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_service_role_policy ON public.user_security_settings TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS user_security_settings_update_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_update_own_policy ON public.user_security_settings FOR UPDATE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id)) WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nALTER TABLE public.user_trusted_devices ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_delete_own_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_delete_own_policy ON public.user_trusted_devices FOR DELETE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_select_own_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_select_own_policy ON public.user_trusted_devices FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_service_role_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_service_role_policy ON public.user_trusted_devices TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.variant_attribute_mapping ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_update_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_update_policy ON public.variant_attribute_mapping FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nGRANT USAGE ON SCHEMA public TO postgres;\r\n\r\nGRANT USAGE ON SCHEMA public TO anon;\r\n\r\nGRANT USAGE ON SCHEMA public TO authenticated;\r\n\r\nGRANT USAGE ON SCHEMA public TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO service_role;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO anon;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO service_role;\r\n\r\nREVOKE ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) FROM PUBLIC;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO service_role;\r\n\r\nREVOKE ALL ON FUNCTION public.handle_new_user() FROM PUBLIC;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO service_role;\r\n\r\nGRANT ALL ON TABLE public.blocks TO anon;\r\n\r\nGRANT ALL ON TABLE public.blocks TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.blocks TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.categories TO anon;\r\n\r\nGRANT ALL ON TABLE public.categories TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.categories TO service_role;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO anon;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO anon;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO anon;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupons TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupons TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupons TO service_role;\r\n\r\nGRANT ALL ON TABLE public.currencies TO anon;\r\n\r\nGRANT ALL ON TABLE public.currencies TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.currencies TO service_role;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO anon;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO service_role;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO anon;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO service_role;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO anon;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO service_role;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO service_role;\r\n\r\nGRANT ALL ON TABLE public.languages TO anon;\r\n\r\nGRANT ALL ON TABLE public.languages TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.languages TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.logos TO anon;\r\n\r\nGRANT ALL ON TABLE public.logos TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.logos TO service_role;\r\n\r\nGRANT ALL ON TABLE public.media TO anon;\r\n\r\nGRANT ALL ON TABLE public.media TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.media TO service_role;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.order_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.order_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.order_items TO service_role;\r\n\r\nGRANT ALL ON TABLE public.orders TO anon;\r\n\r\nGRANT ALL ON TABLE public.orders TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.orders TO service_role;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO anon;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO anon;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.pages TO anon;\r\n\r\nGRANT ALL ON TABLE public.pages TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.pages TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO anon;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.posts TO anon;\r\n\r\nGRANT ALL ON TABLE public.posts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.posts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO anon;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_media TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_media TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_media TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO service_role;\r\n\r\nGRANT ALL ON TABLE public.products TO anon;\r\n\r\nGRANT ALL ON TABLE public.products TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.products TO service_role;\r\n\r\nGRANT ALL ON TABLE public.profiles TO anon;\r\n\r\nGRANT ALL ON TABLE public.profiles TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.profiles TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO service_role;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO anon;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO anon;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO service_role;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO anon;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO service_role;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO anon;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO service_role;\r\n\r\nGRANT ALL ON TABLE public.translations TO anon;\r\n\r\nGRANT ALL ON TABLE public.translations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.translations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO anon;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO service_role;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO anon;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO service_role;\r\n\r\n-- Re-attached: trigger lives on auth.users, which a public-only pg_dump omits (see migration 005).\r\nDROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;\r\nCREATE TRIGGER on_auth_user_created\r\n AFTER INSERT ON auth.users\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_new_user();\r\nREVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;\r\n"
30
+ "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\n-- 02 · row-level security, policies, triggers, grants\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\n\nDROP TRIGGER IF EXISTS on_blocks_update ON public.blocks;\nCREATE TRIGGER on_blocks_update BEFORE UPDATE ON public.blocks FOR EACH ROW EXECUTE FUNCTION public.handle_blocks_update();\n\nDROP TRIGGER IF EXISTS on_content_drafts_update ON public.content_drafts;\nCREATE TRIGGER on_content_drafts_update BEFORE UPDATE ON public.content_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nDROP TRIGGER IF EXISTS on_coupon_freemius_mappings_write ON public.coupon_freemius_mappings;\nCREATE TRIGGER on_coupon_freemius_mappings_write BEFORE INSERT OR UPDATE ON public.coupon_freemius_mappings FOR EACH ROW EXECUTE FUNCTION public.handle_coupon_freemius_mappings_write();\n\nDROP TRIGGER IF EXISTS on_coupons_write ON public.coupons;\nCREATE TRIGGER on_coupons_write BEFORE INSERT OR UPDATE ON public.coupons FOR EACH ROW EXECUTE FUNCTION public.handle_coupons_write();\n\nDROP TRIGGER IF EXISTS on_inventory_item_change ON public.inventory_items;\nCREATE TRIGGER on_inventory_item_change AFTER INSERT OR DELETE OR UPDATE OF quantity ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_item_change();\n\nDROP TRIGGER IF EXISTS on_inventory_items_update ON public.inventory_items;\nCREATE TRIGGER on_inventory_items_update BEFORE UPDATE ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_items_update();\n\nDROP TRIGGER IF EXISTS on_languages_update ON public.languages;\nCREATE TRIGGER on_languages_update BEFORE UPDATE ON public.languages FOR EACH ROW EXECUTE FUNCTION public.handle_languages_update();\n\nDROP TRIGGER IF EXISTS on_media_update ON public.media;\nCREATE TRIGGER on_media_update BEFORE UPDATE ON public.media FOR EACH ROW EXECUTE FUNCTION public.handle_media_update();\n\nDROP TRIGGER IF EXISTS on_navigation_items_update ON public.navigation_items;\nCREATE TRIGGER on_navigation_items_update BEFORE UPDATE ON public.navigation_items FOR EACH ROW EXECUTE FUNCTION public.handle_navigation_items_update();\n\nDROP TRIGGER IF EXISTS on_pages_update ON public.pages;\nCREATE TRIGGER on_pages_update BEFORE UPDATE ON public.pages FOR EACH ROW EXECUTE FUNCTION public.handle_pages_update();\n\nDROP TRIGGER IF EXISTS on_posts_update ON public.posts;\nCREATE TRIGGER on_posts_update BEFORE UPDATE ON public.posts FOR EACH ROW EXECUTE FUNCTION public.handle_posts_update();\n\nDROP TRIGGER IF EXISTS on_product_drafts_update ON public.product_drafts;\nCREATE TRIGGER on_product_drafts_update BEFORE UPDATE ON public.product_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nDROP TRIGGER IF EXISTS on_product_freemius_sale_coupons_write ON public.product_freemius_sale_coupons;\nCREATE TRIGGER on_product_freemius_sale_coupons_write BEFORE INSERT OR UPDATE ON public.product_freemius_sale_coupons FOR EACH ROW EXECUTE FUNCTION public.handle_product_freemius_sale_coupons_write();\n\nDROP TRIGGER IF EXISTS on_shipping_zone_locations_write ON public.shipping_zone_locations;\nCREATE TRIGGER on_shipping_zone_locations_write BEFORE INSERT OR UPDATE ON public.shipping_zone_locations FOR EACH ROW EXECUTE FUNCTION public.handle_shipping_zone_locations_write();\n\nDROP TRIGGER IF EXISTS on_tax_rates_write ON public.tax_rates;\nCREATE TRIGGER on_tax_rates_write BEFORE INSERT OR UPDATE ON public.tax_rates FOR EACH ROW EXECUTE FUNCTION public.handle_tax_rates_write();\n\nDROP TRIGGER IF EXISTS set_updated_at ON public.cms_interactions;\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nDROP TRIGGER IF EXISTS set_updated_at ON public.translations;\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.translations FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nDROP TRIGGER IF EXISTS trg_handle_default_currency_change ON public.currencies;\nCREATE TRIGGER trg_handle_default_currency_change AFTER INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.handle_default_currency_change();\n\nDROP TRIGGER IF EXISTS trg_handle_ucp_cart_sessions_update ON public.ucp_cart_sessions;\nCREATE TRIGGER trg_handle_ucp_cart_sessions_update BEFORE UPDATE ON public.ucp_cart_sessions FOR EACH ROW EXECUTE FUNCTION public.handle_ucp_cart_sessions_update();\n\nDROP TRIGGER IF EXISTS trg_set_currency_defaults ON public.currencies;\nCREATE TRIGGER trg_set_currency_defaults BEFORE INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.set_currency_defaults();\n\nDROP TRIGGER IF EXISTS trg_sync_product_variants_currency_prices ON public.product_variants;\nCREATE TRIGGER trg_sync_product_variants_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.product_variants FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\n\nDROP TRIGGER IF EXISTS trg_sync_products_currency_prices ON public.products;\nCREATE TRIGGER trg_sync_products_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.products FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\n\nDROP TRIGGER IF EXISTS trg_sync_shipping_method_currency_maps ON public.shipping_zone_methods;\nCREATE TRIGGER trg_sync_shipping_method_currency_maps BEFORE INSERT OR UPDATE OF cost_amount, cost_currency, min_order_amount, currency_pricing_mode, cost_amounts, min_order_amounts ON public.shipping_zone_methods FOR EACH ROW EXECUTE FUNCTION public.sync_shipping_method_currency_maps();\n\nDROP TRIGGER IF EXISTS trg_system_alerts_updated_at ON public.system_alerts;\nCREATE TRIGGER trg_system_alerts_updated_at BEFORE UPDATE ON public.system_alerts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nDROP TRIGGER IF EXISTS trigger_update_product_ratings ON public.cms_interactions;\nCREATE TRIGGER trigger_update_product_ratings AFTER INSERT OR DELETE OR UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.update_product_ratings();\n\nDROP POLICY IF EXISTS \"Admin can delete categories\" ON public.categories;\nCREATE POLICY \"Admin can delete categories\" ON public.categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS \"Admin can delete product_categories\" ON public.product_categories;\nCREATE POLICY \"Admin can delete product_categories\" ON public.product_categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS \"Admin can insert categories\" ON public.categories;\nCREATE POLICY \"Admin can insert categories\" ON public.categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS \"Admin can insert product_categories\" ON public.product_categories;\nCREATE POLICY \"Admin can insert product_categories\" ON public.product_categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS \"Admin can update categories\" ON public.categories;\nCREATE POLICY \"Admin can update categories\" ON public.categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS \"Admin can update product_categories\" ON public.product_categories;\nCREATE POLICY \"Admin can update product_categories\" ON public.product_categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS \"Allow authenticated read access\" ON public.package_activations;\nCREATE POLICY \"Allow authenticated read access\" ON public.package_activations FOR SELECT TO authenticated USING (true);\n\nDROP POLICY IF EXISTS \"Allow service role full access\" ON public.package_activations;\nCREATE POLICY \"Allow service role full access\" ON public.package_activations TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS \"Public can view categories\" ON public.categories;\nCREATE POLICY \"Public can view categories\" ON public.categories FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public can view inventory items\" ON public.inventory_items;\nCREATE POLICY \"Public can view inventory items\" ON public.inventory_items FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public can view product media\" ON public.product_media;\nCREATE POLICY \"Public can view product media\" ON public.product_media FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public can view product_categories\" ON public.product_categories;\nCREATE POLICY \"Public can view product_categories\" ON public.product_categories FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public can view products\" ON public.products;\nCREATE POLICY \"Public can view products\" ON public.products FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read access for freemius_plans\" ON public.freemius_plans;\nCREATE POLICY \"Public read access for freemius_plans\" ON public.freemius_plans FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read access for freemius_pricing\" ON public.freemius_pricing;\nCREATE POLICY \"Public read access for freemius_pricing\" ON public.freemius_pricing FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read active currencies\" ON public.currencies;\nCREATE POLICY \"Public read active currencies\" ON public.currencies FOR SELECT TO authenticated, anon USING ((is_active = true));\n\nDROP POLICY IF EXISTS \"Public read product_attribute_terms\" ON public.product_attribute_terms;\nCREATE POLICY \"Public read product_attribute_terms\" ON public.product_attribute_terms FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read product_attributes\" ON public.product_attributes;\nCREATE POLICY \"Public read product_attributes\" ON public.product_attributes FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read product_variants\" ON public.product_variants;\nCREATE POLICY \"Public read product_variants\" ON public.product_variants FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read shipping_zone_locations\" ON public.shipping_zone_locations;\nCREATE POLICY \"Public read shipping_zone_locations\" ON public.shipping_zone_locations FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read shipping_zone_methods\" ON public.shipping_zone_methods;\nCREATE POLICY \"Public read shipping_zone_methods\" ON public.shipping_zone_methods FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read shipping_zones\" ON public.shipping_zones;\nCREATE POLICY \"Public read shipping_zones\" ON public.shipping_zones FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read tax_rates\" ON public.tax_rates;\nCREATE POLICY \"Public read tax_rates\" ON public.tax_rates FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping;\nCREATE POLICY \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS \"Service Role manages inventory items\" ON public.inventory_items;\nCREATE POLICY \"Service Role manages inventory items\" ON public.inventory_items TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS \"Service Role manages order items\" ON public.order_items;\nCREATE POLICY \"Service Role manages order items\" ON public.order_items TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS \"Service Role manages orders\" ON public.orders;\nCREATE POLICY \"Service Role manages orders\" ON public.orders TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS \"Service Role manages tax_rates\" ON public.tax_rates;\nCREATE POLICY \"Service Role manages tax_rates\" ON public.tax_rates TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS \"Service role manages all addresses\" ON public.user_addresses;\nCREATE POLICY \"Service role manages all addresses\" ON public.user_addresses TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS \"Service role manages currencies\" ON public.currencies;\nCREATE POLICY \"Service role manages currencies\" ON public.currencies TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS \"Users can manage own addresses\" ON public.user_addresses;\nCREATE POLICY \"Users can manage own addresses\" ON public.user_addresses TO authenticated USING ((user_id = ( SELECT auth.uid() AS uid))) WITH CHECK ((user_id = ( SELECT auth.uid() AS uid)));\n\nDROP POLICY IF EXISTS \"Users can view own order items\" ON public.order_items;\nCREATE POLICY \"Users can view own order items\" ON public.order_items FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (EXISTS ( SELECT 1\n FROM public.orders\n WHERE ((orders.id = order_items.order_id) AND (orders.user_id = ( SELECT auth.uid() AS uid)))))));\n\nDROP POLICY IF EXISTS \"Users can view own orders\" ON public.orders;\nCREATE POLICY \"Users can view own orders\" ON public.orders FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (user_id = ( SELECT auth.uid() AS uid))));\n\nALTER TABLE public.blocks ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS blocks_anon_read_policy ON public.blocks;\nCREATE POLICY blocks_anon_read_policy ON public.blocks FOR SELECT TO anon USING ((((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\n FROM public.pages p\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\n FROM public.posts pt\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\n FROM public.products pr\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text)))))));\n\nDROP POLICY IF EXISTS blocks_delete_policy ON public.blocks;\nCREATE POLICY blocks_delete_policy ON public.blocks FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS blocks_insert_policy ON public.blocks;\nCREATE POLICY blocks_insert_policy ON public.blocks FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS blocks_read_policy ON public.blocks;\nCREATE POLICY blocks_read_policy ON public.blocks FOR SELECT TO authenticated USING (((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])) OR (((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\n FROM public.pages p\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\n FROM public.posts pt\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\n FROM public.products pr\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text))))))));\n\nDROP POLICY IF EXISTS blocks_update_policy ON public.blocks;\nCREATE POLICY blocks_update_policy ON public.blocks FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.cms_interactions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS cms_interactions_delete_policy ON public.cms_interactions;\nCREATE POLICY cms_interactions_delete_policy ON public.cms_interactions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS cms_interactions_insert_policy ON public.cms_interactions;\nCREATE POLICY cms_interactions_insert_policy ON public.cms_interactions FOR INSERT TO authenticated WITH CHECK (((auth.uid() = user_id) AND ((status = 'pending'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])))));\n\nDROP POLICY IF EXISTS cms_interactions_read_policy ON public.cms_interactions;\nCREATE POLICY cms_interactions_read_policy ON public.cms_interactions FOR SELECT USING (((status = 'approved'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\n\nDROP POLICY IF EXISTS cms_interactions_update_policy ON public.cms_interactions;\nCREATE POLICY cms_interactions_update_policy ON public.cms_interactions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.content_drafts ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS content_drafts_delete_policy ON public.content_drafts;\nCREATE POLICY content_drafts_delete_policy ON public.content_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS content_drafts_insert_policy ON public.content_drafts;\nCREATE POLICY content_drafts_insert_policy ON public.content_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS content_drafts_select_policy ON public.content_drafts;\nCREATE POLICY content_drafts_select_policy ON public.content_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS content_drafts_update_policy ON public.content_drafts;\nCREATE POLICY content_drafts_update_policy ON public.content_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.cortex_ai_db_mutation_audit ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit;\nCREATE POLICY cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit;\nCREATE POLICY cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.coupon_freemius_mappings ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings;\nCREATE POLICY coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings;\nCREATE POLICY coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.coupon_products ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS coupon_products_admin_policy ON public.coupon_products;\nCREATE POLICY coupon_products_admin_policy ON public.coupon_products TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS coupon_products_service_role_policy ON public.coupon_products;\nCREATE POLICY coupon_products_service_role_policy ON public.coupon_products TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.coupon_redemptions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS coupon_redemptions_admin_select_policy ON public.coupon_redemptions;\nCREATE POLICY coupon_redemptions_admin_select_policy ON public.coupon_redemptions FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS coupon_redemptions_service_role_policy ON public.coupon_redemptions;\nCREATE POLICY coupon_redemptions_service_role_policy ON public.coupon_redemptions TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS coupons_admin_delete_policy ON public.coupons;\nCREATE POLICY coupons_admin_delete_policy ON public.coupons FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS coupons_admin_insert_policy ON public.coupons;\nCREATE POLICY coupons_admin_insert_policy ON public.coupons FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS coupons_admin_select_policy ON public.coupons;\nCREATE POLICY coupons_admin_select_policy ON public.coupons FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS coupons_admin_update_policy ON public.coupons;\nCREATE POLICY coupons_admin_update_policy ON public.coupons FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS coupons_service_role_policy ON public.coupons;\nCREATE POLICY coupons_service_role_policy ON public.coupons TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.currencies ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS currencies_delete_policy ON public.currencies;\nCREATE POLICY currencies_delete_policy ON public.currencies FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS currencies_insert_policy ON public.currencies;\nCREATE POLICY currencies_insert_policy ON public.currencies FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS currencies_update_policy ON public.currencies;\nCREATE POLICY currencies_update_policy ON public.currencies FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.custom_block_definitions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS custom_block_definitions_delete_policy ON public.custom_block_definitions;\nCREATE POLICY custom_block_definitions_delete_policy ON public.custom_block_definitions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS custom_block_definitions_insert_policy ON public.custom_block_definitions;\nCREATE POLICY custom_block_definitions_insert_policy ON public.custom_block_definitions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS custom_block_definitions_public_read_policy ON public.custom_block_definitions;\nCREATE POLICY custom_block_definitions_public_read_policy ON public.custom_block_definitions FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS custom_block_definitions_service_role_policy ON public.custom_block_definitions;\nCREATE POLICY custom_block_definitions_service_role_policy ON public.custom_block_definitions TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS custom_block_definitions_update_policy ON public.custom_block_definitions;\nCREATE POLICY custom_block_definitions_update_policy ON public.custom_block_definitions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.email_2fa_challenges ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS email_2fa_challenges_service_role_policy ON public.email_2fa_challenges;\nCREATE POLICY email_2fa_challenges_service_role_policy ON public.email_2fa_challenges TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.freemius_plans ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.freemius_pricing ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.inventory_items ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS inventory_items_delete_policy ON public.inventory_items;\nCREATE POLICY inventory_items_delete_policy ON public.inventory_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS inventory_items_insert_policy ON public.inventory_items;\nCREATE POLICY inventory_items_insert_policy ON public.inventory_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS inventory_items_update_policy ON public.inventory_items;\nCREATE POLICY inventory_items_update_policy ON public.inventory_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.languages ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS languages_delete_policy ON public.languages;\nCREATE POLICY languages_delete_policy ON public.languages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS languages_insert_policy ON public.languages;\nCREATE POLICY languages_insert_policy ON public.languages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS languages_read_policy ON public.languages;\nCREATE POLICY languages_read_policy ON public.languages FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS languages_update_policy ON public.languages;\nCREATE POLICY languages_update_policy ON public.languages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nALTER TABLE public.logos ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS logos_delete_policy ON public.logos;\nCREATE POLICY logos_delete_policy ON public.logos FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS logos_insert_policy ON public.logos;\nCREATE POLICY logos_insert_policy ON public.logos FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS logos_read_policy ON public.logos;\nCREATE POLICY logos_read_policy ON public.logos FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS logos_update_policy ON public.logos;\nCREATE POLICY logos_update_policy ON public.logos FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nALTER TABLE public.media ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS media_delete_policy ON public.media;\nCREATE POLICY media_delete_policy ON public.media FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS media_insert_policy ON public.media;\nCREATE POLICY media_insert_policy ON public.media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS media_read_policy ON public.media;\nCREATE POLICY media_read_policy ON public.media FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS media_service_role_policy ON public.media;\nCREATE POLICY media_service_role_policy ON public.media TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS media_update_policy ON public.media;\nCREATE POLICY media_update_policy ON public.media FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.navigation_items ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS navigation_items_delete_policy ON public.navigation_items;\nCREATE POLICY navigation_items_delete_policy ON public.navigation_items FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS navigation_items_insert_policy ON public.navigation_items;\nCREATE POLICY navigation_items_insert_policy ON public.navigation_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS navigation_items_update_policy ON public.navigation_items;\nCREATE POLICY navigation_items_update_policy ON public.navigation_items FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS navigation_read_policy ON public.navigation_items;\nCREATE POLICY navigation_read_policy ON public.navigation_items FOR SELECT USING (true);\n\nALTER TABLE public.order_items ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS order_items_delete_policy ON public.order_items;\nCREATE POLICY order_items_delete_policy ON public.order_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS order_items_insert_policy ON public.order_items;\nCREATE POLICY order_items_insert_policy ON public.order_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS order_items_update_policy ON public.order_items;\nCREATE POLICY order_items_update_policy ON public.order_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS orders_delete_policy ON public.orders;\nCREATE POLICY orders_delete_policy ON public.orders FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS orders_insert_policy ON public.orders;\nCREATE POLICY orders_insert_policy ON public.orders FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS orders_update_policy ON public.orders;\nCREATE POLICY orders_update_policy ON public.orders FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.package_activations ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.page_revisions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS page_revisions_delete_policy ON public.page_revisions;\nCREATE POLICY page_revisions_delete_policy ON public.page_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS page_revisions_insert_policy ON public.page_revisions;\nCREATE POLICY page_revisions_insert_policy ON public.page_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS page_revisions_read_policy ON public.page_revisions;\nCREATE POLICY page_revisions_read_policy ON public.page_revisions FOR SELECT TO authenticated USING (true);\n\nDROP POLICY IF EXISTS page_revisions_update_policy ON public.page_revisions;\nCREATE POLICY page_revisions_update_policy ON public.page_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS pages_anon_read_policy ON public.pages;\nCREATE POLICY pages_anon_read_policy ON public.pages FOR SELECT TO anon USING ((status = 'published'::public.page_status));\n\nDROP POLICY IF EXISTS pages_delete_policy ON public.pages;\nCREATE POLICY pages_delete_policy ON public.pages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS pages_insert_policy ON public.pages;\nCREATE POLICY pages_insert_policy ON public.pages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS pages_read_policy ON public.pages;\nCREATE POLICY pages_read_policy ON public.pages FOR SELECT TO authenticated USING (((status = 'published'::public.page_status) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\n\nDROP POLICY IF EXISTS pages_update_policy ON public.pages;\nCREATE POLICY pages_update_policy ON public.pages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.post_revisions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS post_revisions_delete_policy ON public.post_revisions;\nCREATE POLICY post_revisions_delete_policy ON public.post_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS post_revisions_insert_policy ON public.post_revisions;\nCREATE POLICY post_revisions_insert_policy ON public.post_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS post_revisions_read_policy ON public.post_revisions;\nCREATE POLICY post_revisions_read_policy ON public.post_revisions FOR SELECT TO authenticated USING (true);\n\nDROP POLICY IF EXISTS post_revisions_update_policy ON public.post_revisions;\nCREATE POLICY post_revisions_update_policy ON public.post_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS posts_anon_read_policy ON public.posts;\nCREATE POLICY posts_anon_read_policy ON public.posts FOR SELECT TO anon USING (((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))));\n\nDROP POLICY IF EXISTS posts_delete_policy ON public.posts;\nCREATE POLICY posts_delete_policy ON public.posts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS posts_insert_policy ON public.posts;\nCREATE POLICY posts_insert_policy ON public.posts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS posts_read_policy ON public.posts;\nCREATE POLICY posts_read_policy ON public.posts FOR SELECT TO authenticated USING ((((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\n\nDROP POLICY IF EXISTS posts_update_policy ON public.posts;\nCREATE POLICY posts_update_policy ON public.posts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.privacy_consent_logs ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs;\nCREATE POLICY privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS privacy_consent_logs_service_role_policy ON public.privacy_consent_logs;\nCREATE POLICY privacy_consent_logs_service_role_policy ON public.privacy_consent_logs TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.product_attribute_terms ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_attribute_terms_delete_policy ON public.product_attribute_terms;\nCREATE POLICY product_attribute_terms_delete_policy ON public.product_attribute_terms FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_attribute_terms_insert_policy ON public.product_attribute_terms;\nCREATE POLICY product_attribute_terms_insert_policy ON public.product_attribute_terms FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_attribute_terms_update_policy ON public.product_attribute_terms;\nCREATE POLICY product_attribute_terms_update_policy ON public.product_attribute_terms FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.product_attributes ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_attributes_delete_policy ON public.product_attributes;\nCREATE POLICY product_attributes_delete_policy ON public.product_attributes FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_attributes_insert_policy ON public.product_attributes;\nCREATE POLICY product_attributes_insert_policy ON public.product_attributes FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_attributes_update_policy ON public.product_attributes;\nCREATE POLICY product_attributes_update_policy ON public.product_attributes FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.product_categories ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.product_drafts ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_drafts_delete_policy ON public.product_drafts;\nCREATE POLICY product_drafts_delete_policy ON public.product_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS product_drafts_insert_policy ON public.product_drafts;\nCREATE POLICY product_drafts_insert_policy ON public.product_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS product_drafts_select_policy ON public.product_drafts;\nCREATE POLICY product_drafts_select_policy ON public.product_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS product_drafts_update_policy ON public.product_drafts;\nCREATE POLICY product_drafts_update_policy ON public.product_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.product_freemius_sale_coupons ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons;\nCREATE POLICY product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons;\nCREATE POLICY product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.product_media ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_media_delete_policy ON public.product_media;\nCREATE POLICY product_media_delete_policy ON public.product_media FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_media_insert_policy ON public.product_media;\nCREATE POLICY product_media_insert_policy ON public.product_media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_media_update_policy ON public.product_media;\nCREATE POLICY product_media_update_policy ON public.product_media FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.product_variants ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_variants_delete_policy ON public.product_variants;\nCREATE POLICY product_variants_delete_policy ON public.product_variants FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_variants_insert_policy ON public.product_variants;\nCREATE POLICY product_variants_insert_policy ON public.product_variants FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_variants_update_policy ON public.product_variants;\nCREATE POLICY product_variants_update_policy ON public.product_variants FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.products ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS products_delete_policy ON public.products;\nCREATE POLICY products_delete_policy ON public.products FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS products_insert_policy ON public.products;\nCREATE POLICY products_insert_policy ON public.products FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS products_update_policy ON public.products;\nCREATE POLICY products_update_policy ON public.products FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS profiles_insert_policy ON public.profiles;\nCREATE POLICY profiles_insert_policy ON public.profiles FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS profiles_read_policy ON public.profiles;\nCREATE POLICY profiles_read_policy ON public.profiles FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS profiles_service_role_policy ON public.profiles;\nCREATE POLICY profiles_service_role_policy ON public.profiles TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS profiles_update_policy ON public.profiles;\nCREATE POLICY profiles_update_policy ON public.profiles FOR UPDATE TO authenticated USING (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))) WITH CHECK (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)));\n\nALTER TABLE public.shipping_zone_locations ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS shipping_zone_locations_delete_policy ON public.shipping_zone_locations;\nCREATE POLICY shipping_zone_locations_delete_policy ON public.shipping_zone_locations FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS shipping_zone_locations_insert_policy ON public.shipping_zone_locations;\nCREATE POLICY shipping_zone_locations_insert_policy ON public.shipping_zone_locations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS shipping_zone_locations_update_policy ON public.shipping_zone_locations;\nCREATE POLICY shipping_zone_locations_update_policy ON public.shipping_zone_locations FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.shipping_zone_methods ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS shipping_zone_methods_delete_policy ON public.shipping_zone_methods;\nCREATE POLICY shipping_zone_methods_delete_policy ON public.shipping_zone_methods FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS shipping_zone_methods_insert_policy ON public.shipping_zone_methods;\nCREATE POLICY shipping_zone_methods_insert_policy ON public.shipping_zone_methods FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS shipping_zone_methods_update_policy ON public.shipping_zone_methods;\nCREATE POLICY shipping_zone_methods_update_policy ON public.shipping_zone_methods FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.shipping_zones ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS shipping_zones_delete_policy ON public.shipping_zones;\nCREATE POLICY shipping_zones_delete_policy ON public.shipping_zones FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS shipping_zones_insert_policy ON public.shipping_zones;\nCREATE POLICY shipping_zones_insert_policy ON public.shipping_zones FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS shipping_zones_update_policy ON public.shipping_zones;\nCREATE POLICY shipping_zones_update_policy ON public.shipping_zones FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.site_settings ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nALTER TABLE public.system_alerts ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS system_alerts_select_admin ON public.system_alerts;\nCREATE POLICY system_alerts_select_admin ON public.system_alerts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS system_alerts_update_admin ON public.system_alerts;\nCREATE POLICY system_alerts_update_admin ON public.system_alerts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nALTER TABLE public.system_configuration ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;\nCREATE POLICY system_configuration_admin_delete ON public.system_configuration FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;\nCREATE POLICY system_configuration_admin_insert ON public.system_configuration FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;\nCREATE POLICY system_configuration_admin_select ON public.system_configuration FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;\nCREATE POLICY system_configuration_admin_update ON public.system_configuration FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS system_configuration_service_role_all ON public.system_configuration;\nCREATE POLICY system_configuration_service_role_all ON public.system_configuration TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.tax_rates ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS tax_rates_delete_policy ON public.tax_rates;\nCREATE POLICY tax_rates_delete_policy ON public.tax_rates FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS tax_rates_insert_policy ON public.tax_rates;\nCREATE POLICY tax_rates_insert_policy ON public.tax_rates FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS tax_rates_update_policy ON public.tax_rates;\nCREATE POLICY tax_rates_update_policy ON public.tax_rates FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nALTER TABLE public.translations ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS translations_delete_policy ON public.translations;\nCREATE POLICY translations_delete_policy ON public.translations FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS translations_insert_policy ON public.translations;\nCREATE POLICY translations_insert_policy ON public.translations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS translations_read_policy ON public.translations;\nCREATE POLICY translations_read_policy ON public.translations FOR SELECT USING (true);\n\nDROP POLICY IF EXISTS translations_update_policy ON public.translations;\nCREATE POLICY translations_update_policy ON public.translations FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nALTER TABLE public.ucp_cart_sessions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions;\nCREATE POLICY ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.user_addresses ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.user_security_settings ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS user_security_settings_insert_own_policy ON public.user_security_settings;\nCREATE POLICY user_security_settings_insert_own_policy ON public.user_security_settings FOR INSERT TO authenticated WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\n\nDROP POLICY IF EXISTS user_security_settings_select_own_policy ON public.user_security_settings;\nCREATE POLICY user_security_settings_select_own_policy ON public.user_security_settings FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\n\nDROP POLICY IF EXISTS user_security_settings_service_role_policy ON public.user_security_settings;\nCREATE POLICY user_security_settings_service_role_policy ON public.user_security_settings TO service_role USING (true) WITH CHECK (true);\n\nDROP POLICY IF EXISTS user_security_settings_update_own_policy ON public.user_security_settings;\nCREATE POLICY user_security_settings_update_own_policy ON public.user_security_settings FOR UPDATE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id)) WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\n\nALTER TABLE public.user_trusted_devices ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS user_trusted_devices_delete_own_policy ON public.user_trusted_devices;\nCREATE POLICY user_trusted_devices_delete_own_policy ON public.user_trusted_devices FOR DELETE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\n\nDROP POLICY IF EXISTS user_trusted_devices_select_own_policy ON public.user_trusted_devices;\nCREATE POLICY user_trusted_devices_select_own_policy ON public.user_trusted_devices FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\n\nDROP POLICY IF EXISTS user_trusted_devices_service_role_policy ON public.user_trusted_devices;\nCREATE POLICY user_trusted_devices_service_role_policy ON public.user_trusted_devices TO service_role USING (true) WITH CHECK (true);\n\nALTER TABLE public.variant_attribute_mapping ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping;\nCREATE POLICY variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping;\nCREATE POLICY variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS variant_attribute_mapping_update_policy ON public.variant_attribute_mapping;\nCREATE POLICY variant_attribute_mapping_update_policy ON public.variant_attribute_mapping FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\n\nGRANT USAGE ON SCHEMA public TO postgres;\n\nGRANT USAGE ON SCHEMA public TO anon;\n\nGRANT USAGE ON SCHEMA public TO authenticated;\n\nGRANT USAGE ON SCHEMA public TO service_role;\n\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO anon;\n\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO authenticated;\n\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO service_role;\n\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO anon;\n\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO authenticated;\n\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO service_role;\n\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO anon;\n\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO authenticated;\n\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO service_role;\n\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO anon;\n\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO authenticated;\n\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO service_role;\n\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO anon;\n\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO authenticated;\n\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO service_role;\n\nGRANT ALL ON TABLE public.custom_block_definitions TO anon;\n\nGRANT ALL ON TABLE public.custom_block_definitions TO authenticated;\n\nGRANT ALL ON TABLE public.custom_block_definitions TO service_role;\n\nREVOKE ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) FROM PUBLIC;\n\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO authenticated;\n\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO service_role;\n\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO anon;\n\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO anon;\n\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO authenticated;\n\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO service_role;\n\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO anon;\n\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO authenticated;\n\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO service_role;\n\nGRANT ALL ON FUNCTION public.get_current_user_role() TO anon;\n\nGRANT ALL ON FUNCTION public.get_current_user_role() TO authenticated;\n\nGRANT ALL ON FUNCTION public.get_current_user_role() TO service_role;\n\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO anon;\n\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO authenticated;\n\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO service_role;\n\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO anon;\n\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO authenticated;\n\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO service_role;\n\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO anon;\n\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO authenticated;\n\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_languages_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_languages_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_languages_update() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_media_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_media_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_media_update() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO service_role;\n\nREVOKE ALL ON FUNCTION public.handle_new_user() FROM PUBLIC;\n\nGRANT ALL ON FUNCTION public.handle_new_user() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_new_user() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_new_user() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_pages_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_pages_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_pages_update() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_posts_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_posts_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_posts_update() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO service_role;\n\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO anon;\n\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO authenticated;\n\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO service_role;\n\nGRANT ALL ON FUNCTION public.is_admin() TO anon;\n\nGRANT ALL ON FUNCTION public.is_admin() TO authenticated;\n\nGRANT ALL ON FUNCTION public.is_admin() TO service_role;\n\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO anon;\n\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO authenticated;\n\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO service_role;\n\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO anon;\n\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO authenticated;\n\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO service_role;\n\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO anon;\n\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO authenticated;\n\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO service_role;\n\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO anon;\n\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO authenticated;\n\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO service_role;\n\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO anon;\n\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO authenticated;\n\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO service_role;\n\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO anon;\n\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO authenticated;\n\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO service_role;\n\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO anon;\n\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO authenticated;\n\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO service_role;\n\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO anon;\n\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO authenticated;\n\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO service_role;\n\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO anon;\n\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO authenticated;\n\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO service_role;\n\nGRANT ALL ON FUNCTION public.update_product_ratings() TO anon;\n\nGRANT ALL ON FUNCTION public.update_product_ratings() TO authenticated;\n\nGRANT ALL ON FUNCTION public.update_product_ratings() TO service_role;\n\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO anon;\n\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO authenticated;\n\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO service_role;\n\nGRANT ALL ON TABLE public.blocks TO anon;\n\nGRANT ALL ON TABLE public.blocks TO authenticated;\n\nGRANT ALL ON TABLE public.blocks TO service_role;\n\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.categories TO anon;\n\nGRANT ALL ON TABLE public.categories TO authenticated;\n\nGRANT ALL ON TABLE public.categories TO service_role;\n\nGRANT ALL ON TABLE public.cms_interactions TO anon;\n\nGRANT ALL ON TABLE public.cms_interactions TO authenticated;\n\nGRANT ALL ON TABLE public.cms_interactions TO service_role;\n\nGRANT ALL ON TABLE public.content_drafts TO anon;\n\nGRANT ALL ON TABLE public.content_drafts TO authenticated;\n\nGRANT ALL ON TABLE public.content_drafts TO service_role;\n\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO anon;\n\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO authenticated;\n\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO service_role;\n\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO anon;\n\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO authenticated;\n\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO service_role;\n\nGRANT ALL ON TABLE public.coupon_products TO anon;\n\nGRANT ALL ON TABLE public.coupon_products TO authenticated;\n\nGRANT ALL ON TABLE public.coupon_products TO service_role;\n\nGRANT ALL ON TABLE public.coupon_redemptions TO anon;\n\nGRANT ALL ON TABLE public.coupon_redemptions TO authenticated;\n\nGRANT ALL ON TABLE public.coupon_redemptions TO service_role;\n\nGRANT ALL ON TABLE public.coupons TO anon;\n\nGRANT ALL ON TABLE public.coupons TO authenticated;\n\nGRANT ALL ON TABLE public.coupons TO service_role;\n\nGRANT ALL ON TABLE public.currencies TO anon;\n\nGRANT ALL ON TABLE public.currencies TO authenticated;\n\nGRANT ALL ON TABLE public.currencies TO service_role;\n\nGRANT ALL ON TABLE public.email_2fa_challenges TO anon;\n\nGRANT ALL ON TABLE public.email_2fa_challenges TO authenticated;\n\nGRANT ALL ON TABLE public.email_2fa_challenges TO service_role;\n\nGRANT ALL ON TABLE public.freemius_plans TO anon;\n\nGRANT ALL ON TABLE public.freemius_plans TO authenticated;\n\nGRANT ALL ON TABLE public.freemius_plans TO service_role;\n\nGRANT ALL ON TABLE public.freemius_pricing TO anon;\n\nGRANT ALL ON TABLE public.freemius_pricing TO authenticated;\n\nGRANT ALL ON TABLE public.freemius_pricing TO service_role;\n\nGRANT ALL ON TABLE public.inventory_items TO anon;\n\nGRANT ALL ON TABLE public.inventory_items TO authenticated;\n\nGRANT ALL ON TABLE public.inventory_items TO service_role;\n\nGRANT ALL ON TABLE public.languages TO anon;\n\nGRANT ALL ON TABLE public.languages TO authenticated;\n\nGRANT ALL ON TABLE public.languages TO service_role;\n\nGRANT ALL ON SEQUENCE public.languages_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.languages_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.languages_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.logos TO anon;\n\nGRANT ALL ON TABLE public.logos TO authenticated;\n\nGRANT ALL ON TABLE public.logos TO service_role;\n\nGRANT ALL ON TABLE public.media TO anon;\n\nGRANT ALL ON TABLE public.media TO authenticated;\n\nGRANT ALL ON TABLE public.media TO service_role;\n\nGRANT ALL ON TABLE public.navigation_items TO anon;\n\nGRANT ALL ON TABLE public.navigation_items TO authenticated;\n\nGRANT ALL ON TABLE public.navigation_items TO service_role;\n\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO service_role;\n\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO service_role;\n\nGRANT ALL ON TABLE public.order_items TO anon;\n\nGRANT ALL ON TABLE public.order_items TO authenticated;\n\nGRANT ALL ON TABLE public.order_items TO service_role;\n\nGRANT ALL ON TABLE public.orders TO anon;\n\nGRANT ALL ON TABLE public.orders TO authenticated;\n\nGRANT ALL ON TABLE public.orders TO service_role;\n\nGRANT ALL ON TABLE public.package_activations TO anon;\n\nGRANT ALL ON TABLE public.package_activations TO authenticated;\n\nGRANT ALL ON TABLE public.package_activations TO service_role;\n\nGRANT ALL ON TABLE public.page_revisions TO anon;\n\nGRANT ALL ON TABLE public.page_revisions TO authenticated;\n\nGRANT ALL ON TABLE public.page_revisions TO service_role;\n\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.pages TO anon;\n\nGRANT ALL ON TABLE public.pages TO authenticated;\n\nGRANT ALL ON TABLE public.pages TO service_role;\n\nGRANT ALL ON SEQUENCE public.pages_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.pages_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.pages_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.post_revisions TO anon;\n\nGRANT ALL ON TABLE public.post_revisions TO authenticated;\n\nGRANT ALL ON TABLE public.post_revisions TO service_role;\n\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.posts TO anon;\n\nGRANT ALL ON TABLE public.posts TO authenticated;\n\nGRANT ALL ON TABLE public.posts TO service_role;\n\nGRANT ALL ON SEQUENCE public.posts_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.posts_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.posts_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.privacy_consent_logs TO anon;\n\nGRANT ALL ON TABLE public.privacy_consent_logs TO authenticated;\n\nGRANT ALL ON TABLE public.privacy_consent_logs TO service_role;\n\nGRANT ALL ON TABLE public.product_attribute_terms TO anon;\n\nGRANT ALL ON TABLE public.product_attribute_terms TO authenticated;\n\nGRANT ALL ON TABLE public.product_attribute_terms TO service_role;\n\nGRANT ALL ON TABLE public.product_attributes TO anon;\n\nGRANT ALL ON TABLE public.product_attributes TO authenticated;\n\nGRANT ALL ON TABLE public.product_attributes TO service_role;\n\nGRANT ALL ON TABLE public.product_categories TO anon;\n\nGRANT ALL ON TABLE public.product_categories TO authenticated;\n\nGRANT ALL ON TABLE public.product_categories TO service_role;\n\nGRANT ALL ON TABLE public.product_drafts TO anon;\n\nGRANT ALL ON TABLE public.product_drafts TO authenticated;\n\nGRANT ALL ON TABLE public.product_drafts TO service_role;\n\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO anon;\n\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO authenticated;\n\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO service_role;\n\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO anon;\n\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO authenticated;\n\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO service_role;\n\nGRANT ALL ON TABLE public.product_media TO anon;\n\nGRANT ALL ON TABLE public.product_media TO authenticated;\n\nGRANT ALL ON TABLE public.product_media TO service_role;\n\nGRANT ALL ON TABLE public.product_variants TO anon;\n\nGRANT ALL ON TABLE public.product_variants TO authenticated;\n\nGRANT ALL ON TABLE public.product_variants TO service_role;\n\nGRANT ALL ON TABLE public.products TO anon;\n\nGRANT ALL ON TABLE public.products TO authenticated;\n\nGRANT ALL ON TABLE public.products TO service_role;\n\nGRANT ALL ON TABLE public.profiles TO anon;\n\nGRANT ALL ON TABLE public.profiles TO authenticated;\n\nGRANT ALL ON TABLE public.profiles TO service_role;\n\nGRANT ALL ON TABLE public.shipping_zone_locations TO anon;\n\nGRANT ALL ON TABLE public.shipping_zone_locations TO authenticated;\n\nGRANT ALL ON TABLE public.shipping_zone_locations TO service_role;\n\nGRANT ALL ON TABLE public.shipping_zone_methods TO anon;\n\nGRANT ALL ON TABLE public.shipping_zone_methods TO authenticated;\n\nGRANT ALL ON TABLE public.shipping_zone_methods TO service_role;\n\nGRANT ALL ON TABLE public.shipping_zones TO anon;\n\nGRANT ALL ON TABLE public.shipping_zones TO authenticated;\n\nGRANT ALL ON TABLE public.shipping_zones TO service_role;\n\nGRANT ALL ON TABLE public.site_settings TO anon;\n\nGRANT ALL ON TABLE public.site_settings TO authenticated;\n\nGRANT ALL ON TABLE public.site_settings TO service_role;\n\nGRANT ALL ON TABLE public.system_alerts TO anon;\n\nGRANT ALL ON TABLE public.system_alerts TO authenticated;\n\nGRANT ALL ON TABLE public.system_alerts TO service_role;\n\nGRANT ALL ON TABLE public.system_configuration TO anon;\n\nGRANT ALL ON TABLE public.system_configuration TO authenticated;\n\nGRANT ALL ON TABLE public.system_configuration TO service_role;\n\nGRANT ALL ON TABLE public.tax_rates TO anon;\n\nGRANT ALL ON TABLE public.tax_rates TO authenticated;\n\nGRANT ALL ON TABLE public.tax_rates TO service_role;\n\nGRANT ALL ON TABLE public.translations TO anon;\n\nGRANT ALL ON TABLE public.translations TO authenticated;\n\nGRANT ALL ON TABLE public.translations TO service_role;\n\nGRANT ALL ON TABLE public.ucp_cart_sessions TO anon;\n\nGRANT ALL ON TABLE public.ucp_cart_sessions TO authenticated;\n\nGRANT ALL ON TABLE public.ucp_cart_sessions TO service_role;\n\nGRANT ALL ON TABLE public.user_addresses TO anon;\n\nGRANT ALL ON TABLE public.user_addresses TO authenticated;\n\nGRANT ALL ON TABLE public.user_addresses TO service_role;\n\nGRANT ALL ON TABLE public.user_security_settings TO anon;\n\nGRANT ALL ON TABLE public.user_security_settings TO authenticated;\n\nGRANT ALL ON TABLE public.user_security_settings TO service_role;\n\nGRANT ALL ON TABLE public.user_trusted_devices TO anon;\n\nGRANT ALL ON TABLE public.user_trusted_devices TO authenticated;\n\nGRANT ALL ON TABLE public.user_trusted_devices TO service_role;\n\nGRANT ALL ON TABLE public.variant_attribute_mapping TO anon;\n\nGRANT ALL ON TABLE public.variant_attribute_mapping TO authenticated;\n\nGRANT ALL ON TABLE public.variant_attribute_mapping TO service_role;\n\n-- Re-attached: trigger lives on auth.users, which a public-only pg_dump omits (see migration 005).\nDROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;\nCREATE TRIGGER on_auth_user_created\n AFTER INSERT ON auth.users\n FOR EACH ROW\n EXECUTE FUNCTION public.handle_new_user();\nREVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;\n"
31
31
  },
32
32
  {
33
33
  "version": "00000000000003",
@@ -37,12 +37,12 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
37
37
  {
38
38
  "version": "00000000000004",
39
39
  "name": "00000000000004_default_logo_email_safe.sql",
40
- "sql": "-- Swap the seeded DEFAULT site logo from the WebP asset to a bundled PNG.\r\n--\r\n-- WebP (and AVIF) don't render in Outlook and several other email clients, so the default\r\n-- logo shown in transactional emails was a broken image out of the box. The PNG is a\r\n-- /public bundled asset (registered in resolveMediaUrl's BUNDLED_PUBLIC_MEDIA_KEYS), so it\r\n-- resolves to `<site>/images/nextblock-logo-button-tiny.png` and renders everywhere.\r\n--\r\n-- Forward-only, data-only (no schema change), idempotent, and GUARDED so it never\r\n-- overrides a logo an operator has since chosen: it only repoints the seeded default logo\r\n-- row while that row still points at the seeded WebP media.\r\n\r\nINSERT INTO public.media (\r\n id, uploader_id, file_name, object_key, file_type, size_bytes, description,\r\n width, height, blur_data_url, variants, file_path, folder, created_at, updated_at\r\n) VALUES (\r\n 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', NULL, 'nextblock-logo-button-tiny.png',\r\n 'images/nextblock-logo-button-tiny.png', 'image/png', 10000, 'Default site logo',\r\n NULL, NULL, NULL, NULL, NULL, NULL, now(), now()\r\n) ON CONFLICT DO NOTHING;\r\n\r\nUPDATE public.logos\r\nSET media_id = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'\r\nWHERE id = 'd45ef03d-27fc-4099-8b46-1cdf82d658d2'\r\n AND media_id = 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38';\r\n"
40
+ "sql": "-- Swap the seeded DEFAULT site logo from the WebP asset to a bundled PNG.\n--\n-- WebP (and AVIF) don't render in Outlook and several other email clients, so the default\n-- logo shown in transactional emails was a broken image out of the box. The PNG is a\n-- /public bundled asset (registered in resolveMediaUrl's BUNDLED_PUBLIC_MEDIA_KEYS), so it\n-- resolves to `<site>/images/nextblock-logo-button-tiny.png` and renders everywhere.\n--\n-- Forward-only, data-only (no schema change), idempotent, and GUARDED so it never\n-- overrides a logo an operator has since chosen: it only repoints the seeded default logo\n-- row while that row still points at the seeded WebP media.\n\nINSERT INTO public.media (\n id, uploader_id, file_name, object_key, file_type, size_bytes, description,\n width, height, blur_data_url, variants, file_path, folder, created_at, updated_at\n) VALUES (\n 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', NULL, 'nextblock-logo-button-tiny.png',\n 'images/nextblock-logo-button-tiny.png', 'image/png', 10000, 'Default site logo',\n NULL, NULL, NULL, NULL, NULL, NULL, now(), now()\n) ON CONFLICT DO NOTHING;\n\nUPDATE public.logos\nSET media_id = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'\nWHERE id = 'd45ef03d-27fc-4099-8b46-1cdf82d658d2'\n AND media_id = 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38';\n"
41
41
  },
42
42
  {
43
43
  "version": "00000000000005",
44
44
  "name": "00000000000005_add_custom_canonical.sql",
45
- "sql": "-- Add a nullable `custom_canonical` column to pages, posts and products.\r\n--\r\n-- This is the per-content manual canonical override used by each route's\r\n-- generateMetadata(): when set, it wins over the default self-referencing\r\n-- `<site_url>/<slug>` canonical (see app/lib/seo.ts buildCanonicalUrl). NULL/empty\r\n-- keeps the self-referencing default, so existing rows are unaffected.\r\n--\r\n-- Forward-only and idempotent (ADD COLUMN IF NOT EXISTS). No backfill: a NULL value\r\n-- is the \"use the default canonical\" sentinel.\r\n\r\nALTER TABLE public.pages ADD COLUMN IF NOT EXISTS custom_canonical text;\r\nALTER TABLE public.posts ADD COLUMN IF NOT EXISTS custom_canonical text;\r\nALTER TABLE public.products ADD COLUMN IF NOT EXISTS custom_canonical text;\r\n"
45
+ "sql": "-- Add a nullable `custom_canonical` column to pages, posts and products.\n--\n-- This is the per-content manual canonical override used by each route's\n-- generateMetadata(): when set, it wins over the default self-referencing\n-- `<site_url>/<slug>` canonical (see app/lib/seo.ts buildCanonicalUrl). NULL/empty\n-- keeps the self-referencing default, so existing rows are unaffected.\n--\n-- Forward-only and idempotent (ADD COLUMN IF NOT EXISTS). No backfill: a NULL value\n-- is the \"use the default canonical\" sentinel.\n\nALTER TABLE public.pages ADD COLUMN IF NOT EXISTS custom_canonical text;\nALTER TABLE public.posts ADD COLUMN IF NOT EXISTS custom_canonical text;\nALTER TABLE public.products ADD COLUMN IF NOT EXISTS custom_canonical text;\n"
46
46
  },
47
47
  {
48
48
  "version": "00000000000006",
@@ -57,7 +57,7 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
57
57
  {
58
58
  "version": "00000000000008",
59
59
  "name": "00000000000008_setup_article_reorder.sql",
60
- "sql": "-- 00000000000008_setup_article_reorder.sql\r\n-- Reorders and rewrites the \"How to Install NextBlock\" setup guide on BOTH language\r\n-- twins: EN (post slug 'how-to-setup-nextblock', post_id=3 / block id=40) and FR\r\n-- (post slug 'comment-configurer-nextblock', post_id=4 / block id=41), both seeded in\r\n-- the baseline seed 00000000000003. New path order, presented as a complexity ladder\r\n-- (simplest -> most hands-on): (1) One-click Vercel, (2) npm create nextblock -> Docker\r\n-- (100% local), (3) npm create nextblock -> your own Supabase + Cloudflare R2, (4) git\r\n-- clone the monorepo (with a note that Docker works there too). The Vercel section now\r\n-- tells users to make the new GitHub repo Public so automatic updates work.\r\n--\r\n-- Forward-only and append-only: the baseline 003 is already applied everywhere and\r\n-- never replays, so this ships the new copy as an idempotent UPDATE (re-running sets\r\n-- identical content). Data-only -- no schema change. blocks.content is JSONB; written\r\n-- with PostgreSQL dollar-quoting to avoid single-quote doubling (same style as 006/007).\r\n\r\nUPDATE public.blocks\r\n SET content = $nben$\r\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock is an open-source, AI-native Next.js CMS built on Supabase — and installing it no longer involves config files, terminal wizards, or manual SQL. Below are four ways to get running, <strong>ordered from the simplest to the most hands-on</strong>: the first is a single click, the last is the full source for people who want to help build NextBlock. They all end in the same place — a browser <strong>setup wizard</strong> that connects your database, configures media storage, and creates your admin account for you.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Simplest</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Most control</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Easiest &middot; one click</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Deploy on Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A live production site with a managed database. No terminal, no accounts to wire up, nothing to copy.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Easy &middot; 100% local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>One prompt scaffolds a project and boots the whole stack — database, auth, storage, CMS — on your machine. No cloud accounts.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermediate &middot; your own cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Scaffold a standalone app on your own managed Supabase project and Cloudflare R2, ready to deploy anywhere.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Advanced &middot; for contributors</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Clone the repository</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Run the complete monorepo — the CMS, every package, and the docs. For people who want to help build NextBlock (Docker works here too).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Not sure where to start? Work down the list — pick the first option whose requirements you already have. Most people should start with <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='NextBlock CMS platform overview showing the block editor, CMS dashboard, and integrations included with every installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Whichever path you choose, you get the same block editor, the same CMS, and the same database schema.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1: One-Click Deploy on Vercel</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Step 1 &middot; Easiest</span><span class='text-slate-500 dark:text-slate-400'>Best for a live site with the least possible effort — no terminal, no accounts to wire up.</span></p>\\n<p>The fastest way to get a production NextBlock site. One button creates your own copy of NextBlock on GitHub, provisions a managed Supabase database, and deploys the site — you never open a terminal or copy a single key.</p>\\n<ol class='space-y-2'>\\n <li><strong>Click Deploy to Vercel</strong> and sign in — Vercel clones NextBlock into a new repository you own.</li>\\n <li><strong>Name the repository — and make it Public.</strong> On Vercel&rsquo;s first step you choose the repository name; set its visibility to <strong>Public</strong> rather than Private. A public repo is what unlocks one-click automatic updates later, so it&rsquo;s the recommended choice.</li>\\n <li><strong>Create the Supabase database</strong> when prompted: pick a name and a region. Vercel connects it to the project and injects the keys before the first build.</li>\\n <li><strong>Open your new site</strong> once the build finishes. Every fresh instance takes you straight to the setup wizard.</li>\\n <li><strong>Create your administrator account.</strong> It is confirmed instantly — no verification email — and you land in the CMS dashboard.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy to Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommended &middot; make the repo public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>When Vercel asks you to name the new repository, choose <strong>Public</strong>. It costs nothing and it&rsquo;s what lets the dashboard&rsquo;s one-click <strong>Connect GitHub</strong> step install a daily workflow that keeps your site synced with the latest NextBlock release. You can still switch a private repo to public later in GitHub — but starting public is the smoothest path to automatic updates.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zero configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>There are no environment variables to fill in. Media storage automatically uses your connected Supabase project, security secrets are derived for you, and database migrations run automatically on every production build. Adding a custom domain later? Set <code>NEXT_PUBLIC_URL</code> in your Vercel project and redeploy.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2: npm create nextblock &rarr; Docker (100% Local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Step 2 &middot; Easy</span><span class='text-slate-500 dark:text-slate-400'>Best for trying everything on your own machine with zero cloud accounts. Needs Docker Desktop.</span></p>\\n<p>Run NextBlock entirely on your own machine with no cloud accounts at all. The CLI scaffolds a standalone Next.js app and then boots the full stack in Docker for you: Supabase&rsquo;s Postgres and auth engines, a PostgREST API behind a Kong gateway, S3-compatible MinIO storage, and the CMS itself — ideal for evaluations, air-gapped environments, and anyone who wants complete data ownership.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a> (it includes npm) and <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, and make sure Docker Desktop is running.</p>\\n<pre><code>npm create nextblock@latest my-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choose the hosting profile.</strong> At the first prompt, pick <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, then confirm that Docker Desktop is installed and running.</li>\\n <li><strong>Let it run.</strong> The CLI copies the template, installs dependencies, generates secure keys, and boots the whole stack with a single Docker command — no questions asked. The first run pulls images and builds the app, so give it a few minutes; every database migration is applied automatically as the stack comes up.</li>\\n <li><strong>Open <code>http://localhost:3000</code></strong> — it redirects to the setup wizard. Because the database and MinIO storage are already wired up, the connection and media-storage steps are skipped: the only thing left is creating your administrator (confirmed instantly, no email required). You land in the CMS dashboard.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Day-2 commands</p>\\n <pre class='mt-4 mb-0'><code># rebuild and restart the stack\\nnpm run docker:up\\n\\n# stop the stack (your data persists in Docker volumes)\\nnpm run docker:down\\n\\n# follow the application logs\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Docker mode is offered only in the interactive prompt — don&rsquo;t pass <code>--yes</code>, which forces the managed-cloud path below.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3: npm create nextblock &rarr; Your Own Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Step 3 &middot; Intermediate</span><span class='text-slate-500 dark:text-slate-400'>Best for building your own site on managed cloud. Needs a Supabase project and an R2 bucket.</span></p>\\n<p>The best starting point for building your own site on managed cloud. The CLI scaffolds a standalone Next.js application with NextBlock already wired in — no monorepo, no workspace tooling — pointed at a Supabase project and Cloudflare R2 bucket you control, and it deploys anywhere Next.js runs.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a>, create a free project at <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, and set up a <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> bucket for your images and files.</p>\\n<pre><code>npm create nextblock@latest my-site\\ncd my-site\\nnpm run dev</code></pre>\\n<p>At the first prompt, choose <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> and name your project. Then open <code>http://localhost:3000/setup</code> and let the wizard take over:</p>\\n<ol class='space-y-2'>\\n <li><strong>Connect Supabase</strong> — paste your project URL, publishable (anon) key, secret (service role) key, and a personal access token so the wizard can apply the database schema for you.</li>\\n <li><strong>Add Cloudflare R2</strong> — enter your R2 account ID, bucket name, access key ID, secret access key, and the bucket&rsquo;s public URL for serving images and files.</li>\\n <li><strong>Create your administrator</strong> — the wizard applies every migration, generates the app secrets, writes <code>.env.local</code>, creates your confirmed admin account, and signs you in. Restart <code>npm run dev</code> once afterwards so the fresh environment is baked into the app.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Premium modules</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Need a store? One command adds products, checkout, orders, and coupons — license-gated and ready when you are: <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4: Clone the Repository</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Step 4 &middot; Advanced</span><span class='text-slate-500 dark:text-slate-400'>Best for contributors and teams who want to customize the platform itself. Needs Node.js and git.</span></p>\\n<p>Run the full Nx monorepo: the CMS application, every shared package, the CLI source, and the documentation. This is the path for people who want to take part in the project — contributors, plugin authors, and teams that customize the platform itself.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Open <code>http://localhost:4200</code> — a fresh install redirects every page to <code>/setup</code>, where the same three-step wizard connects Supabase, configures storage, and creates your admin. It validates your keys, writes <code>.env.local</code> with generated secrets, and applies all migrations over the Supabase Management API — no Supabase CLI required.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Prefer to stay local? Use Docker here too</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>You don&rsquo;t need cloud accounts to develop against the monorepo. With Docker Desktop running, one command boots the same self-hosted stack as Option 2 — Postgres, auth, storage, and the CMS — straight from your clone:</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>When it finishes, open <code>http://localhost:3000</code> and create your administrator. Note the monorepo has no <code>npm run dev</code> — use <code>npx nx serve nextblock</code> (port 4200) for the cloud path.</p>\\n</div>\\n\\n<h2 id='after-install'>After You Install: Your First 10 Minutes</h2>\\n<p>Every path drops you at <code>/cms/dashboard</code>, signed in as the first administrator. A built-in onboarding checklist walks you through the rest:</p>\\n<ul class='space-y-2'>\\n <li><strong>Add your branding</strong> — upload your logo and set the site title.</li>\\n <li><strong>Set your footer</strong> — copyright line and footer navigation.</li>\\n <li><strong>Configure email (SMTP)</strong> — under Settings, so password resets and invitations can send.</li>\\n <li><strong>Optional extras</strong> — connect analytics, enable bot protection, and (on Vercel) turn on automatic updates.</li>\\n</ul>\\n<p>From there, see how the platform fits together in <a href='/article/how-nextblock-works'>How NextBlock Works</a>, add a storefront with the <a href='/article/nextblock-commerce-guide'>Commerce guide</a>, or meet your AI copilot in the <a href='/article/nextblock-cortex-ai-guide'>Cortex AI guide</a>.</p>\\n\\n<h2 id='faq'>Installation FAQ</h2>\\n<h3>What do I need installed?</h3>\\n<p>Nothing for the Vercel path — it runs entirely in the browser. For <code>npm create nextblock</code> in Docker mode: <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> and Docker Desktop. For the managed-cloud path: Node.js 20+ plus a Supabase project and a Cloudflare R2 bucket. For the cloned repository: Node.js 20+ and git (add Docker Desktop if you want to run the local stack).</p>\\n<h3>Is NextBlock free?</h3>\\n<p>Yes — the core of NextBlock is a 100% free, open-source CMS (AGPL). Premium packages such as e-commerce and Cortex AI are optional and activate with a license key. Both Vercel and Supabase offer free tiers, so a starter site can run at no cost.</p>\\n<h3>Do I need a Supabase account?</h3>\\n<p>On Vercel, the database is created for you during the deploy. For the managed-cloud <code>npm create nextblock</code> path and the cloned repository you bring a free Supabase project. With Docker — whether through the CLI&rsquo;s Docker mode or <code>npm run docker:setup</code> in the clone — you need no cloud accounts at all.</p>\\n<h3>Do I have to run migrations or SQL by hand?</h3>\\n<p>No. The setup wizard, the Vercel build, and the Docker stack all apply the database schema automatically — and re-running is always safe.</p>\\n<h3>Can I switch paths later?</h3>\\n<p>Yes. Every path runs the same application and the same database schema, so you can prototype locally with Docker today and deploy to Vercel tomorrow. NextBlock deploys like any standard Next.js app.</p>\\n<h3>How do I update NextBlock?</h3>\\n<p>On Vercel, the Connect GitHub onboarding step enables a daily automatic sync with upstream (this needs your repository to be public). On a cloned repository, <code>git pull</code>, run <code>npm run db:migrate</code>, then restart (on production builds, pending migrations apply automatically). With Docker, pull the latest code and run <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Ready to launch?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Pick your path above, or jump straight to the fastest one.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy on Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\\n </div>\\n</div>\"}\r\n$nben$::jsonb,\r\n updated_at = now()\r\n WHERE post_id = 3\r\n AND block_type = 'text';\r\n\r\nUPDATE public.blocks\r\n SET content = $nbfr$\r\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Voici quatre façons de démarrer, <strong>classées de la plus simple à la plus technique</strong> : la première tient en un clic, la dernière donne le code source complet pour celles et ceux qui veulent participer à NextBlock. Elles aboutissent toutes au même endroit — un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Le plus simple</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Le plus de contrôle</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Le plus simple &middot; un clic</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Déployer sur Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un site de production en ligne avec une base de données gérée. Pas de terminal, aucun compte à configurer, rien à copier.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Facile &middot; 100 % local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un seul prompt génère un projet et démarre toute la pile — base de données, auth, stockage, CMS — sur votre machine. Aucun compte cloud.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermédiaire &middot; votre propre cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Générez une app autonome sur votre propre projet Supabase géré et Cloudflare R2, prête à déployer partout.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Avancé &middot; pour les contributeurs</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Cloner le dépôt</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Pour celles et ceux qui veulent aider à construire NextBlock (Docker fonctionne ici aussi).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Vous ne savez pas par où commencer ? Descendez la liste — choisissez la première option dont vous avez déjà les prérequis. La plupart des gens devraient commencer par <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1 : Déploiement Vercel en un clic</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Étape 1 &middot; Le plus simple</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour un site en ligne avec le minimum d'effort — pas de terminal, aucun compte à configurer.</span></p>\\n<p>Le moyen le plus rapide d'obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\\n<ol class='space-y-2'>\\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\\n <li><strong>Nommez le dépôt — et rendez-le Public.</strong> À la première étape sur Vercel, vous choisissez le nom du dépôt ; réglez sa visibilité sur <strong>Public</strong> plutôt que Privé. Un dépôt public est ce qui débloque les mises à jour automatiques en un clic plus tard — c'est donc le choix recommandé.</li>\\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l'assistant de configuration.</li>\\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommandé &middot; rendez le dépôt public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand Vercel vous demande de nommer le nouveau dépôt, choisissez <strong>Public</strong>. C'est gratuit, et c'est ce qui permet à l'étape <strong>Connect GitHub</strong> du tableau de bord d'installer un workflow quotidien qui garde votre site synchronisé avec la dernière version de NextBlock. Vous pourrez toujours passer un dépôt privé en public plus tard dans GitHub — mais démarrer en public est le chemin le plus simple vers les mises à jour automatiques.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zéro configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Aucune variable d'environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s'exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2 : npm create nextblock &rarr; Docker (100 % local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Étape 2 &middot; Facile</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour tout essayer sur votre machine sans aucun compte cloud. Nécessite Docker Desktop.</span></p>\\n<p>Faites tourner NextBlock entièrement sur votre machine, sans aucun compte cloud. Le CLI génère une application Next.js autonome puis démarre toute la pile dans Docker pour vous : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a> (npm inclus) et <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, et assurez-vous que Docker Desktop est démarré.</p>\\n<pre><code>npm create nextblock@latest mon-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choisissez le profil d'hébergement.</strong> Au premier prompt, choisissez <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, puis confirmez que Docker Desktop est installé et démarré.</li>\\n <li><strong>Laissez faire.</strong> Le CLI copie le template, installe les dépendances, génère des clés sécurisées et démarre toute la pile avec une seule commande Docker — sans aucune question. Le premier lancement télécharge les images et construit l'app, comptez donc quelques minutes ; chaque migration de base de données est appliquée automatiquement au démarrage de la pile.</li>\\n <li><strong>Ouvrez <code>http://localhost:3000</code></strong> — vous êtes redirigé vers l'assistant de configuration. Comme la base de données et le stockage MinIO sont déjà connectés, les étapes de connexion et de stockage sont ignorées : il ne reste qu'à créer votre administrateur (confirmé instantanément, sans email). Vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Commandes du quotidien</p>\\n <pre class='mt-4 mb-0'><code># reconstruire et redémarrer la pile\\nnpm run docker:up\\n\\n# arrêter la pile (vos données persistent dans les volumes Docker)\\nnpm run docker:down\\n\\n# suivre les logs de l'application\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Le mode Docker n'est proposé que dans le prompt interactif — ne passez pas <code>--yes</code>, qui force le mode cloud géré ci-dessous.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3 : npm create nextblock &rarr; votre propre Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Étape 3 &middot; Intermédiaire</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour construire votre propre site sur du cloud géré. Nécessite un projet Supabase et un bucket R2.</span></p>\\n<p>Le meilleur point de départ pour construire votre propre site sur du cloud géré. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — reliée à un projet Supabase et un bucket Cloudflare R2 que vous contrôlez, et elle se déploie partout où Next.js tourne.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a>, créez un projet gratuit sur <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, et configurez un bucket <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> pour vos images et fichiers.</p>\\n<pre><code>npm create nextblock@latest mon-site\\ncd mon-site\\nnpm run dev</code></pre>\\n<p>Au premier prompt, choisissez <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> et nommez votre projet. Ouvrez ensuite <code>http://localhost:3000/setup</code> et laissez l'assistant faire le travail :</p>\\n<ol class='space-y-2'>\\n <li><strong>Connectez Supabase</strong> — collez l'URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d'accès personnel pour que l'assistant applique le schéma de base de données à votre place.</li>\\n <li><strong>Ajoutez Cloudflare R2</strong> — saisissez votre identifiant de compte R2, le nom du bucket, la clé d'accès (access key ID), la clé secrète et l'URL publique du bucket pour servir vos images et fichiers.</li>\\n <li><strong>Créez votre administrateur</strong> — l'assistant applique toutes les migrations, génère les secrets de l'application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l'application.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Modules premium</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Besoin d'une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l'êtes : <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4 : Cloner le dépôt</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Étape 4 &middot; Avancé</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour les contributeurs et les équipes qui veulent personnaliser la plateforme. Nécessite Node.js et git.</span></p>\\n<p>Faites tourner le monorepo Nx complet : l'application CMS, tous les packages partagés, le code du CLI et la documentation. C'est le chemin de celles et ceux qui veulent participer au projet — contributeurs, auteurs de plugins et équipes qui personnalisent la plateforme elle-même.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l'API de management Supabase — sans CLI Supabase.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Vous préférez rester local ? Docker fonctionne ici aussi</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Pas besoin de comptes cloud pour développer sur le monorepo. Avec Docker Desktop démarré, une seule commande lance la même pile auto-hébergée que l'option 2 — Postgres, auth, stockage et le CMS — directement depuis votre clone :</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand c'est terminé, ouvrez <code>http://localhost:3000</code> et créez votre administrateur. Notez que le monorepo n'a pas de <code>npm run dev</code> — utilisez <code>npx nx serve nextblock</code> (port 4200) pour le chemin cloud.</p>\\n</div>\\n\\n<h2 id='after-install'>Après l'installation : vos 10 premières minutes</h2>\\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\\n<ul class='space-y-2'>\\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\\n <li><strong>Configurez l'email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\\n <li><strong>Extras optionnels</strong> — connectez vos outils d'analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\\n</ul>\\n<p>Ensuite, découvrez comment la plateforme s'articule dans <a href='/article/comment-nextblock-fonctionne'>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href='/article/guide-commerce-nextblock'>guide Commerce</a>.</p>\\n\\n<h2 id='faq'>FAQ d'installation</h2>\\n<h3>Que dois-je installer ?</h3>\\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> en mode Docker : <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> et Docker Desktop. Pour le mode cloud géré : Node.js 20+ ainsi qu'un projet Supabase et un bucket Cloudflare R2. Pour le dépôt cloné : Node.js 20+ et git (ajoutez Docker Desktop si vous voulez faire tourner la pile locale).</p>\\n<h3>NextBlock est-il gratuit ?</h3>\\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l'e-commerce et Cortex AI sont optionnels et s'activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\\n<h3>Ai-je besoin d'un compte Supabase ?</h3>\\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour le chemin cloud géré <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker — via le mode Docker du CLI ou <code>npm run docker:setup</code> dans le clone — aucun compte cloud n'est nécessaire.</p>\\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\\n<p>Non. L'assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l'opération est toujours sans risque.</p>\\n<h3>Puis-je changer de chemin plus tard ?</h3>\\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd'hui et déployer sur Vercel demain. NextBlock se déploie comme n'importe quelle app Next.js.</p>\\n<h3>Comment mettre à jour NextBlock ?</h3>\\n<p>Sur Vercel, l'étape Connect GitHub de la checklist active une synchronisation quotidienne automatique (cela nécessite un dépôt public). Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s'appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Prêt à vous lancer ?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\\n </div>\\n</div>\"}\r\n$nbfr$::jsonb,\r\n updated_at = now()\r\n WHERE post_id = 4\r\n AND block_type = 'text';\r\n"
60
+ "sql": "-- 00000000000008_setup_article_reorder.sql\n-- Reorders and rewrites the \"How to Install NextBlock\" setup guide on BOTH language\n-- twins: EN (post slug 'how-to-setup-nextblock', post_id=3 / block id=40) and FR\n-- (post slug 'comment-configurer-nextblock', post_id=4 / block id=41), both seeded in\n-- the baseline seed 00000000000003. New path order, presented as a complexity ladder\n-- (simplest -> most hands-on): (1) One-click Vercel, (2) npm create nextblock -> Docker\n-- (100% local), (3) npm create nextblock -> your own Supabase + Cloudflare R2, (4) git\n-- clone the monorepo (with a note that Docker works there too). The Vercel section now\n-- tells users to make the new GitHub repo Public so automatic updates work.\n--\n-- Forward-only and append-only: the baseline 003 is already applied everywhere and\n-- never replays, so this ships the new copy as an idempotent UPDATE (re-running sets\n-- identical content). Data-only -- no schema change. blocks.content is JSONB; written\n-- with PostgreSQL dollar-quoting to avoid single-quote doubling (same style as 006/007).\n\nUPDATE public.blocks\n SET content = $nben$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock is an open-source, AI-native Next.js CMS built on Supabase — and installing it no longer involves config files, terminal wizards, or manual SQL. Below are four ways to get running, <strong>ordered from the simplest to the most hands-on</strong>: the first is a single click, the last is the full source for people who want to help build NextBlock. They all end in the same place — a browser <strong>setup wizard</strong> that connects your database, configures media storage, and creates your admin account for you.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Simplest</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Most control</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Easiest &middot; one click</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Deploy on Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A live production site with a managed database. No terminal, no accounts to wire up, nothing to copy.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Easy &middot; 100% local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>One prompt scaffolds a project and boots the whole stack — database, auth, storage, CMS — on your machine. No cloud accounts.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermediate &middot; your own cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Scaffold a standalone app on your own managed Supabase project and Cloudflare R2, ready to deploy anywhere.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Advanced &middot; for contributors</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Clone the repository</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Run the complete monorepo — the CMS, every package, and the docs. For people who want to help build NextBlock (Docker works here too).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Not sure where to start? Work down the list — pick the first option whose requirements you already have. Most people should start with <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='NextBlock CMS platform overview showing the block editor, CMS dashboard, and integrations included with every installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Whichever path you choose, you get the same block editor, the same CMS, and the same database schema.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1: One-Click Deploy on Vercel</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Step 1 &middot; Easiest</span><span class='text-slate-500 dark:text-slate-400'>Best for a live site with the least possible effort — no terminal, no accounts to wire up.</span></p>\\n<p>The fastest way to get a production NextBlock site. One button creates your own copy of NextBlock on GitHub, provisions a managed Supabase database, and deploys the site — you never open a terminal or copy a single key.</p>\\n<ol class='space-y-2'>\\n <li><strong>Click Deploy to Vercel</strong> and sign in — Vercel clones NextBlock into a new repository you own.</li>\\n <li><strong>Name the repository — and make it Public.</strong> On Vercel&rsquo;s first step you choose the repository name; set its visibility to <strong>Public</strong> rather than Private. A public repo is what unlocks one-click automatic updates later, so it&rsquo;s the recommended choice.</li>\\n <li><strong>Create the Supabase database</strong> when prompted: pick a name and a region. Vercel connects it to the project and injects the keys before the first build.</li>\\n <li><strong>Open your new site</strong> once the build finishes. Every fresh instance takes you straight to the setup wizard.</li>\\n <li><strong>Create your administrator account.</strong> It is confirmed instantly — no verification email — and you land in the CMS dashboard.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy to Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommended &middot; make the repo public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>When Vercel asks you to name the new repository, choose <strong>Public</strong>. It costs nothing and it&rsquo;s what lets the dashboard&rsquo;s one-click <strong>Connect GitHub</strong> step install a daily workflow that keeps your site synced with the latest NextBlock release. You can still switch a private repo to public later in GitHub — but starting public is the smoothest path to automatic updates.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zero configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>There are no environment variables to fill in. Media storage automatically uses your connected Supabase project, security secrets are derived for you, and database migrations run automatically on every production build. Adding a custom domain later? Set <code>NEXT_PUBLIC_URL</code> in your Vercel project and redeploy.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2: npm create nextblock &rarr; Docker (100% Local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Step 2 &middot; Easy</span><span class='text-slate-500 dark:text-slate-400'>Best for trying everything on your own machine with zero cloud accounts. Needs Docker Desktop.</span></p>\\n<p>Run NextBlock entirely on your own machine with no cloud accounts at all. The CLI scaffolds a standalone Next.js app and then boots the full stack in Docker for you: Supabase&rsquo;s Postgres and auth engines, a PostgREST API behind a Kong gateway, S3-compatible MinIO storage, and the CMS itself — ideal for evaluations, air-gapped environments, and anyone who wants complete data ownership.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a> (it includes npm) and <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, and make sure Docker Desktop is running.</p>\\n<pre><code>npm create nextblock@latest my-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choose the hosting profile.</strong> At the first prompt, pick <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, then confirm that Docker Desktop is installed and running.</li>\\n <li><strong>Let it run.</strong> The CLI copies the template, installs dependencies, generates secure keys, and boots the whole stack with a single Docker command — no questions asked. The first run pulls images and builds the app, so give it a few minutes; every database migration is applied automatically as the stack comes up.</li>\\n <li><strong>Open <code>http://localhost:3000</code></strong> — it redirects to the setup wizard. Because the database and MinIO storage are already wired up, the connection and media-storage steps are skipped: the only thing left is creating your administrator (confirmed instantly, no email required). You land in the CMS dashboard.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Day-2 commands</p>\\n <pre class='mt-4 mb-0'><code># rebuild and restart the stack\\nnpm run docker:up\\n\\n# stop the stack (your data persists in Docker volumes)\\nnpm run docker:down\\n\\n# follow the application logs\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Docker mode is offered only in the interactive prompt — don&rsquo;t pass <code>--yes</code>, which forces the managed-cloud path below.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3: npm create nextblock &rarr; Your Own Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Step 3 &middot; Intermediate</span><span class='text-slate-500 dark:text-slate-400'>Best for building your own site on managed cloud. Needs a Supabase project and an R2 bucket.</span></p>\\n<p>The best starting point for building your own site on managed cloud. The CLI scaffolds a standalone Next.js application with NextBlock already wired in — no monorepo, no workspace tooling — pointed at a Supabase project and Cloudflare R2 bucket you control, and it deploys anywhere Next.js runs.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a>, create a free project at <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, and set up a <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> bucket for your images and files.</p>\\n<pre><code>npm create nextblock@latest my-site\\ncd my-site\\nnpm run dev</code></pre>\\n<p>At the first prompt, choose <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> and name your project. Then open <code>http://localhost:3000/setup</code> and let the wizard take over:</p>\\n<ol class='space-y-2'>\\n <li><strong>Connect Supabase</strong> — paste your project URL, publishable (anon) key, secret (service role) key, and a personal access token so the wizard can apply the database schema for you.</li>\\n <li><strong>Add Cloudflare R2</strong> — enter your R2 account ID, bucket name, access key ID, secret access key, and the bucket&rsquo;s public URL for serving images and files.</li>\\n <li><strong>Create your administrator</strong> — the wizard applies every migration, generates the app secrets, writes <code>.env.local</code>, creates your confirmed admin account, and signs you in. Restart <code>npm run dev</code> once afterwards so the fresh environment is baked into the app.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Premium modules</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Need a store? One command adds products, checkout, orders, and coupons — license-gated and ready when you are: <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4: Clone the Repository</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Step 4 &middot; Advanced</span><span class='text-slate-500 dark:text-slate-400'>Best for contributors and teams who want to customize the platform itself. Needs Node.js and git.</span></p>\\n<p>Run the full Nx monorepo: the CMS application, every shared package, the CLI source, and the documentation. This is the path for people who want to take part in the project — contributors, plugin authors, and teams that customize the platform itself.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Open <code>http://localhost:4200</code> — a fresh install redirects every page to <code>/setup</code>, where the same three-step wizard connects Supabase, configures storage, and creates your admin. It validates your keys, writes <code>.env.local</code> with generated secrets, and applies all migrations over the Supabase Management API — no Supabase CLI required.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Prefer to stay local? Use Docker here too</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>You don&rsquo;t need cloud accounts to develop against the monorepo. With Docker Desktop running, one command boots the same self-hosted stack as Option 2 — Postgres, auth, storage, and the CMS — straight from your clone:</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>When it finishes, open <code>http://localhost:3000</code> and create your administrator. Note the monorepo has no <code>npm run dev</code> — use <code>npx nx serve nextblock</code> (port 4200) for the cloud path.</p>\\n</div>\\n\\n<h2 id='after-install'>After You Install: Your First 10 Minutes</h2>\\n<p>Every path drops you at <code>/cms/dashboard</code>, signed in as the first administrator. A built-in onboarding checklist walks you through the rest:</p>\\n<ul class='space-y-2'>\\n <li><strong>Add your branding</strong> — upload your logo and set the site title.</li>\\n <li><strong>Set your footer</strong> — copyright line and footer navigation.</li>\\n <li><strong>Configure email (SMTP)</strong> — under Settings, so password resets and invitations can send.</li>\\n <li><strong>Optional extras</strong> — connect analytics, enable bot protection, and (on Vercel) turn on automatic updates.</li>\\n</ul>\\n<p>From there, see how the platform fits together in <a href='/article/how-nextblock-works'>How NextBlock Works</a>, add a storefront with the <a href='/article/nextblock-commerce-guide'>Commerce guide</a>, or meet your AI copilot in the <a href='/article/nextblock-cortex-ai-guide'>Cortex AI guide</a>.</p>\\n\\n<h2 id='faq'>Installation FAQ</h2>\\n<h3>What do I need installed?</h3>\\n<p>Nothing for the Vercel path — it runs entirely in the browser. For <code>npm create nextblock</code> in Docker mode: <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> and Docker Desktop. For the managed-cloud path: Node.js 20+ plus a Supabase project and a Cloudflare R2 bucket. For the cloned repository: Node.js 20+ and git (add Docker Desktop if you want to run the local stack).</p>\\n<h3>Is NextBlock free?</h3>\\n<p>Yes — the core of NextBlock is a 100% free, open-source CMS (AGPL). Premium packages such as e-commerce and Cortex AI are optional and activate with a license key. Both Vercel and Supabase offer free tiers, so a starter site can run at no cost.</p>\\n<h3>Do I need a Supabase account?</h3>\\n<p>On Vercel, the database is created for you during the deploy. For the managed-cloud <code>npm create nextblock</code> path and the cloned repository you bring a free Supabase project. With Docker — whether through the CLI&rsquo;s Docker mode or <code>npm run docker:setup</code> in the clone — you need no cloud accounts at all.</p>\\n<h3>Do I have to run migrations or SQL by hand?</h3>\\n<p>No. The setup wizard, the Vercel build, and the Docker stack all apply the database schema automatically — and re-running is always safe.</p>\\n<h3>Can I switch paths later?</h3>\\n<p>Yes. Every path runs the same application and the same database schema, so you can prototype locally with Docker today and deploy to Vercel tomorrow. NextBlock deploys like any standard Next.js app.</p>\\n<h3>How do I update NextBlock?</h3>\\n<p>On Vercel, the Connect GitHub onboarding step enables a daily automatic sync with upstream (this needs your repository to be public). On a cloned repository, <code>git pull</code>, run <code>npm run db:migrate</code>, then restart (on production builds, pending migrations apply automatically). With Docker, pull the latest code and run <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Ready to launch?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Pick your path above, or jump straight to the fastest one.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy on Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\\n </div>\\n</div>\"}\n$nben$::jsonb,\n updated_at = now()\n WHERE post_id = 3\n AND block_type = 'text';\n\nUPDATE public.blocks\n SET content = $nbfr$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Voici quatre façons de démarrer, <strong>classées de la plus simple à la plus technique</strong> : la première tient en un clic, la dernière donne le code source complet pour celles et ceux qui veulent participer à NextBlock. Elles aboutissent toutes au même endroit — un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Le plus simple</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Le plus de contrôle</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Le plus simple &middot; un clic</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Déployer sur Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un site de production en ligne avec une base de données gérée. Pas de terminal, aucun compte à configurer, rien à copier.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Facile &middot; 100 % local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un seul prompt génère un projet et démarre toute la pile — base de données, auth, stockage, CMS — sur votre machine. Aucun compte cloud.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermédiaire &middot; votre propre cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Générez une app autonome sur votre propre projet Supabase géré et Cloudflare R2, prête à déployer partout.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Avancé &middot; pour les contributeurs</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Cloner le dépôt</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Pour celles et ceux qui veulent aider à construire NextBlock (Docker fonctionne ici aussi).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Vous ne savez pas par où commencer ? Descendez la liste — choisissez la première option dont vous avez déjà les prérequis. La plupart des gens devraient commencer par <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1 : Déploiement Vercel en un clic</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Étape 1 &middot; Le plus simple</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour un site en ligne avec le minimum d'effort — pas de terminal, aucun compte à configurer.</span></p>\\n<p>Le moyen le plus rapide d'obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\\n<ol class='space-y-2'>\\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\\n <li><strong>Nommez le dépôt — et rendez-le Public.</strong> À la première étape sur Vercel, vous choisissez le nom du dépôt ; réglez sa visibilité sur <strong>Public</strong> plutôt que Privé. Un dépôt public est ce qui débloque les mises à jour automatiques en un clic plus tard — c'est donc le choix recommandé.</li>\\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l'assistant de configuration.</li>\\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommandé &middot; rendez le dépôt public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand Vercel vous demande de nommer le nouveau dépôt, choisissez <strong>Public</strong>. C'est gratuit, et c'est ce qui permet à l'étape <strong>Connect GitHub</strong> du tableau de bord d'installer un workflow quotidien qui garde votre site synchronisé avec la dernière version de NextBlock. Vous pourrez toujours passer un dépôt privé en public plus tard dans GitHub — mais démarrer en public est le chemin le plus simple vers les mises à jour automatiques.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zéro configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Aucune variable d'environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s'exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2 : npm create nextblock &rarr; Docker (100 % local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Étape 2 &middot; Facile</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour tout essayer sur votre machine sans aucun compte cloud. Nécessite Docker Desktop.</span></p>\\n<p>Faites tourner NextBlock entièrement sur votre machine, sans aucun compte cloud. Le CLI génère une application Next.js autonome puis démarre toute la pile dans Docker pour vous : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a> (npm inclus) et <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, et assurez-vous que Docker Desktop est démarré.</p>\\n<pre><code>npm create nextblock@latest mon-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choisissez le profil d'hébergement.</strong> Au premier prompt, choisissez <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, puis confirmez que Docker Desktop est installé et démarré.</li>\\n <li><strong>Laissez faire.</strong> Le CLI copie le template, installe les dépendances, génère des clés sécurisées et démarre toute la pile avec une seule commande Docker — sans aucune question. Le premier lancement télécharge les images et construit l'app, comptez donc quelques minutes ; chaque migration de base de données est appliquée automatiquement au démarrage de la pile.</li>\\n <li><strong>Ouvrez <code>http://localhost:3000</code></strong> — vous êtes redirigé vers l'assistant de configuration. Comme la base de données et le stockage MinIO sont déjà connectés, les étapes de connexion et de stockage sont ignorées : il ne reste qu'à créer votre administrateur (confirmé instantanément, sans email). Vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Commandes du quotidien</p>\\n <pre class='mt-4 mb-0'><code># reconstruire et redémarrer la pile\\nnpm run docker:up\\n\\n# arrêter la pile (vos données persistent dans les volumes Docker)\\nnpm run docker:down\\n\\n# suivre les logs de l'application\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Le mode Docker n'est proposé que dans le prompt interactif — ne passez pas <code>--yes</code>, qui force le mode cloud géré ci-dessous.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3 : npm create nextblock &rarr; votre propre Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Étape 3 &middot; Intermédiaire</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour construire votre propre site sur du cloud géré. Nécessite un projet Supabase et un bucket R2.</span></p>\\n<p>Le meilleur point de départ pour construire votre propre site sur du cloud géré. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — reliée à un projet Supabase et un bucket Cloudflare R2 que vous contrôlez, et elle se déploie partout où Next.js tourne.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a>, créez un projet gratuit sur <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, et configurez un bucket <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> pour vos images et fichiers.</p>\\n<pre><code>npm create nextblock@latest mon-site\\ncd mon-site\\nnpm run dev</code></pre>\\n<p>Au premier prompt, choisissez <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> et nommez votre projet. Ouvrez ensuite <code>http://localhost:3000/setup</code> et laissez l'assistant faire le travail :</p>\\n<ol class='space-y-2'>\\n <li><strong>Connectez Supabase</strong> — collez l'URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d'accès personnel pour que l'assistant applique le schéma de base de données à votre place.</li>\\n <li><strong>Ajoutez Cloudflare R2</strong> — saisissez votre identifiant de compte R2, le nom du bucket, la clé d'accès (access key ID), la clé secrète et l'URL publique du bucket pour servir vos images et fichiers.</li>\\n <li><strong>Créez votre administrateur</strong> — l'assistant applique toutes les migrations, génère les secrets de l'application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l'application.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Modules premium</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Besoin d'une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l'êtes : <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4 : Cloner le dépôt</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Étape 4 &middot; Avancé</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour les contributeurs et les équipes qui veulent personnaliser la plateforme. Nécessite Node.js et git.</span></p>\\n<p>Faites tourner le monorepo Nx complet : l'application CMS, tous les packages partagés, le code du CLI et la documentation. C'est le chemin de celles et ceux qui veulent participer au projet — contributeurs, auteurs de plugins et équipes qui personnalisent la plateforme elle-même.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l'API de management Supabase — sans CLI Supabase.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Vous préférez rester local ? Docker fonctionne ici aussi</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Pas besoin de comptes cloud pour développer sur le monorepo. Avec Docker Desktop démarré, une seule commande lance la même pile auto-hébergée que l'option 2 — Postgres, auth, stockage et le CMS — directement depuis votre clone :</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand c'est terminé, ouvrez <code>http://localhost:3000</code> et créez votre administrateur. Notez que le monorepo n'a pas de <code>npm run dev</code> — utilisez <code>npx nx serve nextblock</code> (port 4200) pour le chemin cloud.</p>\\n</div>\\n\\n<h2 id='after-install'>Après l'installation : vos 10 premières minutes</h2>\\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\\n<ul class='space-y-2'>\\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\\n <li><strong>Configurez l'email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\\n <li><strong>Extras optionnels</strong> — connectez vos outils d'analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\\n</ul>\\n<p>Ensuite, découvrez comment la plateforme s'articule dans <a href='/article/comment-nextblock-fonctionne'>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href='/article/guide-commerce-nextblock'>guide Commerce</a>.</p>\\n\\n<h2 id='faq'>FAQ d'installation</h2>\\n<h3>Que dois-je installer ?</h3>\\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> en mode Docker : <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> et Docker Desktop. Pour le mode cloud géré : Node.js 20+ ainsi qu'un projet Supabase et un bucket Cloudflare R2. Pour le dépôt cloné : Node.js 20+ et git (ajoutez Docker Desktop si vous voulez faire tourner la pile locale).</p>\\n<h3>NextBlock est-il gratuit ?</h3>\\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l'e-commerce et Cortex AI sont optionnels et s'activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\\n<h3>Ai-je besoin d'un compte Supabase ?</h3>\\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour le chemin cloud géré <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker — via le mode Docker du CLI ou <code>npm run docker:setup</code> dans le clone — aucun compte cloud n'est nécessaire.</p>\\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\\n<p>Non. L'assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l'opération est toujours sans risque.</p>\\n<h3>Puis-je changer de chemin plus tard ?</h3>\\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd'hui et déployer sur Vercel demain. NextBlock se déploie comme n'importe quelle app Next.js.</p>\\n<h3>Comment mettre à jour NextBlock ?</h3>\\n<p>Sur Vercel, l'étape Connect GitHub de la checklist active une synchronisation quotidienne automatique (cela nécessite un dépôt public). Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s'appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Prêt à vous lancer ?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\\n </div>\\n</div>\"}\n$nbfr$::jsonb,\n updated_at = now()\n WHERE post_id = 4\n AND block_type = 'text';\n"
61
61
  },
62
62
  {
63
63
  "version": "00000000000009",
@@ -82,7 +82,7 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
82
82
  {
83
83
  "version": "00000000000013",
84
84
  "name": "00000000000013_youtube_nocookie_embeds.sql",
85
- "sql": "-- 00000000000013_youtube_nocookie_embeds.sql\r\n--\r\n-- Lighthouse Best Practices scored 96 instead of 100: the `inspector-issues` audit\r\n-- (binary, weight 1 of 27 -> 26/27 = 96) failed with a DevTools \"Cookie\" issue\r\n-- attributed to www.youtube.com/embed/71MyfoL4YVM. The seeded home-page hero and the\r\n-- seeded articles embed raw <iframe> markup pointing at www.youtube.com, whose player\r\n-- writes cookies on load.\r\n--\r\n-- The real fix is the render-time click-to-play facade added in\r\n-- apps/nextblock/components/media/YouTubeFacade.tsx (the nocookie host does NOT stop\r\n-- the player's document.cookie writes -- only not loading the player does). This\r\n-- migration is the data-hygiene half: it rewrites every stored embed to the\r\n-- privacy-enhanced host so any surface that still renders a raw iframe (published-lib\r\n-- renderers, CMS previews, exports, downstream installs on an older app build) is at\r\n-- least cookie-reduced, and so the stored content matches what we ship.\r\n--\r\n-- It also fixes the bogus Permissions Policy feature in the seeded allow attribute:\r\n-- 'accelerated-motion' is not a real feature (Chrome logs \"Unrecognized feature\");\r\n-- the canonical YouTube embed uses 'accelerometer'.\r\n--\r\n-- Append-only: 00000000000003_baseline_seed.sql is applied everywhere and is NOT edited.\r\n-- Scoped by CONTENT MATCH, never by block id -- ids drift on installs where the demo\r\n-- content was re-created (see the convention documented in 00000000000006:11-13 and\r\n-- used by 007/009). This is a pure host-substring swap, so it never clobbers a user's\r\n-- edited copy, it also repairs author-pasted embeds, and it is naturally idempotent\r\n-- (a second run matches zero rows) which matters because the nightly sandbox reset\r\n-- replays the whole migration chain from a truncated history.\r\n--\r\n-- No dollar-quoting is needed here: neither search nor replacement literal contains a\r\n-- single quote, and neither contains a JSON metacharacter, so the ::text round-trip\r\n-- always re-parses as valid JSON. Query strings (?si=...) are preserved.\r\n--\r\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts\r\n-- excludes any migration whose filename contains \"sandbox\".\r\n\r\n-- 1. Live page/post block content (the 4 seeded rows + any author-added embed).\r\nUPDATE public.blocks\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content IS NOT NULL\r\n AND (content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%');\r\n\r\n-- 2. Live Draft Mode snapshots. Publishing a pre-existing draft would otherwise write\r\n-- the cookie host straight back into public.blocks.\r\nUPDATE public.content_drafts\r\n SET blocks = replace(\r\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\r\n OR blocks::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.content_drafts\r\n SET meta = replace(\r\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\r\n OR meta::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.product_drafts\r\n SET blocks = replace(\r\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\r\n OR blocks::text LIKE '%accelerated-motion%';\r\n\r\n-- product_drafts.meta carries short_description / description_json.\r\nUPDATE public.product_drafts\r\n SET meta = replace(\r\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\r\n OR meta::text LIKE '%accelerated-motion%';\r\n\r\n-- 3. Revision history. apps/nextblock/app/cms/revisions/service.ts replays a snapshot\r\n-- verbatim, so one \"Restore version\" click would otherwise reintroduce the issue.\r\n-- URL-only rewrite; nothing else about the historical snapshot is touched.\r\nUPDATE public.page_revisions\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.post_revisions\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%';\r\n\r\n-- 4. Product rich text rendered on public /product/* routes.\r\nUPDATE public.products\r\n SET short_description = replace(\r\n replace(short_description, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )\r\n WHERE short_description IS NOT NULL\r\n AND (short_description LIKE '%www.youtube.com/embed/%'\r\n OR short_description LIKE '%accelerated-motion%');\r\n\r\nUPDATE public.products\r\n SET description_json = replace(\r\n replace(description_json::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE description_json IS NOT NULL\r\n AND (description_json::text LIKE '%www.youtube.com/embed/%'\r\n OR description_json::text LIKE '%accelerated-motion%');\r\n\r\n-- 5. Custom block definitions (rich-text defaults / emptyFallback markup rendered by\r\n-- DynamicLayoutEngine). Host-only swap, so the is_valid_custom_block_* CHECK\r\n-- constraints still hold. No-op on a fresh install.\r\nUPDATE public.custom_block_definitions\r\n SET fields = replace(\r\n replace(fields::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE fields::text LIKE '%www.youtube.com/embed/%'\r\n OR fields::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.custom_block_definitions\r\n SET layout_schema = replace(\r\n replace(layout_schema::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE layout_schema::text LIKE '%www.youtube.com/embed/%'\r\n OR layout_schema::text LIKE '%accelerated-motion%';\r\n"
85
+ "sql": "-- 00000000000013_youtube_nocookie_embeds.sql\n--\n-- Lighthouse Best Practices scored 96 instead of 100: the `inspector-issues` audit\n-- (binary, weight 1 of 27 -> 26/27 = 96) failed with a DevTools \"Cookie\" issue\n-- attributed to www.youtube.com/embed/71MyfoL4YVM. The seeded home-page hero and the\n-- seeded articles embed raw <iframe> markup pointing at www.youtube.com, whose player\n-- writes cookies on load.\n--\n-- The real fix is the render-time click-to-play facade added in\n-- apps/nextblock/components/media/YouTubeFacade.tsx (the nocookie host does NOT stop\n-- the player's document.cookie writes -- only not loading the player does). This\n-- migration is the data-hygiene half: it rewrites every stored embed to the\n-- privacy-enhanced host so any surface that still renders a raw iframe (published-lib\n-- renderers, CMS previews, exports, downstream installs on an older app build) is at\n-- least cookie-reduced, and so the stored content matches what we ship.\n--\n-- It also fixes the bogus Permissions Policy feature in the seeded allow attribute:\n-- 'accelerated-motion' is not a real feature (Chrome logs \"Unrecognized feature\");\n-- the canonical YouTube embed uses 'accelerometer'.\n--\n-- Append-only: 00000000000003_baseline_seed.sql is applied everywhere and is NOT edited.\n-- Scoped by CONTENT MATCH, never by block id -- ids drift on installs where the demo\n-- content was re-created (see the convention documented in 00000000000006:11-13 and\n-- used by 007/009). This is a pure host-substring swap, so it never clobbers a user's\n-- edited copy, it also repairs author-pasted embeds, and it is naturally idempotent\n-- (a second run matches zero rows) which matters because the nightly sandbox reset\n-- replays the whole migration chain from a truncated history.\n--\n-- No dollar-quoting is needed here: neither search nor replacement literal contains a\n-- single quote, and neither contains a JSON metacharacter, so the ::text round-trip\n-- always re-parses as valid JSON. Query strings (?si=...) are preserved.\n--\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts\n-- excludes any migration whose filename contains \"sandbox\".\n\n-- 1. Live page/post block content (the 4 seeded rows + any author-added embed).\nUPDATE public.blocks\n SET content = replace(\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE content IS NOT NULL\n AND (content::text LIKE '%www.youtube.com/embed/%'\n OR content::text LIKE '%accelerated-motion%');\n\n-- 2. Live Draft Mode snapshots. Publishing a pre-existing draft would otherwise write\n-- the cookie host straight back into public.blocks.\nUPDATE public.content_drafts\n SET blocks = replace(\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\n OR blocks::text LIKE '%accelerated-motion%';\n\nUPDATE public.content_drafts\n SET meta = replace(\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\n OR meta::text LIKE '%accelerated-motion%';\n\nUPDATE public.product_drafts\n SET blocks = replace(\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\n OR blocks::text LIKE '%accelerated-motion%';\n\n-- product_drafts.meta carries short_description / description_json.\nUPDATE public.product_drafts\n SET meta = replace(\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\n OR meta::text LIKE '%accelerated-motion%';\n\n-- 3. Revision history. apps/nextblock/app/cms/revisions/service.ts replays a snapshot\n-- verbatim, so one \"Restore version\" click would otherwise reintroduce the issue.\n-- URL-only rewrite; nothing else about the historical snapshot is touched.\nUPDATE public.page_revisions\n SET content = replace(\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE content::text LIKE '%www.youtube.com/embed/%'\n OR content::text LIKE '%accelerated-motion%';\n\nUPDATE public.post_revisions\n SET content = replace(\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE content::text LIKE '%www.youtube.com/embed/%'\n OR content::text LIKE '%accelerated-motion%';\n\n-- 4. Product rich text rendered on public /product/* routes.\nUPDATE public.products\n SET short_description = replace(\n replace(short_description, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )\n WHERE short_description IS NOT NULL\n AND (short_description LIKE '%www.youtube.com/embed/%'\n OR short_description LIKE '%accelerated-motion%');\n\nUPDATE public.products\n SET description_json = replace(\n replace(description_json::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE description_json IS NOT NULL\n AND (description_json::text LIKE '%www.youtube.com/embed/%'\n OR description_json::text LIKE '%accelerated-motion%');\n\n-- 5. Custom block definitions (rich-text defaults / emptyFallback markup rendered by\n-- DynamicLayoutEngine). Host-only swap, so the is_valid_custom_block_* CHECK\n-- constraints still hold. No-op on a fresh install.\nUPDATE public.custom_block_definitions\n SET fields = replace(\n replace(fields::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE fields::text LIKE '%www.youtube.com/embed/%'\n OR fields::text LIKE '%accelerated-motion%';\n\nUPDATE public.custom_block_definitions\n SET layout_schema = replace(\n replace(layout_schema::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\n 'accelerated-motion', 'accelerometer'\n )::jsonb\n WHERE layout_schema::text LIKE '%www.youtube.com/embed/%'\n OR layout_schema::text LIKE '%accelerated-motion%';\n"
86
86
  },
87
87
  {
88
88
  "version": "00000000000014",
@@ -177,26 +177,56 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
177
177
  {
178
178
  "version": "00000000000032",
179
179
  "name": "00000000000032_seed_seo_score_optimizations.sql",
180
- "sql": "-- Migration: 00000000000032_seed_seo_score_optimizations.sql\n-- Description: Optimize seeded pages and posts for perfect 100/100 Page SEO scores.\n-- Safety: All updates are forward-only and content-guarded so they only apply to\n-- default seeded copy and will NEVER overwrite customized content on external sites.\n\nDO $$\nBEGIN\n -----------------------------------------------------------------------------\n -- 1. Page Metadata Optimizations\n -----------------------------------------------------------------------------\n UPDATE public.pages\n SET meta_title = 'NextBlock™ - CMS Next.js Haute Performance',\n meta_description = 'NextBlock est un CMS Next.js open-source conçu sur Supabase. Éditeur visuel de blocs, bilingue et scores Lighthouse parfaits dès le premier jour.',\n updated_at = now()\n WHERE slug = 'accueil'\n AND (meta_title IS NULL OR meta_title = '' OR meta_title = 'NextBlock™ - CMS Next.js Haute Performance')\n AND (meta_description IS NULL OR meta_description = '' OR meta_description LIKE 'NextBlock est un CMS Next.js%');\n\n UPDATE public.pages\n SET meta_title = 'NextBlock Journal | Engineering Guides & Tutorials',\n meta_description = 'Read technical deep dives, tutorials, and release notes on Next.js 16, Supabase, and modern visual web publishing from the NextBlock team.',\n updated_at = now()\n WHERE slug = 'articles' AND language_id = 1\n AND (meta_description IS NULL OR meta_description = '' OR meta_description = 'Explore architectural walkthroughs, Supabase recipes, and block editor experiments written by the Nextblock core team.');\n\n UPDATE public.pages\n SET meta_title = 'Journal NextBlock | Guides Techniques et Tutoriels',\n meta_description = 'Découvrez des guides techniques, des tutoriels et des analyses sur Next.js 16, Supabase et l’édition de blocs moderne avec l’équipe NextBlock.',\n updated_at = now()\n WHERE slug = 'articles' AND language_id = 2\n AND (meta_description IS NULL OR meta_description = '' OR meta_description = 'Explorez les guides d''architecture, les recettes Supabase et les experiences de l''editeur de blocs de NextBlock.');\n\n UPDATE public.pages\n SET meta_title = 'Contact Us | NextBlock Open-Source CMS',\n meta_description = 'Have questions, ideas, or feedback about NextBlock? Reach out to our team for technical support, partnership inquiries, and community discussions.',\n updated_at = now()\n WHERE slug = 'contact' AND language_id = 1\n AND (meta_description IS NULL OR meta_description = '' OR meta_description LIKE 'NextBlock™ is an open-source project driven%');\n\n UPDATE public.pages\n SET meta_title = 'Contactez-nous | NextBlock CMS Open Source',\n meta_description = 'Une question, une idée ou un retour sur NextBlock ? Contactez notre équipe pour du support technique, des partenariats ou échanger sur le projet.',\n updated_at = now()\n WHERE slug = 'contact' AND language_id = 2\n AND (meta_description IS NULL OR meta_description = '' OR meta_description LIKE 'NextBlock™ est un projet open-source propulsé%');\n\n UPDATE public.pages\n SET meta_title = 'Shop NextBlock™ Modules | Official Store',\n meta_description = 'Browse official commercial modules and licenses for NextBlock CMS. Get instant access to NextBlock Commerce and Cortex AI with secure checkout.',\n updated_at = now()\n WHERE slug = 'shop'\n AND (meta_description IS NULL OR meta_description = '' OR meta_description = 'Browse our premium products');\n\n UPDATE public.pages\n SET meta_title = 'Boutique NextBlock™ | Modules et Licences Officielles',\n meta_description = 'Achetez des licences et extensions officielles pour NextBlock CMS. Accédez à NextBlock Commerce et Cortex AI avec un paiement simple et sécurisé.',\n updated_at = now()\n WHERE slug = 'boutique'\n AND (meta_description IS NULL OR meta_description = '' OR meta_description IN ('Decouvrez nos produits premium', 'Découvrez nos produits premium'));\n\n -----------------------------------------------------------------------------\n -- 2. Post Metadata Optimizations\n -----------------------------------------------------------------------------\n UPDATE public.posts\n SET meta_description = 'A single command keeps your NextBlock install updated on Docker, Supabase, or a cloned repo. Learn how it works and keeps your data safe.',\n updated_at = now()\n WHERE slug = 'how-updating-works'\n AND (meta_description IS NULL OR meta_description LIKE 'A single command keeps your NextBlock install updated%');\n\n UPDATE public.posts\n SET meta_title = 'Mises à jour NextBlock : une commande pour chaque site',\n meta_description = 'Une seule commande met à jour NextBlock sur Docker, Supabase ou un dépôt cloné. Découvrez le fonctionnement et la protection de vos données.',\n updated_at = now()\n WHERE slug = 'comment-fonctionnent-les-mises-a-jour'\n AND (meta_title IS NULL OR meta_title LIKE 'Les mises à jour de NextBlock%');\n\n -----------------------------------------------------------------------------\n -- 3. Post Content Optimizations (Content-Guarded)\n -----------------------------------------------------------------------------\n -- Post 1: How NextBlock Works EN\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ is built so the hosted CMS, the open-source starter, and the developer tools feel like one clear product. A shared Nx workspace and typed blocks keep code clean and teams moving fast.</p>\n\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>One codebase</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Shared foundation</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Marketing pages, CMS screens, and the starter template grow together instead of drifting apart.</p>\n </div>\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Typed content</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocks with guardrails</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Zod schemas and typed contracts make every custom block safe to build and ship.</p>\n </div>\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Editorial UX</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Product-grade editing</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>The Tiptap layer gives writers a rich editing screen without hiding the power of clean HTML.</p>\n </div>\n</div>\n\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\n <div class=''w-full md:w-1/2 space-y-4''>\n <h2>Monorepo Layout and Dependency Flow</h2>\n <p>The <code>apps/nextblock</code> folder holds the production Next.js app. This includes the public site and the private CMS admin area. The <code>apps/create-nextblock</code> tool mirrors that setup so teams can start with proven patterns right away.</p>\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li><strong>@nextblock-cms/ui</strong> - UI parts, design tokens, and shared buttons</li>\n <li><strong>@nextblock-cms/utils</strong> - Translations, safety checks, and image helpers</li>\n <li><strong>@nextblock-cms/db</strong> - Safe migrations, typed database queries, and schemas</li>\n <li><strong>@nextblock-cms/editor</strong> - The reusable Tiptap v3 block editor</li>\n <li><strong>@nextblock-cms/sdk</strong> - Typed tools to build and check custom blocks</li>\n <li><strong>@nextblock-cms/ecommerce</strong> - The digital store package when activated</li>\n </ul>\n <p>Run <code>nx graph</code> to see how code flows across the workspace. Shared path aliases and Tailwind styles help keep designs aligned across all pages.</p>\n </div>\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube-nocookie.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''How NextBlock™ Works'' allow=''accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx makes workspace relations clear. That is why our starter, CMS, and packages stay aligned.</p>\n </aside>\n</div>\n\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\n <img src=''/images/extensibility.webp'' alt=''NextBlock™ extensibility artwork showing the CMS connected to reusable modules and integrations'' class=''w-full h-auto object-cover'' />\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>One unified design system spans content blocks, editing tools, and store features.</figcaption>\n</figure>\n\n<h2>Block Registry as Product Surface</h2>\n<p>The block registry file is the source of truth for all blocks. It holds Zod schemas, starter content, and editor components. Today it includes everything from text and headings to sections, post grids, checkout forms, and product cards.</p>\n<p>Sections support nested columns, so you can build real page layouts rather than flat lists. Typed helper functions keep custom layouts safe and easy to maintain.</p>\n\n<h2>The Editing Layer</h2>\n<p>The editor package wraps Tiptap into a rich editing surface. It offers slash commands, floating menus, drag handles, tables, checklists, and code blocks. It preserves clean HTML so teams are never locked into a closed format.</p>\n\n<h2>Inside the CMS Shell</h2>\n<p>Inside the CMS folder, each feature follows a clear pattern: item lists, create forms, edit screens, and server actions that wrap database updates. This gives editors a smooth flow while keeping credentials safe on the server.</p>\n\n<h2>Open Core Without Product Drift</h2>\n<p>The core CMS is open-source under the AGPL license. Store modules remain source-available and turn on with verified licenses. This keeps the core lightweight while unlocking advanced tools when you need them.</p>\n\n<h2>Why It Holds Together</h2>\n<p>The Nx monorepo keeps libraries clean. The Next.js framework ensures fast page loads. Supabase migrations define database rules, and Tiptap gives writers a great authoring screen. When you run <code>npm create nextblock</code>, you get a complete, working system from day one.</p>\n'::text)),\n updated_at = now()\n WHERE post_id = 1\n AND content->>'html_content' LIKE '%NextBlock™ is designed so the hosted CMS%';\n\n -- Post 2: Comment NextBlock Fonctionne FR\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ est conçu pour offrir une expérience fluide entre le CMS hébergé, le projet open-source et les outils développeur. L''espace Nx partagé et les blocs typés permettent aux équipes de publier vite tout en gardant un code très propre.</p>\n\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Base unique</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Une même base</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Les pages du site, les écrans du CMS et le modèle de départ évoluent ensemble sans dérive.</p>\n </div>\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Contenu typé</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocs avec garde-fous</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Les schémas Zod et les types stricts sécurisent chaque nouveau bloc personnalisé.</p>\n </div>\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Confort de rédaction</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Édition premium</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>L''éditeur Tiptap offre un vrai confort de travail sans brider la puissance du HTML propre.</p>\n </div>\n</div>\n\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\n <div class=''w-full md:w-1/2 space-y-4''>\n <h2>Architecture monorepo et flux de dépendances</h2>\n <p>Le dossier <code>apps/nextblock</code> contient le site Next.js public et le tableau de bord privé du CMS. La commande <code>apps/create-nextblock</code> reprend cette même base pour lancer des projets sains dès le premier jour.</p>\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li><strong>@nextblock-cms/ui</strong> - Composants d''interface, boutons et styles partagés</li>\n <li><strong>@nextblock-cms/utils</strong> - Traductions, gardes d''environnement et images</li>\n <li><strong>@nextblock-cms/db</strong> - Migrations sûres, accès typé à la base et schémas</li>\n <li><strong>@nextblock-cms/editor</strong> - L''éditeur de blocs réutilisable basé sur Tiptap v3</li>\n <li><strong>@nextblock-cms/sdk</strong> - Outils typés pour créer et vérifier des blocs sur mesure</li>\n <li><strong>@nextblock-cms/ecommerce</strong> - Le module de boutique en ligne une fois activé</li>\n </ul>\n <p>La commande <code>nx graph</code> montre clairement les liens entre chaque dossier. Les alias de code et la configuration Tailwind partagée assurent un design soigné partout.</p>\n </div>\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube-nocookie.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''Comment NextBlock™ Fonctionne'' allow=''accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx rend chaque lien visible dans le projet. C''est pourquoi le starter et le CMS restent toujours synchronisés.</p>\n </aside>\n</div>\n\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\n <img src=''/images/extensibility.webp'' alt=''Illustration NextBlock montrant les connexions entre le CMS et les modules externes'' class=''w-full h-auto object-cover'' />\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Un même système visuel réunit la gestion de contenu, l''édition et les modules du store.</figcaption>\n</figure>\n\n<h2>Le registre de blocs comme surface produit</h2>\n<p>Le registre de blocs est la source de vérité pour tous les blocs du CMS. Il rassemble les schémas Zod, les contenus de départ et les composants d''affichage. Il gère le texte, les titres, les sections, les listes d''articles et les formulaires de paiement.</p>\n<p>Les sections acceptent des colonnes imbriquées. Vous pouvez ainsi monter de vraies pages complètes plutôt que de simples listes de textes. Des fonctions typées rendent cette souplesse très sûre à manipuler.</p>\n\n<h2>La couche d''édition</h2>\n<p>Le paquet éditeur transforme Tiptap en un espace de rédaction riche. Il propose des commandes slash, des menus flottants, des tableaux, des listes de tâches et des blocs de code colorés. Il conserve un HTML propre pour ne jamais vous enfermer.</p>\n\n<h2>À l''intérieur du shell CMS</h2>\n<p>Dans le dossier CMS, chaque module suit la même logique : listes de données, pages d''ajout, écrans d''édition et Server Actions. Les rédacteurs travaillent en toute confiance pendant que les accès restent protégés sur le serveur.</p>\n\n<h2>Open core sans dérive produit</h2>\n<p>Le cœur de NextBlock est open-source sous licence AGPL. Les modules du store s''activent avec une clé de licence valide. Le socle reste léger tout en ouvrant des outils pro quand vous en avez besoin.</p>\n\n<h2>Pourquoi l''ensemble tient</h2>\n<p>Le monorepo Nx garde les librairies bien rangées. L''application Next.js assure la rapidité des pages. Les migrations Supabase fixent les règles de la base, et l''éditeur Tiptap offre un vrai confort de travail. En lançant <code>npm create nextblock</code>, vous profitez d''une base solide et prête à l''emploi.</p>\n'::text)),\n updated_at = now()\n WHERE post_id = 2\n AND content->>'html_content' LIKE '%NextBlock™ relie le CMS h&eacute;berg&eacute;%';\n\n -- Post 3: Setup EN (Card headings H3 -> H2)\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Deploy on Vercel</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Deploy on Vercel</h2>'\n ),\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h2>'\n ),\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h2>'\n ),\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Clone the repository</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Clone the repository</h2>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = 3\n AND content->>'html_content' LIKE '%NextBlock is an open-source, AI-native Next.js CMS%';\n\n -- Post 4: Setup FR (Card headings H3 -> H2)\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déployer sur Vercel</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déployer sur Vercel</h2>'\n ),\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h2>'\n ),\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h2>'\n ),\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h3>',\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h2>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = 4\n AND content->>'html_content' LIKE '%NextBlock est un CMS Next.js open-source%';\n\n -- Post 5: Commerce EN\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce is our first premium module. It adds a complete store layer to the same editor you use for content. It is built for teams that want their catalog and checkout in one place.</p>\n\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Commerce core</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalog + checkout</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Products, orders, shipping, and invoices plug right into the CMS shell.</p>\n </div>\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Global selling</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-currency ready</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Live rate sync, price rounding, and market rules make selling worldwide simple.</p>\n </div>\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Operator workflow</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Provider-aware flow</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe and Freemius run side by side so your shop stays clean and easy to use.</p>\n </div>\n</div>\n\n<h2>Product Catalog</h2>\n<p>The store supports physical goods and digital downloads. You can set up variants, custom options, images, prices, SKUs, and stock levels. Product photos live in your main media library. That means your content and marketing teams work with the same files.</p>\n\n<h2>Multi-Currency Engine</h2>\n<p>The pricing engine is built for real international stores. It handles rates, rounding, and rules with ease:</p>\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li><strong>Unlimited currencies:</strong> Add any ISO currency code, symbol, and exchange rate.</li>\n <li><strong>Auto FX sync:</strong> Refresh daily rates from Frankfurter or your own provider URL.</li>\n <li><strong>Rounding rules:</strong> Round prices up, down, or use charm pricing like <code>9.99</code>.</li>\n <li><strong>Store auto-sync:</strong> Convert product prices whenever currency rates refresh.</li>\n <li><strong>Rebasing:</strong> Switch your base store currency with full recalculations.</li>\n <li><strong>Custom price overrides:</strong> Set exact prices for specific countries when needed.</li>\n</ul>\n\n<h2>Tax Automation</h2>\n<p>You can set tax rates by hand or let Stripe Tax calculate totals automatically:</p>\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Manual mode</h3>\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Set tax rates by country, state, or province. Stacked rates like GST and PST are supported. Detailed tax lines are saved with each order.</p>\n </div>\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Automatic mode</h3>\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calculates final amounts during checkout. Product tax codes travel with each item, and webhooks record final numbers.</p>\n </div>\n</div>\n\n<h2>Shipping and Checkout</h2>\n<p>Shipping zones match by country and state. They support local method names, per-currency pricing, and free shipping rules. Fallback options ensure buyers always see a valid rate.</p>\n<p>The checkout flow understands different payment providers:</p>\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li><strong>Stripe:</strong> Handles physical goods, inventory checks, shipping costs, taxes, and Checkout sessions.</li>\n <li><strong>Freemius:</strong> Handles digital software licenses, plans, and hosted checkout pages.</li>\n <li>Cart items stay grouped by provider so the buying steps are always clear.</li>\n</ul>\n\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\n <img src=''/images/commerce-plan.webp'' alt=''Commerce roadmap board outlining premium module goals and future storefront capabilities for NextBlock™'' class=''w-full h-auto object-cover'' />\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Commerce is the first premium module on our roadmap. It fits naturally into the larger CMS platform.</figcaption>\n</figure>\n\n<h2>Inventory, Orders, and Invoices</h2>\n<p>When stock tracking is on, checkout checks quantities before payment. Once paid, stock counts update in the database right away.</p>\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li>Order status moves smoothly from pending to paid to shipped.</li>\n <li>Invoice numbers generate through safe database functions.</li>\n <li>Printable invoices use your company brand settings.</li>\n <li>Customers can view their past orders and receipts anytime.</li>\n</ul>\n\n<h2>Commerce Surfaces Inside the CMS</h2>\n<p>When the store package is active, the CMS reveals new screens. You get product lists, stock tools, order details, shipping setup, tax rules, and currency settings. All of these screens look and feel like the rest of the CMS, so your team feels right at home.</p>\n'::text)),\n updated_at = now()\n WHERE post_id = 5\n AND content->>'html_content' LIKE '%NextBlock™ Commerce is the first premium module%';\n\n -- Post 6: Commerce FR\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce est notre premier module premium. Il ajoute une vraie boutique en ligne directement dans le CMS. Il s''adresse aux équipes qui veulent réunir leurs articles, leur catalogue et leurs ventes au même endroit.</p>\n\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Cœur commerce</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalogue + checkout</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Articles, commandes, livraison et factures s''intègrent dans le même CMS.</p>\n </div>\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Vente internationale</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-devise</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Taux de change en direct, prix ronds et règles locales facilitent les ventes.</p>\n </div>\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Gestion claire</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Par fournisseur</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe et Freemius fonctionnent ensemble pour garder une boutique claire.</p>\n </div>\n</div>\n\n<h2>Catalogue produits</h2>\n<p>Le module gère les biens physiques et les produits téléchargeables. Vous pouvez créer des variantes, des options sur mesure, des images, des prix, des SKU et des niveaux de stock. Vos visuels restent rangés dans votre médiathèque habituelle. Rédacteurs et vendeurs travaillent donc avec les mêmes fichiers.</p>\n\n<h2>Moteur multi-devise</h2>\n<p>La gestion des prix est conçue pour de vraies boutiques mondiales :</p>\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li><strong>Devises illimitées :</strong> Ajoutez chaque code de devise, son symbole et son taux de conversion.</li>\n <li><strong>Mise à jour automatique :</strong> Actualisez les taux chaque jour avec Frankfurter ou votre URL privée.</li>\n <li><strong>Arrondis des prix :</strong> Arrondissez vers le haut, vers le bas ou avec des prix comme <code>9,99</code>.</li>\n <li><strong>Synchronisation du catalogue :</strong> Convertissez les prix dès que les devises changent.</li>\n <li><strong>Changement de devise socle :</strong> Changez la monnaie par défaut avec recalcul complet.</li>\n <li><strong>Prix personnalisés :</strong> Fixez des prix précis pour chaque pays si nécessaire.</li>\n</ul>\n\n<h2>Taxes automatiques</h2>\n<p>Vous pouvez régler vos taux à la main ou laisser Stripe Tax faire les calculs :</p>\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode manuel</h3>\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Réglez les taux par pays et province. Les taxes cumulées comme la TPS et la TVQ sont gérées. Chaque ligne est enregistrée sur la commande.</p>\n </div>\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode automatique</h3>\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calcule les montants finaux lors de l''achat. Les codes fiscaux suivent les articles, et les webhooks confirment les chiffres.</p>\n </div>\n</div>\n\n<h2>Livraison et checkout</h2>\n<p>Les zones de livraison se basent sur le pays et la région. Elles acceptent des libellés traduits, des tarifs par devise et la gratuité dès un certain montant. Un tarif de secours évite les blocages.</p>\n<p>Le tunnel d''achat s''adapte à chaque passerelle de paiement :</p>\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li><strong>Stripe :</strong> Gère les produits physiques, les stocks, la livraison, les taxes et les sessions Checkout.</li>\n <li><strong>Freemius :</strong> Gère les licences de logiciels, les formules et les pages de paiement sécurisées.</li>\n <li>Les articles restent séparés par fournisseur pour garder un parcours d''achat très simple.</li>\n</ul>\n\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\n <img src=''/images/commerce-plan.webp'' alt=''Plan de développement de la boutique NextBlock illustrant les futures capacités e-commerce'' class=''w-full h-auto object-cover'' />\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Le commerce est le premier module officiel de notre feuille de route. Il s''intègre au cœur du CMS.</figcaption>\n</figure>\n\n<h2>Inventaire, commandes et factures</h2>\n<p>Quand le suivi de stock est actif, le panier vérifie les quantités avant paiement. Après paiement, les stocks diminuent aussitôt dans la base de données.</p>\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li>Le statut passe simplement de panier en attente à payé puis expédié.</li>\n <li>Les numéros de facture sont créés par des fonctions sûres en base.</li>\n <li>Les factures à imprimer reprennent les couleurs de votre marque.</li>\n <li>Vos clients peuvent consulter l''historique de leurs commandes à tout moment.</li>\n</ul>\n\n<h2>Surfaces commerce dans le CMS</h2>\n<p>Quand le module de vente est activé, le CMS affiche de nouveaux écrans. Vous profitez de la liste des articles, du suivi des stocks, des commandes, de la livraison, des taxes et des devises. Ces pages reprennent le design habituel du CMS pour ne pas dépayser votre équipe.</p>\n'::text)),\n updated_at = now()\n WHERE post_id = 6\n AND content->>'html_content' LIKE '%NextBlock™ Commerce est le premier module premium de l''ecosysteme%';\n\n -- Post 7: Cortex AI EN\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock Cortex AI is the smart content tool built for modern web pages. It understands page blocks, section layouts, and editorial rules so you can create better content faster.</p>\n\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\n <div class=''rounded-3xl border border-violet-200/70 bg-violet-50/80 p-6 dark:border-violet-500/20 dark:bg-violet-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Model routing</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Pick the right model</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Route your prompts through OpenRouter or your own provider to balance speed and price.</p>\n </div>\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/80 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>BYOK control</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Use your own keys</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Keep provider keys safe on your server while giving editors a simple AI writing screen.</p>\n </div>\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/80 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Typed output</p>\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Generate valid blocks</h2>\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Typed schemas ensure AI content drops straight into your pages as clean, working blocks.</p>\n </div>\n</div>\n\n<h2>Why Cortex AI Belongs Inside the Editor</h2>\n<p>Generic chat tools can draft text, but they do not know the difference between a hero banner, a card grid, and a blog post. Cortex AI lives right inside your editing screen. It creates content that fits the blocks on your live site.</p>\n<p>This makes AI truly useful for everyday work. You can draft a new landing section, polish a summary, expand product copy, or translate a full article with ease.</p>\n\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50/90 p-6 my-10 dark:border-white/10 dark:bg-slate-900/70''>\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Editorial workflow</p>\n <div class=''grid gap-5 md:grid-cols-2 mt-5''>\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Faster first drafts</h3>\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Start with a prompt and get a section, article draft, or product story that matches your tone.</p>\n </div>\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Cleaner revisions</h3>\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Ask for shorter, clearer, or translated text without leaving your edit screen.</p>\n </div>\n </div>\n</div>\n\n<h2>Model Routing and Cost Control</h2>\n<p>Cortex AI gives you full control over your models. You can pick fast models for quick drafts and stronger models for technical articles. You manage your API keys on the server, so your writers can focus on good content.</p>\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\n <li>Use quick models for rewrites, titles, and summaries.</li>\n <li>Use top models for long guides and difficult translations.</li>\n <li>Keep provider keys safe in your private server settings.</li>\n <li>Control costs without changing how pages display to visitors.</li>\n</ul>\n\n<h2>Block-Aware Generation</h2>\n<p>Cortex AI does more than write plain text. It creates structured content that maps directly to NextBlock components. You get ready-to-use section copy, headings, buttons, and translated blocks. You spend less time fixing messy copy from external tools.</p>\n<p>Because the generated content respects your block rules, everything looks consistent and runs fast on the web.</p>\n\n<h2>Safer Team Workflows</h2>\n<p>AI works best when humans stay in control. With Cortex AI, editors review every draft before publishing. Developers manage the model keys, and the CMS keeps all versions in your normal history log.</p>\n\n<h2>A Practical Launch Flow</h2>\n<ol class=''list-decimal pl-6 space-y-2 text-sm''>\n <li>Draft an article, landing section, or product story from a clear prompt.</li>\n <li>Refine the text to match your audience and brand tone.</li>\n <li>Create a translated version or shorter summary for social cards and search tags.</li>\n <li>Review the content in the editor, hit publish, and track your revisions over time.</li>\n</ol>\n\n<p>Cortex AI turns your CMS into a faster creative workshop. It does not replace human taste, but it helps you turn great ideas into finished pages in record time.</p>\n'::text)),\n updated_at = now()\n WHERE post_id = 7\n AND content->>'html_content' LIKE '%NextBlock Cortex AI is the AI layer built%';\n\n -----------------------------------------------------------------------------\n -- 4. Page Block Content Optimizations (Content-Guarded)\n -----------------------------------------------------------------------------\n\n -- Page 1 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, 'Edge-rendered marketing sites, launches, and docs with uncompromising performance.', 'Fast edge-rendered sites, launches, and docs. Built for top speed and clean code.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Edge-rendered marketing sites, launches,' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ is the open-source, developer-first Next.js CMS that merges 100% Lighthouse scores with a powerful visual block editor.', 'NextBlock™ is the open-source Next.js CMS that pairs 100% Lighthouse scores with a visual block editor.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'NextBlock™ is the open-source, developer' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ is a holistic platform that unites performance, editorial experience, and developer control so every stakeholder delivers their best work.', 'NextBlock™ brings speed, clear editing, and developer control together so your team can build better web pages.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'NextBlock™ is a holistic platform that u' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Built for 100% Lighthouse scores with global delivery and near-instant FCP.', 'Built for top Lighthouse scores, quick page loads, and fast global delivery.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Built for 100% Lighthouse scores with gl' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'A low-code, Notion-style block editor empowers teams to ship pages without engineering help.', 'A clean block editor gives your team the power to create pages without writing code.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'A low-code, Notion-style block editor em' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Open-source control with a clean Nx monorepo and a typed SDK for limitless customization.', 'Full open-source control with a clean monorepo and typed tools to build custom blocks with ease.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Open-source control with a clean Nx mono' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Every layer of NextBlock™ leans on proven developer-first technology so the platform feels familiar, performant, and trustworthy from day one.', 'NextBlock™ uses tools you already know and trust, so your site stays fast, safe, and easy to run.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Every layer of NextBlock™ leans on prove' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'For Content Creators', 'For Writers and Editors')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'For Content Creators' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Drag-and-drop layouts with a Notion-like interface.', 'Drag and drop blocks in a clean, simple layout.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Drag-and-drop layouts with a Notion-like' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Pre-built hero, feature, testimonial, and callout components.', 'Add heroes, galleries, and quotes with one click.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Pre-built hero, feature, testimonial, an' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Drag-and-drop assets with folders, search, and alt text.', 'Keep your images organized with folders and tags.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Drag-and-drop assets with folders, searc' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Audit trail with one-click restore for every change.', 'Restore any saved version with a single click.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Audit trail with one-click restore for e' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Server Components, ISR, and Edge Functions ready out of the box.', 'Built with Server Components and fast edge caching.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Server Components, ISR, and Edge Functio' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Postgres, Auth, Storage, and Row-Level Security fully wired.', 'Postgres database, user auth, and file storage ready to use.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Postgres, Auth, Storage, and Row-Level S' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Nx-powered monorepo engineered for scale and clean separation.', 'An Nx workspace made for scale and clean code.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Nx-powered monorepo engineered for scale' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'TypeScript SDK with Zod validation for building custom blocks.', 'Build and type new blocks in minutes.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'TypeScript SDK with Zod validation for b' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ Commerce transforms your content platform into a complete e-commerce engine. Products, checkout, multi-currency, taxes, shipping, invoices — all natively integrated into the block editor you already know.', 'NextBlock™ Commerce turns your content site into a full online store. Sell products, take payments, handle taxes, and ship orders — all from the block editor you already use.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'NextBlock™ Commerce transforms your cont' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ Commerce transforms your content platform into a complete e-commerce engine. Products, checkout, multi-currency, taxes, shipping, invoices — all natively integrated into the block editor you already know.', 'NextBlock™ Commerce turns your content site into a full online store. Sell products, take payments, handle taxes, and ship orders — all from the block editor you already use.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'NextBlock™ Commerce transforms your cont' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ Commerce ships a complete e-commerce toolkit so you can go from catalog to checkout without third-party plugins.', 'NextBlock™ Commerce ships a complete online store toolkit so you can go from catalog to checkout with ease.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'NextBlock™ Commerce ships a complete e-c' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Live FX rates, rounding rules, and auto-sync across all currencies.', 'Live exchange rates, round prices, and auto-sync across all currencies.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Live FX rates, rounding rules, and auto-' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Define custom rates or let Stripe Tax handle calculations automatically.', 'Set custom tax rates or let Stripe Tax calculate totals automatically.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Define custom rates or let Stripe Tax ha' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Country and state-level zones with free shipping thresholds.', 'Set shipping rates by country and state, with free shipping rules.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Country and state-level zones with free ' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Stripe for physical goods and Freemius for software licenses with seamless checkout.', 'Use Stripe for physical goods and Freemius for software licenses with simple checkout.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Stripe for physical goods and Freemius f' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Automatic stock decrement on payment with variant-level tracking.', 'Update stock counts when an order is paid, with variant tracking.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Automatic stock decrement on payment wit' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Order management, printable invoices, and customer receipts out of the box.', 'Manage orders, print invoices, and view customer reports with ease.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'Order management, printable invoices, an' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ Cortex AI brings native block-level intelligence directly to your editor. Generate copy, refactor structures, and automate translations in one click, built directly on our high-performance architecture.', 'NextBlock™ Cortex AI helps you write, edit, and translate content right inside the block editor. Draft text, refine layouts, and switch languages with ease.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'NextBlock™ Cortex AI brings native block' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ is building a sustainable open-core roadmap so the platform grows with your business.', 'NextBlock™ is built to grow with your business and help your team succeed.')::jsonb,\n updated_at = now()\n WHERE page_id = 1\n AND content::text LIKE '%' || 'NextBlock™ is building a sustainable ope' || '%';\n\n\n -- Page 2 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ est le CMS Next.js open-source alliant scores Lighthouse parfaits et éditeur visuel puissant.', 'NextBlock™ est le CMS Next.js open-source alliant scores parfaits et éditeur de blocs visuel.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'NextBlock™ est le CMS Next.js open-sourc' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Sites marketing et docs rendus à l''edge avec des performances irréprochables.', 'Sites marketing et docs rendus à l''edge. Conçus pour la vitesse et un code propre.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Sites marketing et docs rendus à l''edge ' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ unifie performances, expérience éditoriale et contrôle développeur pour que chaque équipe livre son meilleur travail.', 'NextBlock™ réunit la vitesse, la clarté d''édition et le contrôle développeur. Votre équipe peut ainsi créer de meilleures pages web.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'NextBlock™ unifie performances, expérien' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Pensé pour des scores Lighthouse parfaits avec une diffusion mondiale.', 'Conçu pour des scores élevés et une diffusion rapide.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Pensé pour des scores Lighthouse parfait' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Un éditeur façon Notion pour publier sans dépendre des développeurs.', 'Un éditeur simple pour publier sans coder.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Un éditeur façon Notion pour publier san' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Un socle Next.js + Supabase modulaire, extensible et auto-hébergeable.', 'Un socle moderne, souple et très simple à faire grandir.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Un socle Next.js + Supabase modulaire, e' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Chaque couche de NextBlock™ repose sur des technologies éprouvées pour une expérience familière et performante.', 'NextBlock™ s''appuie sur des outils fiables et éprouvés. Votre site reste rapide, robuste et facile à maintenir au quotidien.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Chaque couche de NextBlock™ repose sur d' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Glisser-déposer façon Notion.', 'Glissez et déposez vos blocs dans une mise en page claire.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Glisser-déposer façon Notion.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Héros, galeries, témoignages.', 'Insérez des héros, des galeries et des avis clients en un clic.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Héros, galeries, témoignages.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Dossiers, tags et actions groupées.', 'Rangez vos photos et médias avec des dossiers et des tags.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Dossiers, tags et actions groupées.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Historique et restauration instantanée.', 'Retrouvez et restaurez vos versions précédentes sans stress.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Historique et restauration instantanée.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Server Components, ISR et Edge prêts à l''emploi.', 'Conçu avec Server Components et un cache edge très rapide.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Server Components, ISR et Edge prêts à l' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Postgres, auth, stockage, temps réel.', 'Base Postgres, profils, auth et stockage de fichiers prêts à l''emploi.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Postgres, auth, stockage, temps réel.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Dépendances lisibles et centrales.', 'Une structure claire et bien rangée faite pour grandir sereinement.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Dépendances lisibles et centrales.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Widgets typés et extensibles.', 'Créez et typez vos nouveaux composants en un instant.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Widgets typés et extensibles.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ Commerce transforme votre plateforme de contenu en moteur e-commerce complet. Produits, checkout, multi-devises, taxes, expédition, factures — le tout intégré nativement dans l''éditeur de blocs que vous connaissez déjà.', 'NextBlock™ Commerce transforme votre site de contenu en vraie boutique en ligne. Vendez des articles et recevez des paiements en ligne. Gérez vos taxes et vos envois dans l''éditeur que vous connaissez déjà.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'NextBlock™ Commerce transforme votre pla' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ Commerce livre une boîte à outils e-commerce complète pour aller du catalogue au paiement sans plugins tiers.', 'NextBlock™ Commerce propose une boîte à outils complète. Allez du catalogue au paiement sans aucun plugin externe.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'NextBlock™ Commerce livre une boîte à ou' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Taux de change en temps réel, modes d''arrondi, prix charme et synchronisation automatique sur toutes les devises.', 'Taux de change en direct, prix ronds et mise à jour automatique sur toutes les devises.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Taux de change en temps réel, modes d''ar' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Taux manuels empilés (TPS + TVQ) ou calcul automatique via Stripe Tax — à vous de choisir.', 'Définissez vos taxes ou laissez Stripe Tax calculer les montants finaux.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Taux manuels empilés (TPS + TVQ) ou calc' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Résolution par pays et état, tarification par devise et seuils de livraison gratuite.', 'Ajustez les frais de port par pays et état avec des règles d''envoi gratuit.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Résolution par pays et état, tarificatio' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Stripe pour les produits physiques, Freemius pour les licences numériques — checkout intelligent avec validation d''inventaire.', 'Paiement Stripe pour les biens et Freemius pour les licences logicielles.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Stripe pour les produits physiques, Free' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Déduction automatique des quantités au paiement avec gestion des stocks par variante.', 'Mettez à jour les stocks dès qu''un achat est validé, avec suivi des variantes.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Déduction automatique des quantités au p' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Gestion du cycle de vie des commandes, numérotation stable des factures et rapports de commandes exportables.', 'Suivez chaque commande, imprimez des factures et téléchargez vos bilans en toute simplicité.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Gestion du cycle de vie des commandes, n' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ Cortex AI apporte une intelligence native au niveau des blocs directement dans votre éditeur. Générez du texte, restructurez vos contenus et automatisez les traductions en un clic, le tout propulsé par notre architecture haute performance.', 'NextBlock™ Cortex AI vous aide à rédiger, corriger et traduire vos textes au cœur de l''éditeur de blocs. Créez des ébauches, améliorez vos titres et passez d''une langue à l''autre en un clin d''œil.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'NextBlock™ Cortex AI apporte une intelli' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ construit une feuille de route open-core durable qui évolue avec votre activité.', 'NextBlock™ est pensé pour accompagner la croissance de votre entreprise. Nous voulons faire réussir vos projets web.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'NextBlock™ construit une feuille de rout' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Le commerce arrive en premier, puis l''ecosysteme s''etend avec des plugins, des blocs et des modules construits par les partenaires.', 'Le commerce ouvre la voie. Notre écosystème grandit avec de nouveaux plugins, des blocs utiles et des modules partenaires.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Le commerce arrive en premier, puis l''ec' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Transformez votre site en vitrine composable avec produits, checkout, tarification multi-devise, taxes automatiques et blocs commerce relies a votre contenu editorial.', 'Ajoutez une boutique complète à côté de vos articles. Profitez de produits, du paiement en ligne, de devises et de taxes sans effort.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Transformez votre site en vitrine compos' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Une marketplace communautaire ouvrira la voie a la publication, la vente et la distribution de blocs, themes, integrations et modules partenaires.', 'Un espace partagé permet aux développeurs de publier et vendre des blocs sur mesure, des thèmes et des outils connectés.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'Une marketplace communautaire ouvrira la' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ se construit en public. Ajoutez une étoile, partagez vos retours et façonnez l''avenir du CMS orienté performance.', 'NextBlock™ avance en public. Mettez une étoile sur le dépôt, partagez vos avis et façonnez la suite avec nous.')::jsonb,\n updated_at = now()\n WHERE page_id = 2\n AND content::text LIKE '%' || 'NextBlock™ se construit en public. Ajout' || '%';\n\n\n -- Page 3 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, '<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Deep dives into performance, DX, and visual editing.</h2>', '<h1 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>The NextBlock Journal: Performance, DX, and Visual Editing</h1>')::jsonb,\n updated_at = now()\n WHERE page_id = 3\n AND content::text LIKE '%' || '<h2 class=''text-4xl md:text-5xl font-bol' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<p class=''text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed''>Explore architectural walkthroughs, Supabase recipes, and block editor experiments written by the Nextblock core team.</p>', '<p class=''text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed''>Explore practical guides, architecture deep dives, and editor workflows written by the NextBlock team and community.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl''> <p>Welcome to the NextBlock Journal. This is your home for in-depth technical guides, release notes, and real-world web architecture patterns. Whether you are launching your first Next.js site or scaling an online store, our articles give you clear, tested steps to help you build faster.</p> <p>We believe modern web projects need developer freedom and a simple editing experience. Our engineering posts show how to pair Next.js 16 with Supabase Postgres, edge caching, and server actions without complex glue code. You will learn how to keep your Lighthouse score at 100% while giving teams a rich, block-based writing experience.</p> <p>Here are the key topics we cover across our editorial library:</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Getting Started Guides:</strong> Simple setup walkthroughs for one-click Vercel deploys, local Docker environments, and custom cloud stacks.</li> <li><strong>Architecture & Performance:</strong> Deep dives into fast page rendering, clean CSS delivery, image optimization, and safe database migrations.</li> <li><strong>E-Commerce Workflows:</strong> Practical patterns for multi-currency pricing, automated tax sync, stock tracking, and provider checkout flows.</li> <li><strong>AI Content Tools:</strong> How to use Cortex AI to generate structured blocks, manage provider keys, and speed up routine translation work.</li> </ul> <p>Our team updates this collection regularly as we release new core features and modules. We test our code on live sites before publishing. Each post includes full technical explanations and architecture diagrams so you can apply the solutions to your own web stack.</p> <p>Have a topic you want us to cover or want to share your own case study? Join our community on GitHub or reach out to our team anytime. We welcome questions, ideas, and pull requests from all builders. You can subscribe to updates or start reading our latest published articles below.</p> </div>')::jsonb,\n updated_at = now()\n WHERE page_id = 3\n AND content::text LIKE '%' || '<p class=''text-slate-300 text-lg max-w-x' || '%';\n\n\n -- Page 4 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, '<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Plongées dans la performance, l''expérience dev et l''édition visuelle.</h2>', '<h1 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Le Journal NextBlock : Performance, Expérience Dev et Édition</h1>')::jsonb,\n updated_at = now()\n WHERE page_id = 4\n AND content::text LIKE '%' || '<h2 class=''text-4xl md:text-5xl font-bol' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<p class=''text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left text-slate-300 leading-relaxed''>Walkthroughs d''architecture, recettes Supabase et expérimentations éditeur écrits par l''équipe Nextblock.</p>', '<p class=''text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left text-slate-300 leading-relaxed''>Retrouvez des guides pratiques, des études d''architecture et des conseils pour éditer vos contenus avec NextBlock.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl''> <p>Bienvenue sur le Journal NextBlock. Cet espace rassemble nos guides techniques détaillés. Vous y trouverez les nouveautés de chaque version et les meilleures pratiques du web moderne. Vous lancez votre premier site Next.js ? Vous faites grandir une boutique en ligne ? Nos articles vous donnent des étapes simples et vérifiées pour construire plus vite.</p> <p>Nous pensons qu''un bon site web doit offrir toute la liberté aux développeurs. Il doit aussi apporter une vraie simplicité aux équipes éditoriales. Nos articles techniques expliquent comment marier Next.js 16 avec Supabase Postgres, le cache edge et les Server Actions. Tout cela se fait sans code superflu. Vous découvrirez comment maintenir un score Lighthouse parfait. En même temps, votre équipe profite d''un éditeur de blocs visuel et très agréable.</p> <p>Voici les principaux sujets abordés dans notre bibliothèque éditoriale :</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Guides de démarrage :</strong> Des tutoriels pas à pas pour déployer sur Vercel en un clic, lancer une pile Docker locale ou installer votre propre cloud.</li> <li><strong>Architecture et performance :</strong> Des explications claires sur le rendu rapide des pages, les styles en ligne, l''optimisation des images et les migrations sûres.</li> <li><strong>Commerce en ligne :</strong> Des conseils concrets pour gérer plusieurs devises, synchroniser les taxes, suivre les stocks et réussir vos paiements.</li> <li><strong>Outils IA pour le contenu :</strong> Comment utiliser Cortex AI pour créer des blocs bien typés, garder le contrôle de vos clés et traduire vos pages en un instant.</li> </ul> <p>Notre équipe enrichit cette collection au fil des mises à jour du projet. Nous testons chaque exemple de code sur des sites réels avant publication. Chaque tutoriel propose des explications complètes pour adapter facilement ces solutions à votre propre projet.</p> <p>Vous avez une idée de sujet ou un retour d''expérience à partager ? Rejoignez notre communauté sur GitHub ou contactez notre équipe. Nous accueillons avec grand plaisir vos questions, vos idées et vos contributions. Parcourez dès maintenant nos derniers articles ci-dessous.</p> </div>')::jsonb,\n updated_at = now()\n WHERE page_id = 4\n AND content::text LIKE '%' || '<p class=''text-lg max-w-xl mx-auto md:mx' || '%';\n\n\n -- Page 5 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, '<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Community Driven</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ is built in the open. We rely on developers and editors like you to help us define the roadmap. Whether it''s a bug report, a feature request, or just a shoutout, every message helps us move faster.</p></div>', '<div class=''mt-10 text-slate-300 space-y-4 max-w-3xl mx-auto''> <h2 class=''text-2xl font-bold text-white mb-4 text-center''>Open Source & Community Driven</h2> <p>NextBlock™ is built entirely in the open. We work closely with developers, editors, and teams who use the platform every day. Whether you found a bug, want to request a new feature, or need advice on your project setup, our team is here to help you move forward.</p> <p>We read every message sent through this form. Here is what you can expect when reaching out to our team:</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Fast Response Times:</strong> Our core maintainers typically review and reply to inquiries within one to two business days.</li> <li><strong>Technical Support:</strong> For public bugs or community questions, our GitHub Discussions and Issues boards offer fast answers from the entire community.</li> <li><strong>Partnership and Modules:</strong> If your team wants to sponsor a roadmap module or build a custom integration, we can schedule a direct call.</li> <li><strong>Security Disclosures:</strong> For responsible disclosure of potential security vulnerabilities, please flag your message as urgent so we can triage it immediately.</li> </ul> <p>Before submitting, please make sure your email address is typed correctly so our team can get back to you. If your question is about a specific code error, including your environment details and reproduction steps helps us find a solution much faster.</p> <p>You can also connect with us directly on GitHub, Discord, or X (formerly Twitter). We host regular community discussions and share sneak peeks of upcoming platform features. If you are building a custom client project with NextBlock, we would love to hear about your experience.</p> <p>We value your time and privacy. We never share your contact information or use your email for marketing without your consent. Send us a message using the form below and we will get back to you shortly.</p> </div>')::jsonb,\n updated_at = now()\n WHERE page_id = 5\n AND content::text LIKE '%' || '<div class=''max-w-2xl mx-auto text-cente' || '%';\n\n\n -- Page 6 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, '<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Communautaire</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ est construit en public. Nous comptons sur les développeurs et éditeurs comme vous pour définir notre roadmap. Qu''il s''agisse d''un bug, d''une suggestion ou d''un simple salut, chaque message compte.</p></div>', '<div class=''mt-10 text-slate-300 space-y-4 max-w-3xl mx-auto''> <h2 class=''text-2xl font-bold text-white mb-4 text-center''>Open Source & Communautaire</h2> <p>NextBlock™ est construit entièrement en public. Nous travaillons avec des développeurs et des équipes qui utilisent le CMS chaque jour. Vous avez trouvé un bug ? Vous souhaitez suggérer une fonction ? Vous avez besoin d''aide pour votre installation ? Notre équipe est là pour vous guider.</p> <p>Nous lisons attentivement chaque message reçu via ce formulaire. Voici nos engagements pour vous répondre au mieux :</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Délais de réponse rapides :</strong> Nos mainteneurs étudient et répondent aux demandes en un ou deux jours ouvrés.</li> <li><strong>Support technique :</strong> Pour toute question publique, nos forums GitHub Discussions et Issues offrent une aide rapide de la communauté.</li> <li><strong>Partenariats et modules :</strong> Votre équipe veut sponsoriser un module ou concevoir une intégration sur mesure ? Nous pouvons planifier un échange direct.</li> <li><strong>Signalements de sécurité :</strong> Pour toute alerte de sécurité, signalez votre message comme prioritaire pour un traitement immédiat.</li> </ul> <p>Avant d''envoyer votre message, vérifiez bien votre adresse email. Notre équipe pourra ainsi vous répondre rapidement. Si votre demande concerne une erreur technique, mentionnez votre environnement et les étapes pour la reproduire. Cela nous aide à trouver une solution efficace.</p> <p>Vous pouvez aussi échanger avec nous sur GitHub, Discord ou X. Nous y partageons des nouvelles et les coulisses des prochaines versions. Vous développez un projet client avec NextBlock ? Racontez-nous votre aventure. Notre équipe aime voir ce que vous créez au quotidien. Chaque projet est unique. Prenez le temps de nous dire ce qui vous plaît et ce qui vous manque. Vos retours nous aident à faire un meilleur outil pour tout le monde.</p> <p>Nous respectons votre vie privée. Vos coordonnées ne sont jamais cédées ni réutilisées sans votre accord. Écrivez-nous ci-dessous et nous vous répondrons très vite.</p> </div>')::jsonb,\n updated_at = now()\n WHERE page_id = 6\n AND content::text LIKE '%' || '<div class=''max-w-2xl mx-auto text-cente' || '%';\n\n\n -- Page 7 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Discover our premium selection of developer tools and digital commerce solutions.</p>', '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Discover our selection of official commercial add-ons and developer tools for NextBlock CMS.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''> <h2 class=''text-2xl font-bold text-white mb-4''>Power Up Your Web Projects</h2> <p>Welcome to the official NextBlock™ digital store. Here you can purchase licenses for our premium packages, including NextBlock™ Commerce and NextBlock™ Cortex AI. Every commercial license helps fund full-time open-source development on our core CMS while giving your team advanced tools to launch high-performance websites faster.</p> <p>Our premium modules are designed to feel native from day one. You get clean code that fits right into your existing NextBlock project without third-party plugins or complex configuration. When you purchase a license from our store, you receive instant access to everything you need:</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Full Source Code:</strong> Inspect, customize, and adapt the modules to your exact requirements.</li> <li><strong>Perpetual Use:</strong> Use the software for your project with full peace of mind.</li> <li><strong>Automated Updates:</strong> Enjoy smooth updates that match each new release of NextBlock and Next.js.</li> <li><strong>Secure Checkout:</strong> Payments are processed safely with Stripe and Freemius with instant receipt generation.</li> </ul> <p>Whether you need multi-currency store features, automated sales tax calculations, or AI block generation in your editor, our modules deliver tested solutions that keep your Lighthouse scores at 100%.</p> <p>All purchases include dedicated onboarding resources, detailed developer docs, and friendly technical support. If you ever run into an issue or need help wiring up a provider webhook, our core engineers are ready to assist you. We also offer a thirty-day refund policy so you can try our tools risk-free.</p> <p>Have questions about license tiers, volume pricing, team seats, or custom agency usage? Contact our support team anytime. Our team is here to answer your questions and help your developers succeed. We are happy to help you pick the best plan for your company. Browse our featured products below to get started today.</p> </div>')::jsonb,\n updated_at = now()\n WHERE page_id = 7\n AND content::text LIKE '%' || '<p style=\"text-align: center; color: var' || '%';\n\n\n -- Page 8 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Decouvrez notre selection premium d outils de developpement.</p>', '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Découvrez nos extensions officielles et nos outils pour développeurs conçus pour le CMS NextBlock.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''> <h2 class=''text-2xl font-bold text-white mb-4''>Accélérez vos projets web</h2> <p>Bienvenue sur la boutique officielle de NextBlock™. Vous pouvez acheter ici des licences pour nos modules professionnels, comme NextBlock™ Commerce et NextBlock™ Cortex AI. Chaque achat aide à financer le travail open-source sur le cœur du CMS. Il donne aussi à votre équipe des outils de pointe pour créer des sites rapides et fiables.</p> <p>Nos modules premium s''intègrent sans effort à votre projet existant. Vous profitez d''un code propre et bien testé, sans plugin externe lourd ni réglage complexe. En choisissant nos outils, vous profitez immédiatement de nombreux avantages :</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Code source complet :</strong> Lisez, adaptez et faites évoluer chaque bloc selon vos besoins métier.</li> <li><strong>Licence perpétuelle :</strong> Utilisez le code sur votre projet en toute sérénité.</li> <li><strong>Mises à jour suivies :</strong> Recevez les nouvelles versions au rythme de Next.js et de NextBlock.</li> <li><strong>Paiement sécurisé :</strong> Les achats passent par Stripe et Freemius avec facture instantanée.</li> </ul> <p>Vous voulez vendre dans plusieurs devises ? Vous avez besoin du calcul automatique des taxes ? Vous voulez générer des blocs avec l''IA ? Nos modules offrent des solutions prêtes à l''emploi qui préservent vos scores Lighthouse à 100%.</p> <p>Chaque commande donne accès à une documentation claire et à notre support technique. Si vous avez besoin d''aide pour brancher un webhook ou un mode de paiement, nos développeurs vous répondent rapidement. Notre équipe est à vos côtés pour vous faire gagner du temps. Nous proposons aussi une garantie satisfait ou remboursé de trente jours.</p> <p>Vous avez des questions sur nos tarifs, les licences agence ou les remises en volume ? Écrivez à notre équipe à tout moment. Nous vous guiderons avec grand plaisir vers l''offre idéale pour votre entreprise. Découvrez dès aujourd''hui l''ensemble de nos produits ci-dessous pour bien démarrer votre projet.</p> </div>')::jsonb,\n updated_at = now()\n WHERE page_id = 8\n AND content::text LIKE '%' || '<p style=\"text-align: center; color: var' || '%';\n\n\n -- Page 9 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ CMS (\"we\", \"us\", or \"our\") respects your privacy and is committed to protecting your personal information in accordance with Quebec''s <em>Act respecting the protection of personal information in the private sector</em> (Law 25), the federal <em>Personal Information Protection and Electronic Documents Act</em> (PIPEDA), and Canada''s Anti-Spam Legislation (CASL).', 'NextBlock™ CMS (\"we\", \"us\", or \"our\") respects your privacy. We protect your personal information under Quebec''s Law 25, the federal PIPEDA act, and Canada''s Anti-Spam Legislation (CASL).')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || 'NextBlock™ CMS (\"we\", \"us\", or \"our\") re' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Our Privacy Officer is responsible for our compliance with applicable privacy laws. You may reach them at', 'Our Privacy Officer oversees our compliance with privacy laws. You can reach them at')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || 'Our Privacy Officer is responsible for o' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<li><strong>Account information</strong> &mdash; name, email address, and credentials when you register.</li>', '<li><strong>Account information:</strong> Your name, email address, and login details when you register.</li>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<li><strong>Account information</strong>' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<li><strong>Usage and device data</strong> &mdash; collected only with your consent through analytics technologies.</li>', '<li><strong>Usage and device data:</strong> Collected only with your consent through analytics tools.</li>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<li><strong>Usage and device data</stron' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<li><strong>Communications</strong> &mdash; messages you send us and your marketing preferences.</li>', '<li><strong>Communications:</strong> Messages you send us and your newsletter choices.</li>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<li><strong>Communications</strong> &mda' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>3. Why we collect it and your consent</h2>\r\n<p>We collect personal information for clearly identified purposes: to provide and secure our services, to communicate with you, and &mdash; only with your express, opt-in consent &mdash; for analytics and marketing. Consistent with Law 25, non-essential cookies and trackers remain disabled until you actively accept them, and you may withdraw your consent at any time.</p>', '<h2>3. Why we collect data and your consent</h2> <p>We collect personal information for clear reasons: to provide our services, keep accounts secure, and reply to your messages. We use analytics and marketing tools only with your direct, opt-in consent. Under Law 25, optional cookies stay off until you choose to accept them. You can withdraw your consent at any time.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<h2>3. Why we collect it and your consen' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Strictly necessary cookies keep the site working and require no consent. Analytics and marketing technologies are loaded <strong>only after</strong> you opt in through our consent banner. Your choice is recorded so we can honour it and demonstrate accountability.', 'Essential cookies keep the site running and require no consent. Analytics and marketing cookies load only after you opt in via our cookie banner. We record your choice to honor it and follow privacy rules.')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || 'Strictly necessary cookies keep the site' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'We do not sell your personal information. We share it only with service providers who help us operate the platform under contractual confidentiality obligations, or where required by law.', 'We do not sell your personal data. We share it only with trusted service providers who help us run the platform under strict confidentiality agreements, or when required by law.')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || 'We do not sell your personal information' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>6. Retention</h2>\r\n<p>We keep personal information only for as long as necessary to fulfil the purposes described above or as required by law, after which it is securely destroyed or anonymized.</p>', '<h2>6. Data retention</h2> <p>We keep personal information only as long as needed for the purposes described above or as required by law. After that, we securely delete or anonymize your data.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<h2>6. Retention</h2>\r\n<p>We keep person' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Subject to applicable law, you have the right to access, rectify, and delete your personal information, to withdraw consent, to data portability, and to be informed about automated processing. To exercise these rights, contact our Privacy Officer at', 'Under privacy laws, you have the right to view, correct, and delete your personal information. You can also withdraw consent or ask for a portable copy of your data. To use these rights, write to our Privacy Officer at')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || 'Subject to applicable law, you have the ' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>8. Commercial electronic messages (CASL)</h2>\r\n<p>We send commercial electronic messages only with your consent. Every message identifies us and includes a working unsubscribe mechanism that we honour promptly.</p>', '<h2>8. Commercial electronic messages</h2> <p>We send commercial emails only with your permission. Every email clearly identifies us and includes an easy unsubscribe link that takes effect right away.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<h2>8. Commercial electronic messages (C' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>9. Safeguards</h2>\r\n<p>We use appropriate physical, organizational, and technological measures &mdash; including encryption in transit and access controls &mdash; to protect personal information against loss, theft, and unauthorized access.</p>', '<h2>9. Security safeguards</h2> <p>We use strong technical and physical protections to keep your data safe. These include encrypted connections and strict access controls to prevent loss, theft, and unauthorized access.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<h2>9. Safeguards</h2>\r\n<p>We use approp' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ CMS is free, open-source software distributed under the GNU Affero General Public License v3. When you self-host NextBlock, you are the operator responsible for the personal information processed by your own deployment, and this policy serves as a starting point you may adapt to your organization.', 'NextBlock™ CMS is free open-source software under the AGPLv3 license. When you self-host NextBlock, you control your own server. You are responsible for the personal data on your deployment, and this policy is a helpful model you can adapt.')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || 'NextBlock™ CMS is free, open-source soft' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>11. Changes to this policy</h2>\r\n<p>We may update this policy from time to time. Material changes will be communicated through the site, and the \"last updated\" date will be revised.</p>', '<h2>11. Policy changes</h2> <p>We may update this policy over time. We announce important changes on our site and update the revision date at the top.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || '<h2>11. Changes to this policy</h2>\r\n<p>' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Questions or complaints? Contact NextBlock™ CMS at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. You may also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.', 'Have questions or complaints? Contact NextBlock™ at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. You can also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.')::jsonb,\n updated_at = now()\n WHERE page_id = 9\n AND content::text LIKE '%' || 'Questions or complaints? Contact NextBlo' || '%';\n\n\n -- Page 10 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ CMS (« nous ») respecte votre vie privée et s''engage à protéger vos renseignements personnels conformément à la <em>Loi sur la protection des renseignements personnels dans le secteur privé</em> du Québec (Loi 25), à la <em>Loi sur la protection des renseignements personnels et les documents électroniques</em> (LPRPDE) et à la Loi canadienne anti-pourriel (LCAP).', 'NextBlock™ CMS (« nous ») respecte votre vie privée. Nous protégeons vos renseignements personnels selon la Loi 25 du Québec, la loi fédérale LPRPDE et les règles canadiennes anti-pourriel (LCAP).')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || 'NextBlock™ CMS (« nous ») respecte votre' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Notre responsable de la protection des renseignements personnels veille au respect des lois applicables. Vous pouvez le joindre à', 'Notre responsable veille au respect des règles de confidentialité. Vous pouvez lui écrire à')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || 'Notre responsable de la protection des r' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>2. Renseignements que nous recueillons</h2>\r\n<ul>\r\n <li><strong>Renseignements de compte</strong> &mdash; nom, adresse courriel et identifiants lors de l''inscription.</li>\r\n <li><strong>Données d''utilisation et d''appareil</strong> &mdash; recueillies uniquement avec votre consentement au moyen de technologies d''analyse.</li>\r\n <li><strong>Communications</strong> &mdash; les messages que vous nous envoyez et vos préférences marketing.</li>\r\n</ul>', '<h2>2. Renseignements recueillis</h2> <ul> <li><strong>Renseignements de compte :</strong> Votre nom, votre courriel et vos accès lors de l''inscription.</li> <li><strong>Données d''utilisation et d''appareil :</strong> Recueillies avec votre accord via nos outils de mesure.</li> <li><strong>Communications :</strong> Vos messages reçus et vos choix de suivi par courriel.</li> </ul>')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || '<h2>2. Renseignements que nous recueillo' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<p>Nous recueillons des renseignements personnels à des fins clairement déterminées : fournir et sécuriser nos services, communiquer avec vous et &mdash; uniquement avec votre consentement exprès &mdash; à des fins d''analyse et de marketing. Conformément à la Loi 25, les témoins et traceurs non essentiels demeurent désactivés tant que vous ne les avez pas acceptés, et vous pouvez retirer votre consentement en tout temps.</p>', '<p>Nous recueillons vos données pour des motifs clairs : faire fonctionner nos services, sécuriser les accès et vous répondre. Les outils d''analyse et de suivi ne s''activent qu''avec votre accord clair. Selon la Loi 25, les témoins optionnels restent coupés tant que vous ne les acceptez pas. Vous pouvez retirer votre accord en tout temps.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || '<p>Nous recueillons des renseignements p' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Les témoins strictement nécessaires assurent le fonctionnement du site et ne requièrent aucun consentement. Les technologies d''analyse et de marketing ne sont chargées qu''<strong>après</strong> votre consentement explicite. Votre choix est enregistré afin de le respecter.', 'Les témoins essentiels assurent le bon fonctionnement du site. Ils ne demandent aucun accord préalable. Les témoins d''analyse se chargent seulement après votre choix sur notre bandeau. Nous gardons votre choix en mémoire pour le respecter.')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || 'Les témoins strictement nécessaires assu' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>5. Communication à des tiers</h2>\r\n<p>Nous ne vendons pas vos renseignements personnels. Nous ne les communiquons qu''à des fournisseurs qui nous aident à exploiter la plateforme, sous obligation de confidentialité, ou lorsque la loi l''exige.</p>', '<h2>5. Partage et communication</h2> <p>Nous ne vendons jamais vos données personnelles. Nous les partageons uniquement avec des prestataires de confiance qui nous aident à faire tourner le site sous contrat de secret, ou si la loi l''impose.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || '<h2>5. Communication à des tiers</h2>\r\n<' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>6. Conservation</h2>\r\n<p>Nous ne conservons les renseignements personnels que le temps nécessaire aux fins décrites ou exigé par la loi, après quoi ils sont détruits ou anonymisés de façon sécuritaire.</p>', '<h2>6. Durée de conservation</h2> <p>Nous gardons vos données seulement le temps utile pour les buts décrits ou selon la loi. Ensuite, nous les effaçons ou nous les rendons anonymes de façon sûre.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || '<h2>6. Conservation</h2>\r\n<p>Nous ne con' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Sous réserve de la loi applicable, vous avez le droit d''accéder à vos renseignements, de les rectifier et de les supprimer, de retirer votre consentement, à la portabilité de vos données et d''être informé du traitement automatisé. Pour exercer ces droits, écrivez à', 'Vous avez le droit de lire, de corriger et de faire effacer vos données. Vous pouvez aussi retirer votre accord ou demander une copie de vos données. Pour exercer vos droits, écrivez à notre responsable à')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || 'Sous réserve de la loi applicable, vous ' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>8. Messages électroniques commerciaux (LCAP)</h2>\r\n<p>Nous n''envoyons des messages électroniques commerciaux qu''avec votre consentement. Chaque message nous identifie et comporte un mécanisme de désabonnement fonctionnel que nous respectons rapidement.</p>', '<h2>8. Messages électroniques</h2> <p>Nous envoyons des courriels informatifs seulement avec votre accord. Chaque courriel montre notre nom et propose un lien simple pour vous désabonner d''un clic.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || '<h2>8. Messages électroniques commerciau' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>9. Mesures de sécurité</h2>\r\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées &mdash; dont le chiffrement en transit et le contrôle des accès &mdash; pour protéger vos renseignements.</p>', '<h2>9. Mesures de sécurité</h2> <p>Nous utilisons des moyens techniques et physiques solides pour garder vos données en sûreté. Cela comprend des échanges chiffrés et un contrôle strict des accès contre toute fuite ou vol.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || '<h2>9. Mesures de sécurité</h2>\r\n<p>Nous' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la licence publique générale GNU Affero v3. Lorsque vous hébergez NextBlock vous-même, vous êtes l''exploitant responsable des renseignements personnels traités par votre propre instance, et la présente politique vous sert de point de départ adaptable à votre organisation.', 'NextBlock™ CMS est un logiciel libre sous licence AGPLv3. Si vous hébergez NextBlock vous-même, vous gérez votre propre serveur. Vous êtes responsable des données sur votre instance, et ce texte est un modèle que vous pouvez adapter.')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || 'NextBlock™ CMS est un logiciel libre et ' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>11. Modifications</h2>\r\n<p>Nous pouvons mettre à jour cette politique. Les changements importants seront communiqués sur le site et la date de mise à jour sera révisée.</p>', '<h2>11. Mises à jour</h2> <p>Nous pouvons mettre à jour ce texte au fil du temps. Les changements notables seront affichés sur le site avec la nouvelle date en tête de page.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || '<h2>11. Modifications</h2>\r\n<p>Nous pouv' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi vous adresser à la Commission d''accès à l''information du Québec.', 'Une question ou un avis ? Écrivez à NextBlock™ à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi joindre la Commission d''accès à l''information du Québec.')::jsonb,\n updated_at = now()\n WHERE page_id = 10\n AND content::text LIKE '%' || 'Des questions ou des plaintes ? Contacte' || '%';\n\n\n -- Page 12 Block Updates\n UPDATE public.blocks\n SET content = replace(content::text, 'En accédant à NextBlock™ CMS et aux services que nous fournissons (les « Services ») ou en les utilisant, vous acceptez d''être lié par les présentes conditions d''utilisation. Si vous n''êtes pas d''accord, n''utilisez pas les Services.', 'En utilisant le CMS NextBlock™ et les services associés (les « Services »), vous acceptez ces conditions d''utilisation. Si vous refusez ces règles, veuillez ne pas utiliser les Services.')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || 'En accédant à NextBlock™ CMS et aux serv' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>2. Logiciel libre et à code source ouvert</h2>\r\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou, à votre choix, toute version ultérieure. Vous êtes libre d''exécuter, d''étudier, de partager et de modifier le logiciel selon les termes de cette licence. Une copie de la licence est fournie avec le logiciel et est aussi disponible à <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>', '<h2>2. Logiciel libre et code source ouvert</h2> <p>NextBlock™ CMS est un logiciel libre sous <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou toute version ultérieure. Vous pouvez lancer, étudier, copier et faire évoluer le code selon cette licence. Le texte complet se trouve avec le code et en ligne sur <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || '<h2>2. Logiciel libre et à code source o' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>3. Disponibilité du code source</h2>\r\n<p>Conformément à l''article 13 de l''AGPL-3.0, si vous exploitez une version modifiée de NextBlock™ CMS et la rendez accessible à des utilisateurs sur un réseau, vous devez offrir clairement à ces utilisateurs l''accès au code source correspondant de votre version modifiée, gratuitement, par un moyen usuel de copie de logiciels.</p>', '<h2>3. Partage du code source</h2> <p>Selon l''article 13 de la licence AGPL-3.0, si vous modifiez NextBlock™ CMS et le mettez en ligne pour des usagers sur un réseau, vous devez offrir l''accès libre et sans frais au code source de votre version modifiée.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || '<h2>3. Disponibilité du code source</h2>' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>4. Marques de commerce</h2>\r\n<p>L''AGPL-3.0 accorde de larges droits sur le code source du logiciel, mais <strong>n''accorde aucun droit</strong> sur nos noms commerciaux, marques de commerce ou marques de service. « NextBlock™ », le nom NextBlock™ CMS et les logos associés demeurent notre propriété et ne peuvent être utilisés d''une manière laissant entendre une approbation ou une affiliation sans notre autorisation écrite préalable.</p>', '<h2>4. Marques et logos</h2> <p>La licence AGPL-3.0 donne de larges droits sur le code. Mais <strong>elle ne donne aucun droit</strong> sur nos noms et marques. Les termes « NextBlock™ », NextBlock™ CMS et les logos restent notre bien exclusif. Ils ne peuvent être repris sans accord écrit préalable.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || '<h2>4. Marques de commerce</h2>\r\n<p>L''AG' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>5. Comptes et utilisation acceptable</h2>\r\n<p>Si vous créez un compte, vous êtes responsable de la protection de vos identifiants et de toute activité effectuée à partir de votre compte, et vous vous engagez à nous aviser rapidement de toute utilisation non autorisée. Vous vous engagez à ne pas détourner les Services, notamment en tentant de les perturber, d''y accéder sans autorisation ou de les utiliser à des fins illégales.</p>', '<h2>5. Comptes et bon usage</h2> <p>Si vous ouvrez un compte, vous gardez la garde de vos accès et de toute action faite sous votre nom. Vous devez nous prévenir vite en cas d''usage non permis. Vous vous engagez à ne pas bloquer les Services, ne pas forcer les accès et ne pas agir contre la loi.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || '<h2>5. Comptes et utilisation acceptable' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Comme l''énonce l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou implicite, y compris, sans s''y limiter, les garanties implicites de qualité marchande et d''adéquation à un usage particulier. Vous assumez l''entièreté du risque quant à la qualité et au rendement du logiciel.', 'Selon l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou tacite. Cela inclut les garanties de vente ou d''usage pour un besoin précis. Vous prenez sur vous les risques liés au bon emploi du logiciel.')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || 'Comme l''énonce l''article 15 de l''AGPL-3.' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>7. Limitation de responsabilité</h2>\r\n<p>Comme l''énonce l''article 16 de l''AGPL-3.0, et dans toute la mesure permise par la loi applicable, en aucun cas un titulaire de droits d''auteur ou toute autre partie qui modifie ou transmet le logiciel ne saurait être tenu responsable envers vous de dommages, y compris tout dommage général, spécial, accessoire ou consécutif découlant de l''utilisation ou de l''impossibilité d''utiliser le logiciel.</p>', '<h2>7. Limite de responsabilité</h2> <p>Selon l''article 16 de l''AGPL-3.0 et dans la limite permise par la loi, aucun auteur ou tiers modifiant le code ne peut être tenu pour responsable de vos pertes ou dommages liés à l''usage du logiciel.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || '<h2>7. Limitation de responsabilité</h2>' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Les présentes conditions sont régies par les lois de la province de Québec et les lois fédérales du Canada qui y sont applicables, sans égard aux règles de conflit de lois. Rien dans les présentes conditions ne limite les droits impératifs de protection du consommateur dont vous pourriez bénéficier en vertu de ces lois.', 'Ces conditions suivent les lois de la province de Québec et les lois du Canada applicables. Rien ici ne réduit vos droits stricts de consommateur selon ces lois.')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || 'Les présentes conditions sont régies par' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, '<h2>9. Modifications</h2>\r\n<p>Nous pouvons réviser ces conditions de temps à autre. Les changements importants seront communiqués au moyen des Services, et l''utilisation continue des Services après leur entrée en vigueur vaut acceptation.</p>', '<h2>9. Mises à jour</h2> <p>Nous pouvons adapter ces règles au fil du temps. Les changements notables passeront sur le site. Votre usage continu vaut accord avec les nouvelles règles.</p>')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || '<h2>9. Modifications</h2>\r\n<p>Nous pouvo' || '%';\n\n UPDATE public.blocks\n SET content = replace(content::text, 'Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.', 'Une question sur ces conditions ? Écrivez à NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.')::jsonb,\n updated_at = now()\n WHERE page_id = 12\n AND content::text LIKE '%' || 'Des questions sur ces conditions ? Conta' || '%';\n\nEND $$;\n"
180
+ "sql": "-- Migration: 00000000000032_seed_seo_score_optimizations.sql\r\n-- Description: Optimize seeded pages and posts for perfect 100/100 Page SEO scores.\r\n-- Safety: All updates are forward-only and content-guarded so they only apply to\r\n-- default seeded copy and will NEVER overwrite customized content on external sites.\r\n\r\nDO $$\r\nBEGIN\r\n -----------------------------------------------------------------------------\r\n -- 1. Page Metadata Optimizations\r\n -----------------------------------------------------------------------------\r\n UPDATE public.pages\r\n SET meta_title = 'NextBlock™ - CMS Next.js Haute Performance',\r\n meta_description = 'NextBlock est un CMS Next.js open-source conçu sur Supabase. Éditeur visuel de blocs, bilingue et scores Lighthouse parfaits dès le premier jour.',\r\n updated_at = now()\r\n WHERE slug = 'accueil'\r\n AND (meta_title IS NULL OR meta_title = '' OR meta_title = 'NextBlock™ - CMS Next.js Haute Performance')\r\n AND (meta_description IS NULL OR meta_description = '' OR meta_description LIKE 'NextBlock est un CMS Next.js%');\r\n\r\n UPDATE public.pages\r\n SET meta_title = 'NextBlock Journal | Engineering Guides & Tutorials',\r\n meta_description = 'Read technical deep dives, tutorials, and release notes on Next.js 16, Supabase, and modern visual web publishing from the NextBlock team.',\r\n updated_at = now()\r\n WHERE slug = 'articles' AND language_id = 1\r\n AND (meta_description IS NULL OR meta_description = '' OR meta_description = 'Explore architectural walkthroughs, Supabase recipes, and block editor experiments written by the Nextblock core team.');\r\n\r\n UPDATE public.pages\r\n SET meta_title = 'Journal NextBlock | Guides Techniques et Tutoriels',\r\n meta_description = 'Découvrez des guides techniques, des tutoriels et des analyses sur Next.js 16, Supabase et l’édition de blocs moderne avec l’équipe NextBlock.',\r\n updated_at = now()\r\n WHERE slug = 'articles' AND language_id = 2\r\n AND (meta_description IS NULL OR meta_description = '' OR meta_description = 'Explorez les guides d''architecture, les recettes Supabase et les experiences de l''editeur de blocs de NextBlock.');\r\n\r\n UPDATE public.pages\r\n SET meta_title = 'Contact Us | NextBlock Open-Source CMS',\r\n meta_description = 'Have questions, ideas, or feedback about NextBlock? Reach out to our team for technical support, partnership inquiries, and community discussions.',\r\n updated_at = now()\r\n WHERE slug = 'contact' AND language_id = 1\r\n AND (meta_description IS NULL OR meta_description = '' OR meta_description LIKE 'NextBlock™ is an open-source project driven%');\r\n\r\n UPDATE public.pages\r\n SET meta_title = 'Contactez-nous | NextBlock CMS Open Source',\r\n meta_description = 'Une question, une idée ou un retour sur NextBlock ? Contactez notre équipe pour du support technique, des partenariats ou échanger sur le projet.',\r\n updated_at = now()\r\n WHERE slug = 'contact' AND language_id = 2\r\n AND (meta_description IS NULL OR meta_description = '' OR meta_description LIKE 'NextBlock™ est un projet open-source propulsé%');\r\n\r\n UPDATE public.pages\r\n SET meta_title = 'Shop NextBlock™ Modules | Official Store',\r\n meta_description = 'Browse official commercial modules and licenses for NextBlock CMS. Get instant access to NextBlock Commerce and Cortex AI with secure checkout.',\r\n updated_at = now()\r\n WHERE slug = 'shop'\r\n AND (meta_description IS NULL OR meta_description = '' OR meta_description = 'Browse our premium products');\r\n\r\n UPDATE public.pages\r\n SET meta_title = 'Boutique NextBlock™ | Modules et Licences Officielles',\r\n meta_description = 'Achetez des licences et extensions officielles pour NextBlock CMS. Accédez à NextBlock Commerce et Cortex AI avec un paiement simple et sécurisé.',\r\n updated_at = now()\r\n WHERE slug = 'boutique'\r\n AND (meta_description IS NULL OR meta_description = '' OR meta_description IN ('Decouvrez nos produits premium', 'Découvrez nos produits premium'));\r\n\r\n -----------------------------------------------------------------------------\r\n -- 2. Post Metadata Optimizations\r\n -----------------------------------------------------------------------------\r\n UPDATE public.posts\r\n SET meta_description = 'A single command keeps your NextBlock install updated on Docker, Supabase, or a cloned repo. Learn how it works and keeps your data safe.',\r\n updated_at = now()\r\n WHERE slug = 'how-updating-works'\r\n AND (meta_description IS NULL OR meta_description LIKE 'A single command keeps your NextBlock install updated%');\r\n\r\n UPDATE public.posts\r\n SET meta_title = 'Mises à jour NextBlock : une commande pour chaque site',\r\n meta_description = 'Une seule commande met à jour NextBlock sur Docker, Supabase ou un dépôt cloné. Découvrez le fonctionnement et la protection de vos données.',\r\n updated_at = now()\r\n WHERE slug = 'comment-fonctionnent-les-mises-a-jour'\r\n AND (meta_title IS NULL OR meta_title LIKE 'Les mises à jour de NextBlock%');\r\n\r\n -----------------------------------------------------------------------------\r\n -- 3. Post Content Optimizations (Content-Guarded)\r\n -----------------------------------------------------------------------------\r\n -- Post 1: How NextBlock Works EN\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ is built so the hosted CMS, the open-source starter, and the developer tools feel like one clear product. A shared Nx workspace and typed blocks keep code clean and teams moving fast.</p>\r\n\r\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\r\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>One codebase</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Shared foundation</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Marketing pages, CMS screens, and the starter template grow together instead of drifting apart.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Typed content</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocks with guardrails</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Zod schemas and typed contracts make every custom block safe to build and ship.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Editorial UX</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Product-grade editing</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>The Tiptap layer gives writers a rich editing screen without hiding the power of clean HTML.</p>\r\n </div>\r\n</div>\r\n\r\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\r\n <div class=''w-full md:w-1/2 space-y-4''>\r\n <h2>Monorepo Layout and Dependency Flow</h2>\r\n <p>The <code>apps/nextblock</code> folder holds the production Next.js app. This includes the public site and the private CMS admin area. The <code>apps/create-nextblock</code> tool mirrors that setup so teams can start with proven patterns right away.</p>\r\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li><strong>@nextblock-cms/ui</strong> - UI parts, design tokens, and shared buttons</li>\r\n <li><strong>@nextblock-cms/utils</strong> - Translations, safety checks, and image helpers</li>\r\n <li><strong>@nextblock-cms/db</strong> - Safe migrations, typed database queries, and schemas</li>\r\n <li><strong>@nextblock-cms/editor</strong> - The reusable Tiptap v3 block editor</li>\r\n <li><strong>@nextblock-cms/sdk</strong> - Typed tools to build and check custom blocks</li>\r\n <li><strong>@nextblock-cms/ecommerce</strong> - The digital store package when activated</li>\r\n </ul>\r\n <p>Run <code>nx graph</code> to see how code flows across the workspace. Shared path aliases and Tailwind styles help keep designs aligned across all pages.</p>\r\n </div>\r\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\r\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube-nocookie.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''How NextBlock™ Works'' allow=''accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\r\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx makes workspace relations clear. That is why our starter, CMS, and packages stay aligned.</p>\r\n </aside>\r\n</div>\r\n\r\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\r\n <img src=''/images/extensibility.webp'' alt=''NextBlock™ extensibility artwork showing the CMS connected to reusable modules and integrations'' class=''w-full h-auto object-cover'' />\r\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>One unified design system spans content blocks, editing tools, and store features.</figcaption>\r\n</figure>\r\n\r\n<h2>Block Registry as Product Surface</h2>\r\n<p>The block registry file is the source of truth for all blocks. It holds Zod schemas, starter content, and editor components. Today it includes everything from text and headings to sections, post grids, checkout forms, and product cards.</p>\r\n<p>Sections support nested columns, so you can build real page layouts rather than flat lists. Typed helper functions keep custom layouts safe and easy to maintain.</p>\r\n\r\n<h2>The Editing Layer</h2>\r\n<p>The editor package wraps Tiptap into a rich editing surface. It offers slash commands, floating menus, drag handles, tables, checklists, and code blocks. It preserves clean HTML so teams are never locked into a closed format.</p>\r\n\r\n<h2>Inside the CMS Shell</h2>\r\n<p>Inside the CMS folder, each feature follows a clear pattern: item lists, create forms, edit screens, and server actions that wrap database updates. This gives editors a smooth flow while keeping credentials safe on the server.</p>\r\n\r\n<h2>Open Core Without Product Drift</h2>\r\n<p>The core CMS is open-source under the AGPL license. Store modules remain source-available and turn on with verified licenses. This keeps the core lightweight while unlocking advanced tools when you need them.</p>\r\n\r\n<h2>Why It Holds Together</h2>\r\n<p>The Nx monorepo keeps libraries clean. The Next.js framework ensures fast page loads. Supabase migrations define database rules, and Tiptap gives writers a great authoring screen. When you run <code>npm create nextblock</code>, you get a complete, working system from day one.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE post_id = 1\r\n AND content->>'html_content' LIKE '%NextBlock™ is designed so the hosted CMS%';\r\n\r\n -- Post 2: Comment NextBlock Fonctionne FR\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ est conçu pour offrir une expérience fluide entre le CMS hébergé, le projet open-source et les outils développeur. L''espace Nx partagé et les blocs typés permettent aux équipes de publier vite tout en gardant un code très propre.</p>\r\n\r\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\r\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Base unique</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Une même base</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Les pages du site, les écrans du CMS et le modèle de départ évoluent ensemble sans dérive.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Contenu typé</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocs avec garde-fous</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Les schémas Zod et les types stricts sécurisent chaque nouveau bloc personnalisé.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Confort de rédaction</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Édition premium</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>L''éditeur Tiptap offre un vrai confort de travail sans brider la puissance du HTML propre.</p>\r\n </div>\r\n</div>\r\n\r\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\r\n <div class=''w-full md:w-1/2 space-y-4''>\r\n <h2>Architecture monorepo et flux de dépendances</h2>\r\n <p>Le dossier <code>apps/nextblock</code> contient le site Next.js public et le tableau de bord privé du CMS. La commande <code>apps/create-nextblock</code> reprend cette même base pour lancer des projets sains dès le premier jour.</p>\r\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li><strong>@nextblock-cms/ui</strong> - Composants d''interface, boutons et styles partagés</li>\r\n <li><strong>@nextblock-cms/utils</strong> - Traductions, gardes d''environnement et images</li>\r\n <li><strong>@nextblock-cms/db</strong> - Migrations sûres, accès typé à la base et schémas</li>\r\n <li><strong>@nextblock-cms/editor</strong> - L''éditeur de blocs réutilisable basé sur Tiptap v3</li>\r\n <li><strong>@nextblock-cms/sdk</strong> - Outils typés pour créer et vérifier des blocs sur mesure</li>\r\n <li><strong>@nextblock-cms/ecommerce</strong> - Le module de boutique en ligne une fois activé</li>\r\n </ul>\r\n <p>La commande <code>nx graph</code> montre clairement les liens entre chaque dossier. Les alias de code et la configuration Tailwind partagée assurent un design soigné partout.</p>\r\n </div>\r\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\r\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube-nocookie.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''Comment NextBlock™ Fonctionne'' allow=''accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\r\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx rend chaque lien visible dans le projet. C''est pourquoi le starter et le CMS restent toujours synchronisés.</p>\r\n </aside>\r\n</div>\r\n\r\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\r\n <img src=''/images/extensibility.webp'' alt=''Illustration NextBlock montrant les connexions entre le CMS et les modules externes'' class=''w-full h-auto object-cover'' />\r\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Un même système visuel réunit la gestion de contenu, l''édition et les modules du store.</figcaption>\r\n</figure>\r\n\r\n<h2>Le registre de blocs comme surface produit</h2>\r\n<p>Le registre de blocs est la source de vérité pour tous les blocs du CMS. Il rassemble les schémas Zod, les contenus de départ et les composants d''affichage. Il gère le texte, les titres, les sections, les listes d''articles et les formulaires de paiement.</p>\r\n<p>Les sections acceptent des colonnes imbriquées. Vous pouvez ainsi monter de vraies pages complètes plutôt que de simples listes de textes. Des fonctions typées rendent cette souplesse très sûre à manipuler.</p>\r\n\r\n<h2>La couche d''édition</h2>\r\n<p>Le paquet éditeur transforme Tiptap en un espace de rédaction riche. Il propose des commandes slash, des menus flottants, des tableaux, des listes de tâches et des blocs de code colorés. Il conserve un HTML propre pour ne jamais vous enfermer.</p>\r\n\r\n<h2>À l''intérieur du shell CMS</h2>\r\n<p>Dans le dossier CMS, chaque module suit la même logique : listes de données, pages d''ajout, écrans d''édition et Server Actions. Les rédacteurs travaillent en toute confiance pendant que les accès restent protégés sur le serveur.</p>\r\n\r\n<h2>Open core sans dérive produit</h2>\r\n<p>Le cœur de NextBlock est open-source sous licence AGPL. Les modules du store s''activent avec une clé de licence valide. Le socle reste léger tout en ouvrant des outils pro quand vous en avez besoin.</p>\r\n\r\n<h2>Pourquoi l''ensemble tient</h2>\r\n<p>Le monorepo Nx garde les librairies bien rangées. L''application Next.js assure la rapidité des pages. Les migrations Supabase fixent les règles de la base, et l''éditeur Tiptap offre un vrai confort de travail. En lançant <code>npm create nextblock</code>, vous profitez d''une base solide et prête à l''emploi.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE post_id = 2\r\n AND content->>'html_content' LIKE '%NextBlock™ relie le CMS h&eacute;berg&eacute;%';\r\n\r\n -- Post 3: Setup EN (Card headings H3 -> H2)\r\n UPDATE public.blocks\r\n SET content = jsonb_set(\r\n content,\r\n '{html_content}',\r\n to_jsonb(\r\n replace(\r\n replace(\r\n replace(\r\n replace(\r\n content->>'html_content',\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Deploy on Vercel</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Deploy on Vercel</h2>'\r\n ),\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h2>'\r\n ),\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h2>'\r\n ),\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Clone the repository</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Clone the repository</h2>'\r\n )\r\n )\r\n ),\r\n updated_at = now()\r\n WHERE post_id = 3\r\n AND content->>'html_content' LIKE '%NextBlock is an open-source, AI-native Next.js CMS%';\r\n\r\n -- Post 4: Setup FR (Card headings H3 -> H2)\r\n UPDATE public.blocks\r\n SET content = jsonb_set(\r\n content,\r\n '{html_content}',\r\n to_jsonb(\r\n replace(\r\n replace(\r\n replace(\r\n replace(\r\n content->>'html_content',\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déployer sur Vercel</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déployer sur Vercel</h2>'\r\n ),\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h2>'\r\n ),\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h2>'\r\n ),\r\n '<h3 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h3>',\r\n '<h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h2>'\r\n )\r\n )\r\n ),\r\n updated_at = now()\r\n WHERE post_id = 4\r\n AND content->>'html_content' LIKE '%NextBlock est un CMS Next.js open-source%';\r\n\r\n -- Post 5: Commerce EN\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce is our first premium module. It adds a complete store layer to the same editor you use for content. It is built for teams that want their catalog and checkout in one place.</p>\r\n\r\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\r\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Commerce core</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalog + checkout</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Products, orders, shipping, and invoices plug right into the CMS shell.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Global selling</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-currency ready</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Live rate sync, price rounding, and market rules make selling worldwide simple.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Operator workflow</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Provider-aware flow</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe and Freemius run side by side so your shop stays clean and easy to use.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Product Catalog</h2>\r\n<p>The store supports physical goods and digital downloads. You can set up variants, custom options, images, prices, SKUs, and stock levels. Product photos live in your main media library. That means your content and marketing teams work with the same files.</p>\r\n\r\n<h2>Multi-Currency Engine</h2>\r\n<p>The pricing engine is built for real international stores. It handles rates, rounding, and rules with ease:</p>\r\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li><strong>Unlimited currencies:</strong> Add any ISO currency code, symbol, and exchange rate.</li>\r\n <li><strong>Auto FX sync:</strong> Refresh daily rates from Frankfurter or your own provider URL.</li>\r\n <li><strong>Rounding rules:</strong> Round prices up, down, or use charm pricing like <code>9.99</code>.</li>\r\n <li><strong>Store auto-sync:</strong> Convert product prices whenever currency rates refresh.</li>\r\n <li><strong>Rebasing:</strong> Switch your base store currency with full recalculations.</li>\r\n <li><strong>Custom price overrides:</strong> Set exact prices for specific countries when needed.</li>\r\n</ul>\r\n\r\n<h2>Tax Automation</h2>\r\n<p>You can set tax rates by hand or let Stripe Tax calculate totals automatically:</p>\r\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\r\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\r\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Manual mode</h3>\r\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Set tax rates by country, state, or province. Stacked rates like GST and PST are supported. Detailed tax lines are saved with each order.</p>\r\n </div>\r\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\r\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Automatic mode</h3>\r\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calculates final amounts during checkout. Product tax codes travel with each item, and webhooks record final numbers.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Shipping and Checkout</h2>\r\n<p>Shipping zones match by country and state. They support local method names, per-currency pricing, and free shipping rules. Fallback options ensure buyers always see a valid rate.</p>\r\n<p>The checkout flow understands different payment providers:</p>\r\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li><strong>Stripe:</strong> Handles physical goods, inventory checks, shipping costs, taxes, and Checkout sessions.</li>\r\n <li><strong>Freemius:</strong> Handles digital software licenses, plans, and hosted checkout pages.</li>\r\n <li>Cart items stay grouped by provider so the buying steps are always clear.</li>\r\n</ul>\r\n\r\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\r\n <img src=''/images/commerce-plan.webp'' alt=''Commerce roadmap board outlining premium module goals and future storefront capabilities for NextBlock™'' class=''w-full h-auto object-cover'' />\r\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Commerce is the first premium module on our roadmap. It fits naturally into the larger CMS platform.</figcaption>\r\n</figure>\r\n\r\n<h2>Inventory, Orders, and Invoices</h2>\r\n<p>When stock tracking is on, checkout checks quantities before payment. Once paid, stock counts update in the database right away.</p>\r\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li>Order status moves smoothly from pending to paid to shipped.</li>\r\n <li>Invoice numbers generate through safe database functions.</li>\r\n <li>Printable invoices use your company brand settings.</li>\r\n <li>Customers can view their past orders and receipts anytime.</li>\r\n</ul>\r\n\r\n<h2>Commerce Surfaces Inside the CMS</h2>\r\n<p>When the store package is active, the CMS reveals new screens. You get product lists, stock tools, order details, shipping setup, tax rules, and currency settings. All of these screens look and feel like the rest of the CMS, so your team feels right at home.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE post_id = 5\r\n AND content->>'html_content' LIKE '%NextBlock™ Commerce is the first premium module%';\r\n\r\n -- Post 6: Commerce FR\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce est notre premier module premium. Il ajoute une vraie boutique en ligne directement dans le CMS. Il s''adresse aux équipes qui veulent réunir leurs articles, leur catalogue et leurs ventes au même endroit.</p>\r\n\r\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\r\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Cœur commerce</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalogue + checkout</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Articles, commandes, livraison et factures s''intègrent dans le même CMS.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Vente internationale</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-devise</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Taux de change en direct, prix ronds et règles locales facilitent les ventes.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Gestion claire</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Par fournisseur</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe et Freemius fonctionnent ensemble pour garder une boutique claire.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Catalogue produits</h2>\r\n<p>Le module gère les biens physiques et les produits téléchargeables. Vous pouvez créer des variantes, des options sur mesure, des images, des prix, des SKU et des niveaux de stock. Vos visuels restent rangés dans votre médiathèque habituelle. Rédacteurs et vendeurs travaillent donc avec les mêmes fichiers.</p>\r\n\r\n<h2>Moteur multi-devise</h2>\r\n<p>La gestion des prix est conçue pour de vraies boutiques mondiales :</p>\r\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li><strong>Devises illimitées :</strong> Ajoutez chaque code de devise, son symbole et son taux de conversion.</li>\r\n <li><strong>Mise à jour automatique :</strong> Actualisez les taux chaque jour avec Frankfurter ou votre URL privée.</li>\r\n <li><strong>Arrondis des prix :</strong> Arrondissez vers le haut, vers le bas ou avec des prix comme <code>9,99</code>.</li>\r\n <li><strong>Synchronisation du catalogue :</strong> Convertissez les prix dès que les devises changent.</li>\r\n <li><strong>Changement de devise socle :</strong> Changez la monnaie par défaut avec recalcul complet.</li>\r\n <li><strong>Prix personnalisés :</strong> Fixez des prix précis pour chaque pays si nécessaire.</li>\r\n</ul>\r\n\r\n<h2>Taxes automatiques</h2>\r\n<p>Vous pouvez régler vos taux à la main ou laisser Stripe Tax faire les calculs :</p>\r\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\r\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\r\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode manuel</h3>\r\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Réglez les taux par pays et province. Les taxes cumulées comme la TPS et la TVQ sont gérées. Chaque ligne est enregistrée sur la commande.</p>\r\n </div>\r\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\r\n <h3 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode automatique</h3>\r\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calcule les montants finaux lors de l''achat. Les codes fiscaux suivent les articles, et les webhooks confirment les chiffres.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Livraison et checkout</h2>\r\n<p>Les zones de livraison se basent sur le pays et la région. Elles acceptent des libellés traduits, des tarifs par devise et la gratuité dès un certain montant. Un tarif de secours évite les blocages.</p>\r\n<p>Le tunnel d''achat s''adapte à chaque passerelle de paiement :</p>\r\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li><strong>Stripe :</strong> Gère les produits physiques, les stocks, la livraison, les taxes et les sessions Checkout.</li>\r\n <li><strong>Freemius :</strong> Gère les licences de logiciels, les formules et les pages de paiement sécurisées.</li>\r\n <li>Les articles restent séparés par fournisseur pour garder un parcours d''achat très simple.</li>\r\n</ul>\r\n\r\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\r\n <img src=''/images/commerce-plan.webp'' alt=''Plan de développement de la boutique NextBlock illustrant les futures capacités e-commerce'' class=''w-full h-auto object-cover'' />\r\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Le commerce est le premier module officiel de notre feuille de route. Il s''intègre au cœur du CMS.</figcaption>\r\n</figure>\r\n\r\n<h2>Inventaire, commandes et factures</h2>\r\n<p>Quand le suivi de stock est actif, le panier vérifie les quantités avant paiement. Après paiement, les stocks diminuent aussitôt dans la base de données.</p>\r\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li>Le statut passe simplement de panier en attente à payé puis expédié.</li>\r\n <li>Les numéros de facture sont créés par des fonctions sûres en base.</li>\r\n <li>Les factures à imprimer reprennent les couleurs de votre marque.</li>\r\n <li>Vos clients peuvent consulter l''historique de leurs commandes à tout moment.</li>\r\n</ul>\r\n\r\n<h2>Surfaces commerce dans le CMS</h2>\r\n<p>Quand le module de vente est activé, le CMS affiche de nouveaux écrans. Vous profitez de la liste des articles, du suivi des stocks, des commandes, de la livraison, des taxes et des devises. Ces pages reprennent le design habituel du CMS pour ne pas dépayser votre équipe.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE post_id = 6\r\n AND content->>'html_content' LIKE '%NextBlock™ Commerce est le premier module premium de l''ecosysteme%';\r\n\r\n -- Post 7: Cortex AI EN\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock Cortex AI is the smart content tool built for modern web pages. It understands page blocks, section layouts, and editorial rules so you can create better content faster.</p>\r\n\r\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\r\n <div class=''rounded-3xl border border-violet-200/70 bg-violet-50/80 p-6 dark:border-violet-500/20 dark:bg-violet-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Model routing</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Pick the right model</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Route your prompts through OpenRouter or your own provider to balance speed and price.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/80 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>BYOK control</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Use your own keys</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Keep provider keys safe on your server while giving editors a simple AI writing screen.</p>\r\n </div>\r\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/80 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Typed output</p>\r\n <h2 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Generate valid blocks</h2>\r\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Typed schemas ensure AI content drops straight into your pages as clean, working blocks.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Why Cortex AI Belongs Inside the Editor</h2>\r\n<p>Generic chat tools can draft text, but they do not know the difference between a hero banner, a card grid, and a blog post. Cortex AI lives right inside your editing screen. It creates content that fits the blocks on your live site.</p>\r\n<p>This makes AI truly useful for everyday work. You can draft a new landing section, polish a summary, expand product copy, or translate a full article with ease.</p>\r\n\r\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50/90 p-6 my-10 dark:border-white/10 dark:bg-slate-900/70''>\r\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Editorial workflow</p>\r\n <div class=''grid gap-5 md:grid-cols-2 mt-5''>\r\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\r\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Faster first drafts</h3>\r\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Start with a prompt and get a section, article draft, or product story that matches your tone.</p>\r\n </div>\r\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\r\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Cleaner revisions</h3>\r\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Ask for shorter, clearer, or translated text without leaving your edit screen.</p>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<h2>Model Routing and Cost Control</h2>\r\n<p>Cortex AI gives you full control over your models. You can pick fast models for quick drafts and stronger models for technical articles. You manage your API keys on the server, so your writers can focus on good content.</p>\r\n<ul class=''list-disc pl-6 space-y-2 text-sm''>\r\n <li>Use quick models for rewrites, titles, and summaries.</li>\r\n <li>Use top models for long guides and difficult translations.</li>\r\n <li>Keep provider keys safe in your private server settings.</li>\r\n <li>Control costs without changing how pages display to visitors.</li>\r\n</ul>\r\n\r\n<h2>Block-Aware Generation</h2>\r\n<p>Cortex AI does more than write plain text. It creates structured content that maps directly to NextBlock components. You get ready-to-use section copy, headings, buttons, and translated blocks. You spend less time fixing messy copy from external tools.</p>\r\n<p>Because the generated content respects your block rules, everything looks consistent and runs fast on the web.</p>\r\n\r\n<h2>Safer Team Workflows</h2>\r\n<p>AI works best when humans stay in control. With Cortex AI, editors review every draft before publishing. Developers manage the model keys, and the CMS keeps all versions in your normal history log.</p>\r\n\r\n<h2>A Practical Launch Flow</h2>\r\n<ol class=''list-decimal pl-6 space-y-2 text-sm''>\r\n <li>Draft an article, landing section, or product story from a clear prompt.</li>\r\n <li>Refine the text to match your audience and brand tone.</li>\r\n <li>Create a translated version or shorter summary for social cards and search tags.</li>\r\n <li>Review the content in the editor, hit publish, and track your revisions over time.</li>\r\n</ol>\r\n\r\n<p>Cortex AI turns your CMS into a faster creative workshop. It does not replace human taste, but it helps you turn great ideas into finished pages in record time.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE post_id = 7\r\n AND content->>'html_content' LIKE '%NextBlock Cortex AI is the AI layer built%';\r\n\r\n -----------------------------------------------------------------------------\r\n -- 4. Page Block Content Optimizations (Content-Guarded)\r\n -----------------------------------------------------------------------------\r\n\r\n -- Page 1 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Edge-rendered marketing sites, launches, and docs with uncompromising performance.', 'Fast edge-rendered sites, launches, and docs. Built for top speed and clean code.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Edge-rendered marketing sites, launches,' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ is the open-source, developer-first Next.js CMS that merges 100% Lighthouse scores with a powerful visual block editor.', 'NextBlock™ is the open-source Next.js CMS that pairs 100% Lighthouse scores with a visual block editor.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'NextBlock™ is the open-source, developer' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ is a holistic platform that unites performance, editorial experience, and developer control so every stakeholder delivers their best work.', 'NextBlock™ brings speed, clear editing, and developer control together so your team can build better web pages.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'NextBlock™ is a holistic platform that u' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Built for 100% Lighthouse scores with global delivery and near-instant FCP.', 'Built for top Lighthouse scores, quick page loads, and fast global delivery.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Built for 100% Lighthouse scores with gl' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'A low-code, Notion-style block editor empowers teams to ship pages without engineering help.', 'A clean block editor gives your team the power to create pages without writing code.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'A low-code, Notion-style block editor em' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Open-source control with a clean Nx monorepo and a typed SDK for limitless customization.', 'Full open-source control with a clean monorepo and typed tools to build custom blocks with ease.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Open-source control with a clean Nx mono' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Every layer of NextBlock™ leans on proven developer-first technology so the platform feels familiar, performant, and trustworthy from day one.', 'NextBlock™ uses tools you already know and trust, so your site stays fast, safe, and easy to run.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Every layer of NextBlock™ leans on prove' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'For Content Creators', 'For Writers and Editors')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'For Content Creators' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Drag-and-drop layouts with a Notion-like interface.', 'Drag and drop blocks in a clean, simple layout.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Drag-and-drop layouts with a Notion-like' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Pre-built hero, feature, testimonial, and callout components.', 'Add heroes, galleries, and quotes with one click.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Pre-built hero, feature, testimonial, an' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Drag-and-drop assets with folders, search, and alt text.', 'Keep your images organized with folders and tags.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Drag-and-drop assets with folders, searc' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Audit trail with one-click restore for every change.', 'Restore any saved version with a single click.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Audit trail with one-click restore for e' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Server Components, ISR, and Edge Functions ready out of the box.', 'Built with Server Components and fast edge caching.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Server Components, ISR, and Edge Functio' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Postgres, Auth, Storage, and Row-Level Security fully wired.', 'Postgres database, user auth, and file storage ready to use.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Postgres, Auth, Storage, and Row-Level S' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Nx-powered monorepo engineered for scale and clean separation.', 'An Nx workspace made for scale and clean code.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Nx-powered monorepo engineered for scale' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'TypeScript SDK with Zod validation for building custom blocks.', 'Build and type new blocks in minutes.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'TypeScript SDK with Zod validation for b' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ Commerce transforms your content platform into a complete e-commerce engine. Products, checkout, multi-currency, taxes, shipping, invoices — all natively integrated into the block editor you already know.', 'NextBlock™ Commerce turns your content site into a full online store. Sell products, take payments, handle taxes, and ship orders — all from the block editor you already use.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'NextBlock™ Commerce transforms your cont' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ Commerce transforms your content platform into a complete e-commerce engine. Products, checkout, multi-currency, taxes, shipping, invoices — all natively integrated into the block editor you already know.', 'NextBlock™ Commerce turns your content site into a full online store. Sell products, take payments, handle taxes, and ship orders — all from the block editor you already use.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'NextBlock™ Commerce transforms your cont' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ Commerce ships a complete e-commerce toolkit so you can go from catalog to checkout without third-party plugins.', 'NextBlock™ Commerce ships a complete online store toolkit so you can go from catalog to checkout with ease.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'NextBlock™ Commerce ships a complete e-c' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Live FX rates, rounding rules, and auto-sync across all currencies.', 'Live exchange rates, round prices, and auto-sync across all currencies.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Live FX rates, rounding rules, and auto-' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Define custom rates or let Stripe Tax handle calculations automatically.', 'Set custom tax rates or let Stripe Tax calculate totals automatically.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Define custom rates or let Stripe Tax ha' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Country and state-level zones with free shipping thresholds.', 'Set shipping rates by country and state, with free shipping rules.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Country and state-level zones with free ' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Stripe for physical goods and Freemius for software licenses with seamless checkout.', 'Use Stripe for physical goods and Freemius for software licenses with simple checkout.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Stripe for physical goods and Freemius f' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Automatic stock decrement on payment with variant-level tracking.', 'Update stock counts when an order is paid, with variant tracking.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Automatic stock decrement on payment wit' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Order management, printable invoices, and customer receipts out of the box.', 'Manage orders, print invoices, and view customer reports with ease.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'Order management, printable invoices, an' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ Cortex AI brings native block-level intelligence directly to your editor. Generate copy, refactor structures, and automate translations in one click, built directly on our high-performance architecture.', 'NextBlock™ Cortex AI helps you write, edit, and translate content right inside the block editor. Draft text, refine layouts, and switch languages with ease.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'NextBlock™ Cortex AI brings native block' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ is building a sustainable open-core roadmap so the platform grows with your business.', 'NextBlock™ is built to grow with your business and help your team succeed.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 1\r\n AND content::text LIKE '%' || 'NextBlock™ is building a sustainable ope' || '%';\r\n\r\n\r\n -- Page 2 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ est le CMS Next.js open-source alliant scores Lighthouse parfaits et éditeur visuel puissant.', 'NextBlock™ est le CMS Next.js open-source alliant scores parfaits et éditeur de blocs visuel.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'NextBlock™ est le CMS Next.js open-sourc' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Sites marketing et docs rendus à l''edge avec des performances irréprochables.', 'Sites marketing et docs rendus à l''edge. Conçus pour la vitesse et un code propre.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Sites marketing et docs rendus à l''edge ' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ unifie performances, expérience éditoriale et contrôle développeur pour que chaque équipe livre son meilleur travail.', 'NextBlock™ réunit la vitesse, la clarté d''édition et le contrôle développeur. Votre équipe peut ainsi créer de meilleures pages web.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'NextBlock™ unifie performances, expérien' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Pensé pour des scores Lighthouse parfaits avec une diffusion mondiale.', 'Conçu pour des scores élevés et une diffusion rapide.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Pensé pour des scores Lighthouse parfait' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Un éditeur façon Notion pour publier sans dépendre des développeurs.', 'Un éditeur simple pour publier sans coder.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Un éditeur façon Notion pour publier san' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Un socle Next.js + Supabase modulaire, extensible et auto-hébergeable.', 'Un socle moderne, souple et très simple à faire grandir.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Un socle Next.js + Supabase modulaire, e' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Chaque couche de NextBlock™ repose sur des technologies éprouvées pour une expérience familière et performante.', 'NextBlock™ s''appuie sur des outils fiables et éprouvés. Votre site reste rapide, robuste et facile à maintenir au quotidien.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Chaque couche de NextBlock™ repose sur d' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Glisser-déposer façon Notion.', 'Glissez et déposez vos blocs dans une mise en page claire.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Glisser-déposer façon Notion.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Héros, galeries, témoignages.', 'Insérez des héros, des galeries et des avis clients en un clic.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Héros, galeries, témoignages.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Dossiers, tags et actions groupées.', 'Rangez vos photos et médias avec des dossiers et des tags.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Dossiers, tags et actions groupées.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Historique et restauration instantanée.', 'Retrouvez et restaurez vos versions précédentes sans stress.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Historique et restauration instantanée.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Server Components, ISR et Edge prêts à l''emploi.', 'Conçu avec Server Components et un cache edge très rapide.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Server Components, ISR et Edge prêts à l' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Postgres, auth, stockage, temps réel.', 'Base Postgres, profils, auth et stockage de fichiers prêts à l''emploi.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Postgres, auth, stockage, temps réel.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Dépendances lisibles et centrales.', 'Une structure claire et bien rangée faite pour grandir sereinement.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Dépendances lisibles et centrales.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Widgets typés et extensibles.', 'Créez et typez vos nouveaux composants en un instant.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Widgets typés et extensibles.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ Commerce transforme votre plateforme de contenu en moteur e-commerce complet. Produits, checkout, multi-devises, taxes, expédition, factures — le tout intégré nativement dans l''éditeur de blocs que vous connaissez déjà.', 'NextBlock™ Commerce transforme votre site de contenu en vraie boutique en ligne. Vendez des articles et recevez des paiements en ligne. Gérez vos taxes et vos envois dans l''éditeur que vous connaissez déjà.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'NextBlock™ Commerce transforme votre pla' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ Commerce livre une boîte à outils e-commerce complète pour aller du catalogue au paiement sans plugins tiers.', 'NextBlock™ Commerce propose une boîte à outils complète. Allez du catalogue au paiement sans aucun plugin externe.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'NextBlock™ Commerce livre une boîte à ou' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Taux de change en temps réel, modes d''arrondi, prix charme et synchronisation automatique sur toutes les devises.', 'Taux de change en direct, prix ronds et mise à jour automatique sur toutes les devises.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Taux de change en temps réel, modes d''ar' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Taux manuels empilés (TPS + TVQ) ou calcul automatique via Stripe Tax — à vous de choisir.', 'Définissez vos taxes ou laissez Stripe Tax calculer les montants finaux.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Taux manuels empilés (TPS + TVQ) ou calc' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Résolution par pays et état, tarification par devise et seuils de livraison gratuite.', 'Ajustez les frais de port par pays et état avec des règles d''envoi gratuit.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Résolution par pays et état, tarificatio' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Stripe pour les produits physiques, Freemius pour les licences numériques — checkout intelligent avec validation d''inventaire.', 'Paiement Stripe pour les biens et Freemius pour les licences logicielles.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Stripe pour les produits physiques, Free' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Déduction automatique des quantités au paiement avec gestion des stocks par variante.', 'Mettez à jour les stocks dès qu''un achat est validé, avec suivi des variantes.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Déduction automatique des quantités au p' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Gestion du cycle de vie des commandes, numérotation stable des factures et rapports de commandes exportables.', 'Suivez chaque commande, imprimez des factures et téléchargez vos bilans en toute simplicité.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Gestion du cycle de vie des commandes, n' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ Cortex AI apporte une intelligence native au niveau des blocs directement dans votre éditeur. Générez du texte, restructurez vos contenus et automatisez les traductions en un clic, le tout propulsé par notre architecture haute performance.', 'NextBlock™ Cortex AI vous aide à rédiger, corriger et traduire vos textes au cœur de l''éditeur de blocs. Créez des ébauches, améliorez vos titres et passez d''une langue à l''autre en un clin d''œil.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'NextBlock™ Cortex AI apporte une intelli' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ construit une feuille de route open-core durable qui évolue avec votre activité.', 'NextBlock™ est pensé pour accompagner la croissance de votre entreprise. Nous voulons faire réussir vos projets web.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'NextBlock™ construit une feuille de rout' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Le commerce arrive en premier, puis l''ecosysteme s''etend avec des plugins, des blocs et des modules construits par les partenaires.', 'Le commerce ouvre la voie. Notre écosystème grandit avec de nouveaux plugins, des blocs utiles et des modules partenaires.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Le commerce arrive en premier, puis l''ec' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Transformez votre site en vitrine composable avec produits, checkout, tarification multi-devise, taxes automatiques et blocs commerce relies a votre contenu editorial.', 'Ajoutez une boutique complète à côté de vos articles. Profitez de produits, du paiement en ligne, de devises et de taxes sans effort.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Transformez votre site en vitrine compos' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Une marketplace communautaire ouvrira la voie a la publication, la vente et la distribution de blocs, themes, integrations et modules partenaires.', 'Un espace partagé permet aux développeurs de publier et vendre des blocs sur mesure, des thèmes et des outils connectés.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'Une marketplace communautaire ouvrira la' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ se construit en public. Ajoutez une étoile, partagez vos retours et façonnez l''avenir du CMS orienté performance.', 'NextBlock™ avance en public. Mettez une étoile sur le dépôt, partagez vos avis et façonnez la suite avec nous.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 2\r\n AND content::text LIKE '%' || 'NextBlock™ se construit en public. Ajout' || '%';\r\n\r\n\r\n -- Page 3 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Deep dives into performance, DX, and visual editing.</h2>', '<h1 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>The NextBlock Journal: Performance, DX, and Visual Editing</h1>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 3\r\n AND content::text LIKE '%' || '<h2 class=''text-4xl md:text-5xl font-bol' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<p class=''text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed''>Explore architectural walkthroughs, Supabase recipes, and block editor experiments written by the Nextblock core team.</p>', '<p class=''text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed''>Explore practical guides, architecture deep dives, and editor workflows written by the NextBlock team and community.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl''> <p>Welcome to the NextBlock Journal. This is your home for in-depth technical guides, release notes, and real-world web architecture patterns. Whether you are launching your first Next.js site or scaling an online store, our articles give you clear, tested steps to help you build faster.</p> <p>We believe modern web projects need developer freedom and a simple editing experience. Our engineering posts show how to pair Next.js 16 with Supabase Postgres, edge caching, and server actions without complex glue code. You will learn how to keep your Lighthouse score at 100% while giving teams a rich, block-based writing experience.</p> <p>Here are the key topics we cover across our editorial library:</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Getting Started Guides:</strong> Simple setup walkthroughs for one-click Vercel deploys, local Docker environments, and custom cloud stacks.</li> <li><strong>Architecture & Performance:</strong> Deep dives into fast page rendering, clean CSS delivery, image optimization, and safe database migrations.</li> <li><strong>E-Commerce Workflows:</strong> Practical patterns for multi-currency pricing, automated tax sync, stock tracking, and provider checkout flows.</li> <li><strong>AI Content Tools:</strong> How to use Cortex AI to generate structured blocks, manage provider keys, and speed up routine translation work.</li> </ul> <p>Our team updates this collection regularly as we release new core features and modules. We test our code on live sites before publishing. Each post includes full technical explanations and architecture diagrams so you can apply the solutions to your own web stack.</p> <p>Have a topic you want us to cover or want to share your own case study? Join our community on GitHub or reach out to our team anytime. We welcome questions, ideas, and pull requests from all builders. You can subscribe to updates or start reading our latest published articles below.</p> </div>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 3\r\n AND content::text LIKE '%' || '<p class=''text-slate-300 text-lg max-w-x' || '%';\r\n\r\n\r\n -- Page 4 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Plongées dans la performance, l''expérience dev et l''édition visuelle.</h2>', '<h1 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Le Journal NextBlock : Performance, Expérience Dev et Édition</h1>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 4\r\n AND content::text LIKE '%' || '<h2 class=''text-4xl md:text-5xl font-bol' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<p class=''text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left text-slate-300 leading-relaxed''>Walkthroughs d''architecture, recettes Supabase et expérimentations éditeur écrits par l''équipe Nextblock.</p>', '<p class=''text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left text-slate-300 leading-relaxed''>Retrouvez des guides pratiques, des études d''architecture et des conseils pour éditer vos contenus avec NextBlock.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl''> <p>Bienvenue sur le Journal NextBlock. Cet espace rassemble nos guides techniques détaillés. Vous y trouverez les nouveautés de chaque version et les meilleures pratiques du web moderne. Vous lancez votre premier site Next.js ? Vous faites grandir une boutique en ligne ? Nos articles vous donnent des étapes simples et vérifiées pour construire plus vite.</p> <p>Nous pensons qu''un bon site web doit offrir toute la liberté aux développeurs. Il doit aussi apporter une vraie simplicité aux équipes éditoriales. Nos articles techniques expliquent comment marier Next.js 16 avec Supabase Postgres, le cache edge et les Server Actions. Tout cela se fait sans code superflu. Vous découvrirez comment maintenir un score Lighthouse parfait. En même temps, votre équipe profite d''un éditeur de blocs visuel et très agréable.</p> <p>Voici les principaux sujets abordés dans notre bibliothèque éditoriale :</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Guides de démarrage :</strong> Des tutoriels pas à pas pour déployer sur Vercel en un clic, lancer une pile Docker locale ou installer votre propre cloud.</li> <li><strong>Architecture et performance :</strong> Des explications claires sur le rendu rapide des pages, les styles en ligne, l''optimisation des images et les migrations sûres.</li> <li><strong>Commerce en ligne :</strong> Des conseils concrets pour gérer plusieurs devises, synchroniser les taxes, suivre les stocks et réussir vos paiements.</li> <li><strong>Outils IA pour le contenu :</strong> Comment utiliser Cortex AI pour créer des blocs bien typés, garder le contrôle de vos clés et traduire vos pages en un instant.</li> </ul> <p>Notre équipe enrichit cette collection au fil des mises à jour du projet. Nous testons chaque exemple de code sur des sites réels avant publication. Chaque tutoriel propose des explications complètes pour adapter facilement ces solutions à votre propre projet.</p> <p>Vous avez une idée de sujet ou un retour d''expérience à partager ? Rejoignez notre communauté sur GitHub ou contactez notre équipe. Nous accueillons avec grand plaisir vos questions, vos idées et vos contributions. Parcourez dès maintenant nos derniers articles ci-dessous.</p> </div>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 4\r\n AND content::text LIKE '%' || '<p class=''text-lg max-w-xl mx-auto md:mx' || '%';\r\n\r\n\r\n -- Page 5 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Community Driven</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ is built in the open. We rely on developers and editors like you to help us define the roadmap. Whether it''s a bug report, a feature request, or just a shoutout, every message helps us move faster.</p></div>', '<div class=''mt-10 text-slate-300 space-y-4 max-w-3xl mx-auto''> <h2 class=''text-2xl font-bold text-white mb-4 text-center''>Open Source & Community Driven</h2> <p>NextBlock™ is built entirely in the open. We work closely with developers, editors, and teams who use the platform every day. Whether you found a bug, want to request a new feature, or need advice on your project setup, our team is here to help you move forward.</p> <p>We read every message sent through this form. Here is what you can expect when reaching out to our team:</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Fast Response Times:</strong> Our core maintainers typically review and reply to inquiries within one to two business days.</li> <li><strong>Technical Support:</strong> For public bugs or community questions, our GitHub Discussions and Issues boards offer fast answers from the entire community.</li> <li><strong>Partnership and Modules:</strong> If your team wants to sponsor a roadmap module or build a custom integration, we can schedule a direct call.</li> <li><strong>Security Disclosures:</strong> For responsible disclosure of potential security vulnerabilities, please flag your message as urgent so we can triage it immediately.</li> </ul> <p>Before submitting, please make sure your email address is typed correctly so our team can get back to you. If your question is about a specific code error, including your environment details and reproduction steps helps us find a solution much faster.</p> <p>You can also connect with us directly on GitHub, Discord, or X (formerly Twitter). We host regular community discussions and share sneak peeks of upcoming platform features. If you are building a custom client project with NextBlock, we would love to hear about your experience.</p> <p>We value your time and privacy. We never share your contact information or use your email for marketing without your consent. Send us a message using the form below and we will get back to you shortly.</p> </div>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 5\r\n AND content::text LIKE '%' || '<div class=''max-w-2xl mx-auto text-cente' || '%';\r\n\r\n\r\n -- Page 6 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Communautaire</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ est construit en public. Nous comptons sur les développeurs et éditeurs comme vous pour définir notre roadmap. Qu''il s''agisse d''un bug, d''une suggestion ou d''un simple salut, chaque message compte.</p></div>', '<div class=''mt-10 text-slate-300 space-y-4 max-w-3xl mx-auto''> <h2 class=''text-2xl font-bold text-white mb-4 text-center''>Open Source & Communautaire</h2> <p>NextBlock™ est construit entièrement en public. Nous travaillons avec des développeurs et des équipes qui utilisent le CMS chaque jour. Vous avez trouvé un bug ? Vous souhaitez suggérer une fonction ? Vous avez besoin d''aide pour votre installation ? Notre équipe est là pour vous guider.</p> <p>Nous lisons attentivement chaque message reçu via ce formulaire. Voici nos engagements pour vous répondre au mieux :</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Délais de réponse rapides :</strong> Nos mainteneurs étudient et répondent aux demandes en un ou deux jours ouvrés.</li> <li><strong>Support technique :</strong> Pour toute question publique, nos forums GitHub Discussions et Issues offrent une aide rapide de la communauté.</li> <li><strong>Partenariats et modules :</strong> Votre équipe veut sponsoriser un module ou concevoir une intégration sur mesure ? Nous pouvons planifier un échange direct.</li> <li><strong>Signalements de sécurité :</strong> Pour toute alerte de sécurité, signalez votre message comme prioritaire pour un traitement immédiat.</li> </ul> <p>Avant d''envoyer votre message, vérifiez bien votre adresse email. Notre équipe pourra ainsi vous répondre rapidement. Si votre demande concerne une erreur technique, mentionnez votre environnement et les étapes pour la reproduire. Cela nous aide à trouver une solution efficace.</p> <p>Vous pouvez aussi échanger avec nous sur GitHub, Discord ou X. Nous y partageons des nouvelles et les coulisses des prochaines versions. Vous développez un projet client avec NextBlock ? Racontez-nous votre aventure. Notre équipe aime voir ce que vous créez au quotidien. Chaque projet est unique. Prenez le temps de nous dire ce qui vous plaît et ce qui vous manque. Vos retours nous aident à faire un meilleur outil pour tout le monde.</p> <p>Nous respectons votre vie privée. Vos coordonnées ne sont jamais cédées ni réutilisées sans votre accord. Écrivez-nous ci-dessous et nous vous répondrons très vite.</p> </div>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 6\r\n AND content::text LIKE '%' || '<div class=''max-w-2xl mx-auto text-cente' || '%';\r\n\r\n\r\n -- Page 7 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Discover our premium selection of developer tools and digital commerce solutions.</p>', '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Discover our selection of official commercial add-ons and developer tools for NextBlock CMS.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''> <h2 class=''text-2xl font-bold text-white mb-4''>Power Up Your Web Projects</h2> <p>Welcome to the official NextBlock™ digital store. Here you can purchase licenses for our premium packages, including NextBlock™ Commerce and NextBlock™ Cortex AI. Every commercial license helps fund full-time open-source development on our core CMS while giving your team advanced tools to launch high-performance websites faster.</p> <p>Our premium modules are designed to feel native from day one. You get clean code that fits right into your existing NextBlock project without third-party plugins or complex configuration. When you purchase a license from our store, you receive instant access to everything you need:</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Full Source Code:</strong> Inspect, customize, and adapt the modules to your exact requirements.</li> <li><strong>Perpetual Use:</strong> Use the software for your project with full peace of mind.</li> <li><strong>Automated Updates:</strong> Enjoy smooth updates that match each new release of NextBlock and Next.js.</li> <li><strong>Secure Checkout:</strong> Payments are processed safely with Stripe and Freemius with instant receipt generation.</li> </ul> <p>Whether you need multi-currency store features, automated sales tax calculations, or AI block generation in your editor, our modules deliver tested solutions that keep your Lighthouse scores at 100%.</p> <p>All purchases include dedicated onboarding resources, detailed developer docs, and friendly technical support. If you ever run into an issue or need help wiring up a provider webhook, our core engineers are ready to assist you. We also offer a thirty-day refund policy so you can try our tools risk-free.</p> <p>Have questions about license tiers, volume pricing, team seats, or custom agency usage? Contact our support team anytime. Our team is here to answer your questions and help your developers succeed. We are happy to help you pick the best plan for your company. Browse our featured products below to get started today.</p> </div>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 7\r\n AND content::text LIKE '%' || '<p style=\"text-align: center; color: var' || '%';\r\n\r\n\r\n -- Page 8 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Decouvrez notre selection premium d outils de developpement.</p>', '<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Découvrez nos extensions officielles et nos outils pour développeurs conçus pour le CMS NextBlock.</p> <div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''> <h2 class=''text-2xl font-bold text-white mb-4''>Accélérez vos projets web</h2> <p>Bienvenue sur la boutique officielle de NextBlock™. Vous pouvez acheter ici des licences pour nos modules professionnels, comme NextBlock™ Commerce et NextBlock™ Cortex AI. Chaque achat aide à financer le travail open-source sur le cœur du CMS. Il donne aussi à votre équipe des outils de pointe pour créer des sites rapides et fiables.</p> <p>Nos modules premium s''intègrent sans effort à votre projet existant. Vous profitez d''un code propre et bien testé, sans plugin externe lourd ni réglage complexe. En choisissant nos outils, vous profitez immédiatement de nombreux avantages :</p> <ul class=''space-y-2 list-disc pl-5''> <li><strong>Code source complet :</strong> Lisez, adaptez et faites évoluer chaque bloc selon vos besoins métier.</li> <li><strong>Licence perpétuelle :</strong> Utilisez le code sur votre projet en toute sérénité.</li> <li><strong>Mises à jour suivies :</strong> Recevez les nouvelles versions au rythme de Next.js et de NextBlock.</li> <li><strong>Paiement sécurisé :</strong> Les achats passent par Stripe et Freemius avec facture instantanée.</li> </ul> <p>Vous voulez vendre dans plusieurs devises ? Vous avez besoin du calcul automatique des taxes ? Vous voulez générer des blocs avec l''IA ? Nos modules offrent des solutions prêtes à l''emploi qui préservent vos scores Lighthouse à 100%.</p> <p>Chaque commande donne accès à une documentation claire et à notre support technique. Si vous avez besoin d''aide pour brancher un webhook ou un mode de paiement, nos développeurs vous répondent rapidement. Notre équipe est à vos côtés pour vous faire gagner du temps. Nous proposons aussi une garantie satisfait ou remboursé de trente jours.</p> <p>Vous avez des questions sur nos tarifs, les licences agence ou les remises en volume ? Écrivez à notre équipe à tout moment. Nous vous guiderons avec grand plaisir vers l''offre idéale pour votre entreprise. Découvrez dès aujourd''hui l''ensemble de nos produits ci-dessous pour bien démarrer votre projet.</p> </div>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 8\r\n AND content::text LIKE '%' || '<p style=\"text-align: center; color: var' || '%';\r\n\r\n\r\n -- Page 9 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ CMS (\"we\", \"us\", or \"our\") respects your privacy and is committed to protecting your personal information in accordance with Quebec''s <em>Act respecting the protection of personal information in the private sector</em> (Law 25), the federal <em>Personal Information Protection and Electronic Documents Act</em> (PIPEDA), and Canada''s Anti-Spam Legislation (CASL).', 'NextBlock™ CMS (\"we\", \"us\", or \"our\") respects your privacy. We protect your personal information under Quebec''s Law 25, the federal PIPEDA act, and Canada''s Anti-Spam Legislation (CASL).')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || 'NextBlock™ CMS (\"we\", \"us\", or \"our\") re' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Our Privacy Officer is responsible for our compliance with applicable privacy laws. You may reach them at', 'Our Privacy Officer oversees our compliance with privacy laws. You can reach them at')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || 'Our Privacy Officer is responsible for o' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<li><strong>Account information</strong> &mdash; name, email address, and credentials when you register.</li>', '<li><strong>Account information:</strong> Your name, email address, and login details when you register.</li>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<li><strong>Account information</strong>' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<li><strong>Usage and device data</strong> &mdash; collected only with your consent through analytics technologies.</li>', '<li><strong>Usage and device data:</strong> Collected only with your consent through analytics tools.</li>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<li><strong>Usage and device data</stron' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<li><strong>Communications</strong> &mdash; messages you send us and your marketing preferences.</li>', '<li><strong>Communications:</strong> Messages you send us and your newsletter choices.</li>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<li><strong>Communications</strong> &mda' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>3. Why we collect it and your consent</h2>\r\n<p>We collect personal information for clearly identified purposes: to provide and secure our services, to communicate with you, and &mdash; only with your express, opt-in consent &mdash; for analytics and marketing. Consistent with Law 25, non-essential cookies and trackers remain disabled until you actively accept them, and you may withdraw your consent at any time.</p>', '<h2>3. Why we collect data and your consent</h2> <p>We collect personal information for clear reasons: to provide our services, keep accounts secure, and reply to your messages. We use analytics and marketing tools only with your direct, opt-in consent. Under Law 25, optional cookies stay off until you choose to accept them. You can withdraw your consent at any time.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<h2>3. Why we collect it and your consen' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Strictly necessary cookies keep the site working and require no consent. Analytics and marketing technologies are loaded <strong>only after</strong> you opt in through our consent banner. Your choice is recorded so we can honour it and demonstrate accountability.', 'Essential cookies keep the site running and require no consent. Analytics and marketing cookies load only after you opt in via our cookie banner. We record your choice to honor it and follow privacy rules.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || 'Strictly necessary cookies keep the site' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'We do not sell your personal information. We share it only with service providers who help us operate the platform under contractual confidentiality obligations, or where required by law.', 'We do not sell your personal data. We share it only with trusted service providers who help us run the platform under strict confidentiality agreements, or when required by law.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || 'We do not sell your personal information' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>6. Retention</h2>\r\n<p>We keep personal information only for as long as necessary to fulfil the purposes described above or as required by law, after which it is securely destroyed or anonymized.</p>', '<h2>6. Data retention</h2> <p>We keep personal information only as long as needed for the purposes described above or as required by law. After that, we securely delete or anonymize your data.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<h2>6. Retention</h2>\r\n<p>We keep person' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Subject to applicable law, you have the right to access, rectify, and delete your personal information, to withdraw consent, to data portability, and to be informed about automated processing. To exercise these rights, contact our Privacy Officer at', 'Under privacy laws, you have the right to view, correct, and delete your personal information. You can also withdraw consent or ask for a portable copy of your data. To use these rights, write to our Privacy Officer at')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || 'Subject to applicable law, you have the ' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>8. Commercial electronic messages (CASL)</h2>\r\n<p>We send commercial electronic messages only with your consent. Every message identifies us and includes a working unsubscribe mechanism that we honour promptly.</p>', '<h2>8. Commercial electronic messages</h2> <p>We send commercial emails only with your permission. Every email clearly identifies us and includes an easy unsubscribe link that takes effect right away.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<h2>8. Commercial electronic messages (C' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>9. Safeguards</h2>\r\n<p>We use appropriate physical, organizational, and technological measures &mdash; including encryption in transit and access controls &mdash; to protect personal information against loss, theft, and unauthorized access.</p>', '<h2>9. Security safeguards</h2> <p>We use strong technical and physical protections to keep your data safe. These include encrypted connections and strict access controls to prevent loss, theft, and unauthorized access.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<h2>9. Safeguards</h2>\r\n<p>We use approp' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ CMS is free, open-source software distributed under the GNU Affero General Public License v3. When you self-host NextBlock, you are the operator responsible for the personal information processed by your own deployment, and this policy serves as a starting point you may adapt to your organization.', 'NextBlock™ CMS is free open-source software under the AGPLv3 license. When you self-host NextBlock, you control your own server. You are responsible for the personal data on your deployment, and this policy is a helpful model you can adapt.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || 'NextBlock™ CMS is free, open-source soft' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>11. Changes to this policy</h2>\r\n<p>We may update this policy from time to time. Material changes will be communicated through the site, and the \"last updated\" date will be revised.</p>', '<h2>11. Policy changes</h2> <p>We may update this policy over time. We announce important changes on our site and update the revision date at the top.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || '<h2>11. Changes to this policy</h2>\r\n<p>' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Questions or complaints? Contact NextBlock™ CMS at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. You may also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.', 'Have questions or complaints? Contact NextBlock™ at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. You can also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content::text LIKE '%' || 'Questions or complaints? Contact NextBlo' || '%';\r\n\r\n\r\n -- Page 10 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ CMS (« nous ») respecte votre vie privée et s''engage à protéger vos renseignements personnels conformément à la <em>Loi sur la protection des renseignements personnels dans le secteur privé</em> du Québec (Loi 25), à la <em>Loi sur la protection des renseignements personnels et les documents électroniques</em> (LPRPDE) et à la Loi canadienne anti-pourriel (LCAP).', 'NextBlock™ CMS (« nous ») respecte votre vie privée. Nous protégeons vos renseignements personnels selon la Loi 25 du Québec, la loi fédérale LPRPDE et les règles canadiennes anti-pourriel (LCAP).')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || 'NextBlock™ CMS (« nous ») respecte votre' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Notre responsable de la protection des renseignements personnels veille au respect des lois applicables. Vous pouvez le joindre à', 'Notre responsable veille au respect des règles de confidentialité. Vous pouvez lui écrire à')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || 'Notre responsable de la protection des r' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>2. Renseignements que nous recueillons</h2>\r\n<ul>\r\n <li><strong>Renseignements de compte</strong> &mdash; nom, adresse courriel et identifiants lors de l''inscription.</li>\r\n <li><strong>Données d''utilisation et d''appareil</strong> &mdash; recueillies uniquement avec votre consentement au moyen de technologies d''analyse.</li>\r\n <li><strong>Communications</strong> &mdash; les messages que vous nous envoyez et vos préférences marketing.</li>\r\n</ul>', '<h2>2. Renseignements recueillis</h2> <ul> <li><strong>Renseignements de compte :</strong> Votre nom, votre courriel et vos accès lors de l''inscription.</li> <li><strong>Données d''utilisation et d''appareil :</strong> Recueillies avec votre accord via nos outils de mesure.</li> <li><strong>Communications :</strong> Vos messages reçus et vos choix de suivi par courriel.</li> </ul>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || '<h2>2. Renseignements que nous recueillo' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<p>Nous recueillons des renseignements personnels à des fins clairement déterminées : fournir et sécuriser nos services, communiquer avec vous et &mdash; uniquement avec votre consentement exprès &mdash; à des fins d''analyse et de marketing. Conformément à la Loi 25, les témoins et traceurs non essentiels demeurent désactivés tant que vous ne les avez pas acceptés, et vous pouvez retirer votre consentement en tout temps.</p>', '<p>Nous recueillons vos données pour des motifs clairs : faire fonctionner nos services, sécuriser les accès et vous répondre. Les outils d''analyse et de suivi ne s''activent qu''avec votre accord clair. Selon la Loi 25, les témoins optionnels restent coupés tant que vous ne les acceptez pas. Vous pouvez retirer votre accord en tout temps.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || '<p>Nous recueillons des renseignements p' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Les témoins strictement nécessaires assurent le fonctionnement du site et ne requièrent aucun consentement. Les technologies d''analyse et de marketing ne sont chargées qu''<strong>après</strong> votre consentement explicite. Votre choix est enregistré afin de le respecter.', 'Les témoins essentiels assurent le bon fonctionnement du site. Ils ne demandent aucun accord préalable. Les témoins d''analyse se chargent seulement après votre choix sur notre bandeau. Nous gardons votre choix en mémoire pour le respecter.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || 'Les témoins strictement nécessaires assu' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>5. Communication à des tiers</h2>\r\n<p>Nous ne vendons pas vos renseignements personnels. Nous ne les communiquons qu''à des fournisseurs qui nous aident à exploiter la plateforme, sous obligation de confidentialité, ou lorsque la loi l''exige.</p>', '<h2>5. Partage et communication</h2> <p>Nous ne vendons jamais vos données personnelles. Nous les partageons uniquement avec des prestataires de confiance qui nous aident à faire tourner le site sous contrat de secret, ou si la loi l''impose.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || '<h2>5. Communication à des tiers</h2>\r\n<' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>6. Conservation</h2>\r\n<p>Nous ne conservons les renseignements personnels que le temps nécessaire aux fins décrites ou exigé par la loi, après quoi ils sont détruits ou anonymisés de façon sécuritaire.</p>', '<h2>6. Durée de conservation</h2> <p>Nous gardons vos données seulement le temps utile pour les buts décrits ou selon la loi. Ensuite, nous les effaçons ou nous les rendons anonymes de façon sûre.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || '<h2>6. Conservation</h2>\r\n<p>Nous ne con' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Sous réserve de la loi applicable, vous avez le droit d''accéder à vos renseignements, de les rectifier et de les supprimer, de retirer votre consentement, à la portabilité de vos données et d''être informé du traitement automatisé. Pour exercer ces droits, écrivez à', 'Vous avez le droit de lire, de corriger et de faire effacer vos données. Vous pouvez aussi retirer votre accord ou demander une copie de vos données. Pour exercer vos droits, écrivez à notre responsable à')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || 'Sous réserve de la loi applicable, vous ' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>8. Messages électroniques commerciaux (LCAP)</h2>\r\n<p>Nous n''envoyons des messages électroniques commerciaux qu''avec votre consentement. Chaque message nous identifie et comporte un mécanisme de désabonnement fonctionnel que nous respectons rapidement.</p>', '<h2>8. Messages électroniques</h2> <p>Nous envoyons des courriels informatifs seulement avec votre accord. Chaque courriel montre notre nom et propose un lien simple pour vous désabonner d''un clic.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || '<h2>8. Messages électroniques commerciau' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>9. Mesures de sécurité</h2>\r\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées &mdash; dont le chiffrement en transit et le contrôle des accès &mdash; pour protéger vos renseignements.</p>', '<h2>9. Mesures de sécurité</h2> <p>Nous utilisons des moyens techniques et physiques solides pour garder vos données en sûreté. Cela comprend des échanges chiffrés et un contrôle strict des accès contre toute fuite ou vol.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || '<h2>9. Mesures de sécurité</h2>\r\n<p>Nous' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la licence publique générale GNU Affero v3. Lorsque vous hébergez NextBlock vous-même, vous êtes l''exploitant responsable des renseignements personnels traités par votre propre instance, et la présente politique vous sert de point de départ adaptable à votre organisation.', 'NextBlock™ CMS est un logiciel libre sous licence AGPLv3. Si vous hébergez NextBlock vous-même, vous gérez votre propre serveur. Vous êtes responsable des données sur votre instance, et ce texte est un modèle que vous pouvez adapter.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || 'NextBlock™ CMS est un logiciel libre et ' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>11. Modifications</h2>\r\n<p>Nous pouvons mettre à jour cette politique. Les changements importants seront communiqués sur le site et la date de mise à jour sera révisée.</p>', '<h2>11. Mises à jour</h2> <p>Nous pouvons mettre à jour ce texte au fil du temps. Les changements notables seront affichés sur le site avec la nouvelle date en tête de page.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || '<h2>11. Modifications</h2>\r\n<p>Nous pouv' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi vous adresser à la Commission d''accès à l''information du Québec.', 'Une question ou un avis ? Écrivez à NextBlock™ à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi joindre la Commission d''accès à l''information du Québec.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content::text LIKE '%' || 'Des questions ou des plaintes ? Contacte' || '%';\r\n\r\n\r\n -- Page 12 Block Updates\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'En accédant à NextBlock™ CMS et aux services que nous fournissons (les « Services ») ou en les utilisant, vous acceptez d''être lié par les présentes conditions d''utilisation. Si vous n''êtes pas d''accord, n''utilisez pas les Services.', 'En utilisant le CMS NextBlock™ et les services associés (les « Services »), vous acceptez ces conditions d''utilisation. Si vous refusez ces règles, veuillez ne pas utiliser les Services.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || 'En accédant à NextBlock™ CMS et aux serv' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>2. Logiciel libre et à code source ouvert</h2>\r\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou, à votre choix, toute version ultérieure. Vous êtes libre d''exécuter, d''étudier, de partager et de modifier le logiciel selon les termes de cette licence. Une copie de la licence est fournie avec le logiciel et est aussi disponible à <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>', '<h2>2. Logiciel libre et code source ouvert</h2> <p>NextBlock™ CMS est un logiciel libre sous <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou toute version ultérieure. Vous pouvez lancer, étudier, copier et faire évoluer le code selon cette licence. Le texte complet se trouve avec le code et en ligne sur <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || '<h2>2. Logiciel libre et à code source o' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>3. Disponibilité du code source</h2>\r\n<p>Conformément à l''article 13 de l''AGPL-3.0, si vous exploitez une version modifiée de NextBlock™ CMS et la rendez accessible à des utilisateurs sur un réseau, vous devez offrir clairement à ces utilisateurs l''accès au code source correspondant de votre version modifiée, gratuitement, par un moyen usuel de copie de logiciels.</p>', '<h2>3. Partage du code source</h2> <p>Selon l''article 13 de la licence AGPL-3.0, si vous modifiez NextBlock™ CMS et le mettez en ligne pour des usagers sur un réseau, vous devez offrir l''accès libre et sans frais au code source de votre version modifiée.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || '<h2>3. Disponibilité du code source</h2>' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>4. Marques de commerce</h2>\r\n<p>L''AGPL-3.0 accorde de larges droits sur le code source du logiciel, mais <strong>n''accorde aucun droit</strong> sur nos noms commerciaux, marques de commerce ou marques de service. « NextBlock™ », le nom NextBlock™ CMS et les logos associés demeurent notre propriété et ne peuvent être utilisés d''une manière laissant entendre une approbation ou une affiliation sans notre autorisation écrite préalable.</p>', '<h2>4. Marques et logos</h2> <p>La licence AGPL-3.0 donne de larges droits sur le code. Mais <strong>elle ne donne aucun droit</strong> sur nos noms et marques. Les termes « NextBlock™ », NextBlock™ CMS et les logos restent notre bien exclusif. Ils ne peuvent être repris sans accord écrit préalable.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || '<h2>4. Marques de commerce</h2>\r\n<p>L''AG' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>5. Comptes et utilisation acceptable</h2>\r\n<p>Si vous créez un compte, vous êtes responsable de la protection de vos identifiants et de toute activité effectuée à partir de votre compte, et vous vous engagez à nous aviser rapidement de toute utilisation non autorisée. Vous vous engagez à ne pas détourner les Services, notamment en tentant de les perturber, d''y accéder sans autorisation ou de les utiliser à des fins illégales.</p>', '<h2>5. Comptes et bon usage</h2> <p>Si vous ouvrez un compte, vous gardez la garde de vos accès et de toute action faite sous votre nom. Vous devez nous prévenir vite en cas d''usage non permis. Vous vous engagez à ne pas bloquer les Services, ne pas forcer les accès et ne pas agir contre la loi.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || '<h2>5. Comptes et utilisation acceptable' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Comme l''énonce l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou implicite, y compris, sans s''y limiter, les garanties implicites de qualité marchande et d''adéquation à un usage particulier. Vous assumez l''entièreté du risque quant à la qualité et au rendement du logiciel.', 'Selon l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou tacite. Cela inclut les garanties de vente ou d''usage pour un besoin précis. Vous prenez sur vous les risques liés au bon emploi du logiciel.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || 'Comme l''énonce l''article 15 de l''AGPL-3.' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>7. Limitation de responsabilité</h2>\r\n<p>Comme l''énonce l''article 16 de l''AGPL-3.0, et dans toute la mesure permise par la loi applicable, en aucun cas un titulaire de droits d''auteur ou toute autre partie qui modifie ou transmet le logiciel ne saurait être tenu responsable envers vous de dommages, y compris tout dommage général, spécial, accessoire ou consécutif découlant de l''utilisation ou de l''impossibilité d''utiliser le logiciel.</p>', '<h2>7. Limite de responsabilité</h2> <p>Selon l''article 16 de l''AGPL-3.0 et dans la limite permise par la loi, aucun auteur ou tiers modifiant le code ne peut être tenu pour responsable de vos pertes ou dommages liés à l''usage du logiciel.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || '<h2>7. Limitation de responsabilité</h2>' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Les présentes conditions sont régies par les lois de la province de Québec et les lois fédérales du Canada qui y sont applicables, sans égard aux règles de conflit de lois. Rien dans les présentes conditions ne limite les droits impératifs de protection du consommateur dont vous pourriez bénéficier en vertu de ces lois.', 'Ces conditions suivent les lois de la province de Québec et les lois du Canada applicables. Rien ici ne réduit vos droits stricts de consommateur selon ces lois.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || 'Les présentes conditions sont régies par' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, '<h2>9. Modifications</h2>\r\n<p>Nous pouvons réviser ces conditions de temps à autre. Les changements importants seront communiqués au moyen des Services, et l''utilisation continue des Services après leur entrée en vigueur vaut acceptation.</p>', '<h2>9. Mises à jour</h2> <p>Nous pouvons adapter ces règles au fil du temps. Les changements notables passeront sur le site. Votre usage continu vaut accord avec les nouvelles règles.</p>')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || '<h2>9. Modifications</h2>\r\n<p>Nous pouvo' || '%';\r\n\r\n UPDATE public.blocks\r\n SET content = replace(content::text, 'Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.', 'Une question sur ces conditions ? Écrivez à NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.')::jsonb,\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content::text LIKE '%' || 'Des questions sur ces conditions ? Conta' || '%';\r\n\r\nEND $$;\r\n"
181
181
  },
182
182
  {
183
183
  "version": "00000000000033",
184
184
  "name": "00000000000033_seed_seo_score_optimizations_p2.sql",
185
- "sql": "-- Migration: 00000000000033_seed_seo_score_optimizations_p2.sql\n-- Description: Complete 100/100 Page SEO optimization for shop/boutique, legal pages, and setup/updating posts.\n-- Safety: Forward-only, content-guarded updates that only touch matching seeded copy.\n\nDO $$\nBEGIN\n -----------------------------------------------------------------------------\n -- 1. Post Metadata (Post 8 & Post 9)\n -----------------------------------------------------------------------------\n UPDATE public.posts\n SET meta_description = 'A single command keeps your NextBlock install updated on Docker, Supabase, or a cloned repo. Learn how it works and keeps your data safe.',\n updated_at = now()\n WHERE slug = 'how-updating-works'\n AND meta_description LIKE 'Update NextBlock in one step%';\n\n UPDATE public.posts\n SET meta_title = 'Mises à jour NextBlock : une commande pour chaque site',\n meta_description = 'Une seule commande met à jour NextBlock sur Docker, Supabase ou un dépôt cloné. Découvrez le fonctionnement et la protection de vos données.',\n updated_at = now()\n WHERE slug = 'comment-fonctionnent-les-mises-a-jour'\n AND meta_title LIKE 'Mettre à jour NextBlock%';\n\n -----------------------------------------------------------------------------\n -- 2. Post 4 Setup FR Card Headings (H3 -> H2 to prevent heading level skip)\n -----------------------------------------------------------------------------\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Voici quatre façons de démarrer, <strong>classées de la plus simple à la plus technique</strong> : la première tient en un clic, la dernière donne le code source complet pour celles et ceux qui veulent participer à NextBlock. Elles aboutissent toutes au même endroit — un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur.</p>\n\n<div class=''mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400''>\n <span class=''flex-shrink-0''>Le plus simple</span>\n <span class=''h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400''></span>\n <span class=''flex-shrink-0''>Le plus de contrôle</span>\n</div>\n\n<div class=''grid gap-5 md:grid-cols-2 my-6''>\n <a href=''#one-click-vercel'' class=''block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10''>\n <div class=''flex items-center justify-between mb-4''>\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white''>1</span>\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-blue-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\n </div>\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Le plus simple &middot; un clic</p>\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déployer sur Vercel</h2>\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un site de production en ligne avec une base de données gérée. Pas de terminal, aucun compte à configurer, rien à copier.</p>\n </a>\n <a href=''#npm-docker'' class=''block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10''>\n <div class=''flex items-center justify-between mb-4''>\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white''>2</span>\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-amber-500''></span><span class=''h-1.5 w-5 rounded-full bg-amber-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\n </div>\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Facile &middot; 100 % local</p>\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h2>\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un seul prompt génère un projet et démarre toute la pile — base de données, auth, stockage, CMS — sur votre machine. Aucun compte cloud.</p>\n </a>\n <a href=''#npm-cloud'' class=''block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10''>\n <div class=''flex items-center justify-between mb-4''>\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white''>3</span>\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\n </div>\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Intermédiaire &middot; votre propre cloud</p>\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h2>\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Générez une app autonome sur votre propre projet Supabase géré et Cloudflare R2, prête à déployer partout.</p>\n </a>\n <a href=''#git-clone'' class=''block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <div class=''flex items-center justify-between mb-4''>\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white''>4</span>\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span></span>\n </div>\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Avancé &middot; pour les contributeurs</p>\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h2>\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Pour celles et ceux qui veulent aider à construire NextBlock (Docker fonctionne ici aussi).</p>\n </a>\n</div>\n\n<p class=''text-sm text-slate-500 dark:text-slate-400''>Vous ne savez pas par où commencer ? Descendez la liste — choisissez la première option dont vous avez déjà les prérequis. La plupart des gens devraient commencer par <a href=''#one-click-vercel''>Vercel</a>.</p>\n\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\n <img src=''/images/included.webp'' alt=''Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation'' class=''w-full h-auto object-cover'' />\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\n</figure>\n\n<h2 id=''one-click-vercel''>Option 1 : Déploiement Vercel en un clic</h2>\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200''>Étape 1 &middot; Le plus simple</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour un site en ligne avec le minimum d''effort — pas de terminal, aucun compte à configurer.</span></p>\n<p>Le moyen le plus rapide d''obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\n<ol class=''space-y-2''>\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\n <li><strong>Nommez le dépôt — et rendez-le Public.</strong> À la première étape sur Vercel, vous choisissez le nom du dépôt ; réglez sa visibilité sur <strong>Public</strong> plutôt que Privé. Un dépôt public est ce qui débloque les mises à jour automatiques en un clic plus tard — c''est donc le choix recommandé.</li>\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l''assistant de configuration.</li>\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\n</ol>\n<div class=''my-8''>\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel &rarr;</a>\n</div>\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Recommandé &middot; rendez le dépôt public</p>\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Quand Vercel vous demande de nommer le nouveau dépôt, choisissez <strong>Public</strong>. C''est gratuit, et c''est ce qui permet à l''étape <strong>Connect GitHub</strong> du tableau de bord d''installer un workflow quotidien qui garde votre site synchronisé avec la dernière version de NextBlock. Vous pourrez toujours passer un dépôt privé en public plus tard dans GitHub — mais démarrer en public est le chemin le plus simple vers les mises à jour automatiques.</p>\n</div>\n<div class=''rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10''>\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Zéro configuration</p>\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Aucune variable d''environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s''exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\n</div>\n\n<h2 id=''npm-docker''>Option 2 : npm create nextblock &rarr; Docker (100 % local)</h2>\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200''>Étape 2 &middot; Facile</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour tout essayer sur votre machine sans aucun compte cloud. Nécessite Docker Desktop.</span></p>\n<p>Faites tourner NextBlock entièrement sur votre machine, sans aucun compte cloud. Le CLI génère une application Next.js autonome puis démarre toute la pile dans Docker pour vous : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a> (npm inclus) et <a href=''https://www.docker.com/products/docker-desktop/'' target=''_blank'' rel=''noopener''>Docker Desktop</a>, et assurez-vous que Docker Desktop est démarré.</p>\n<pre><code>npm create nextblock@latest mon-site</code></pre>\n<ol class=''space-y-2''>\n <li><strong>Choisissez le profil d''hébergement.</strong> Au premier prompt, choisissez <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, puis confirmez que Docker Desktop est installé et démarré.</li>\n <li><strong>Laissez faire.</strong> Le CLI copie le template, installe les dépendances, génère des clés sécurisées et démarre toute la pile avec une seule commande Docker — sans aucune question. Le premier lancement télécharge les images et construit l''app, comptez donc quelques minutes ; chaque migration de base de données est appliquée automatiquement au démarrage de la pile.</li>\n <li><strong>Ouvrez <code>http://localhost:3000</code></strong> — vous êtes redirigé vers l''assistant de configuration. Comme la base de données et le stockage MinIO sont déjà connectés, les étapes de connexion et de stockage sont ignorées : il ne reste qu''à créer votre administrateur (confirmé instantanément, sans email). Vous arrivez dans le tableau de bord du CMS.</li>\n</ol>\n<div class=''rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10''>\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Commandes du quotidien</p>\n <pre class=''mt-4 mb-0''><code># reconstruire et redémarrer la pile\nnpm run docker:up\n\n# arrêter la pile (vos données persistent dans les volumes Docker)\nnpm run docker:down\n\n# suivre les logs de l''application\nnpm run docker:logs</code></pre>\n <p class=''mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200''>Le mode Docker n''est proposé que dans le prompt interactif — ne passez pas <code>--yes</code>, qui force le mode cloud géré ci-dessous.</p>\n</div>\n\n<h2 id=''npm-cloud''>Option 3 : npm create nextblock &rarr; votre propre Supabase + Cloudflare</h2>\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200''>Étape 3 &middot; Intermédiaire</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour construire votre propre site sur du cloud géré. Nécessite un projet Supabase et un bucket R2.</span></p>\n<p>Le meilleur point de départ pour construire votre propre site sur du cloud géré. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — reliée à un projet Supabase et un bucket Cloudflare R2 que vous contrôlez, et elle se déploie partout où Next.js tourne.</p>\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a>, créez un projet gratuit sur <a href=''https://supabase.com'' target=''_blank'' rel=''noopener''>supabase.com</a>, et configurez un bucket <a href=''https://developers.cloudflare.com/r2/'' target=''_blank'' rel=''noopener''>Cloudflare R2</a> pour vos images et fichiers.</p>\n<pre><code>npm create nextblock@latest mon-site\ncd mon-site\nnpm run dev</code></pre>\n<p>Au premier prompt, choisissez <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> et nommez votre projet. Ouvrez ensuite <code>http://localhost:3000/setup</code> et laissez l''assistant faire le travail :</p>\n<ol class=''space-y-2''>\n <li><strong>Connectez Supabase</strong> — collez l''URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d''accès personnel pour que l''assistant applique le schéma de base de données à votre place.</li>\n <li><strong>Ajoutez Cloudflare R2</strong> — saisissez votre identifiant de compte R2, le nom du bucket, la clé d''accès (access key ID), la clé secrète et l''URL publique du bucket pour servir vos images et fichiers.</li>\n <li><strong>Créez votre administrateur</strong> — l''assistant applique toutes les migrations, génère les secrets de l''application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l''application.</li>\n</ol>\n<div class=''rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10''>\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Modules premium</p>\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Besoin d''une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l''êtes : <code>npx create-nextblock activate ecommerce</code></p>\n</div>\n\n<h2 id=''git-clone''>Option 4 : Cloner le dépôt</h2>\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200''>Étape 4 &middot; Avancé</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour les contributeurs et les équipes qui veulent personnaliser la plateforme. Nécessite Node.js et git.</span></p>\n<p>Faites tourner le monorepo Nx complet : l''application CMS, tous les packages partagés, le code du CLI et la documentation. C''est le chemin de celles et ceux qui veulent participer au projet — contributeurs, auteurs de plugins et équipes qui personnalisent la plateforme elle-même.</p>\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\ncd nextblock\nnpm install\nnpx nx serve nextblock</code></pre>\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l''API de management Supabase — sans CLI Supabase.</p>\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Vous préférez rester local ? Docker fonctionne ici aussi</p>\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Pas besoin de comptes cloud pour développer sur le monorepo. Avec Docker Desktop démarré, une seule commande lance la même pile auto-hébergée que l''option 2 — Postgres, auth, stockage et le CMS — directement depuis votre clone :</p>\n <pre class=''mt-4 mb-0''><code>npm run docker:setup</code></pre>\n <p class=''mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200''>Quand c''est terminé, ouvrez <code>http://localhost:3000</code> et créez votre administrateur. Notez que le monorepo n''a pas de <code>npm run dev</code> — utilisez <code>npx nx serve nextblock</code> (port 4200) pour le chemin cloud.</p>\n</div>\n\n<h2 id=''after-install''>Après l''installation : vos 10 premières minutes</h2>\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\n<ul class=''space-y-2''>\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\n <li><strong>Configurez l''email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\n <li><strong>Extras optionnels</strong> — connectez vos outils d''analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\n</ul>\n<p>Ensuite, découvrez comment la plateforme s''articule dans <a href=''/article/comment-nextblock-fonctionne''>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href=''/article/guide-commerce-nextblock''>guide Commerce</a>.</p>\n\n<h2 id=''faq''>FAQ d''installation</h2>\n<h3>Que dois-je installer ?</h3>\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> en mode Docker : <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20+</a> et Docker Desktop. Pour le mode cloud géré : Node.js 20+ ainsi qu''un projet Supabase et un bucket Cloudflare R2. Pour le dépôt cloné : Node.js 20+ et git (ajoutez Docker Desktop si vous voulez faire tourner la pile locale).</p>\n<h3>NextBlock est-il gratuit ?</h3>\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l''e-commerce et Cortex AI sont optionnels et s''activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\n<h3>Ai-je besoin d''un compte Supabase ?</h3>\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour le chemin cloud géré <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker — via le mode Docker du CLI ou <code>npm run docker:setup</code> dans le clone — aucun compte cloud n''est nécessaire.</p>\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\n<p>Non. L''assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l''opération est toujours sans risque.</p>\n<h3>Puis-je changer de chemin plus tard ?</h3>\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd''hui et déployer sur Vercel demain. NextBlock se déploie comme n''importe quelle app Next.js.</p>\n<h3>Comment mettre à jour NextBlock ?</h3>\n<p>Sur Vercel, l''étape Connect GitHub de la checklist active une synchronisation quotidienne automatique (cela nécessite un dépôt public). Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s''appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\n\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5''>\n <p class=''mt-0 text-2xl font-semibold text-slate-900 dark:text-white''>Prêt à vous lancer ?</p>\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\n <div class=''mt-5 flex flex-wrap justify-center gap-3''>\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel</a>\n <a href=''https://github.com/nextblock-cms/nextblock'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50''>Voir sur GitHub</a>\n </div>\n</div>'::text)),\n updated_at = now()\n WHERE post_id = 4\n AND content->>'html_content' LIKE '%NextBlock est un CMS open source et%';\n\n -----------------------------------------------------------------------------\n -- 3. Page 7 (Shop EN) & Page 8 (Boutique FR) Expanded Body Content (>= 300 words)\n -----------------------------------------------------------------------------\n UPDATE public.blocks\n SET content = jsonb_set(content, '{column_blocks,0,1,content,html_content}', to_jsonb('<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Discover our selection of official commercial add-ons and developer tools for NextBlock CMS.</p><div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''><h2 class=''text-2xl font-bold text-white mb-4''>Power Up Your Web Projects</h2><p>Welcome to the official NextBlock™ digital store. Here you can purchase licenses for our premium packages, including NextBlock™ Commerce and NextBlock™ Cortex AI. Every commercial license helps fund full-time open-source development on our core CMS while giving your team advanced tools to launch high-performance websites faster.</p><p>Our premium modules are designed to feel native from day one. You get clean code that fits right into your existing NextBlock project without third-party plugins or complex configuration. When you purchase a license from our store, you receive instant access to everything you need:</p><ul class=''space-y-2 list-disc pl-5''><li><strong>Full Source Code:</strong> Inspect, customize, and adapt the modules to your exact requirements.</li><li><strong>Perpetual Use:</strong> Use the software for your project with full peace of mind.</li><li><strong>Automated Updates:</strong> Enjoy smooth updates that match each new release of NextBlock and Next.js.</li><li><strong>Secure Checkout:</strong> Payments are processed safely with Stripe and Freemius with instant receipt generation.</li></ul><p>Whether you need multi-currency store features, automated sales tax calculations, or AI block generation in your editor, our modules deliver tested solutions that keep your Lighthouse scores at 100%.</p><p>All purchases include dedicated onboarding resources, detailed developer docs, and friendly technical support. If you ever run into an issue or need help wiring up a provider webhook, our core engineers are ready to assist you. We also offer a thirty-day refund policy so you can try our tools risk-free.</p><p>Have questions about license tiers, volume pricing, team seats, or custom agency usage? Contact our support team anytime. Our team is here to answer your questions and help your developers succeed. We are happy to help you pick the best plan for your company. Browse our featured products below to get started today.</p></div>'::text)),\n updated_at = now()\n WHERE page_id = 7\n AND content#>>'{column_blocks,0,1,content,html_content}' LIKE '%Discover our premium selection%';\n\n UPDATE public.blocks\n SET content = jsonb_set(content, '{column_blocks,0,1,content,html_content}', to_jsonb('<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Découvrez nos extensions officielles et nos outils pour développeurs conçus pour le CMS NextBlock.</p><div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''><h2 class=''text-2xl font-bold text-white mb-4''>Accélérez vos projets web</h2><p>Bienvenue sur la boutique officielle de NextBlock™. Vous pouvez acheter ici des licences pour nos modules professionnels, comme NextBlock™ Commerce et NextBlock™ Cortex AI. Chaque achat aide à financer le travail open-source sur le cœur du CMS. Il donne aussi à votre équipe des outils de pointe pour créer des sites rapides et fiables.</p><p>Nos modules premium s''intègrent sans effort à votre projet existant. Vous profitez d''un code propre et bien testé, sans plugin externe lourd ni réglage complexe. En choisissant nos outils, vous profitez immédiatement de nombreux avantages :</p><ul class=''space-y-2 list-disc pl-5''><li><strong>Code source complet :</strong> Lisez, adaptez et faites évoluer chaque bloc selon vos besoins métier.</li><li><strong>Licence perpétuelle :</strong> Utilisez le code sur votre projet en toute sérénité.</li><li><strong>Mises à jour suivies :</strong> Recevez les nouvelles versions au rythme de Next.js et de NextBlock.</li><li><strong>Paiement sécurisé :</strong> Les achats passent par Stripe et Freemius avec facture instantanée.</li></ul><p>Vous voulez vendre dans plusieurs devises ? Vous avez besoin du calcul automatique des taxes ? Vous voulez générer des blocs avec l''IA ? Nos modules offrent des solutions prêtes à l''emploi qui préservent vos scores Lighthouse à 100%.</p><p>Chaque commande donne accès à une documentation claire et à notre support technique. Si vous avez besoin d''aide pour brancher un webhook ou un mode de paiement, nos développeurs vous répondent rapidement. Notre équipe est à vos côtés pour vous faire gagner du temps. Nous proposons aussi une garantie satisfait ou remboursé de trente jours.</p><p>Vous avez des questions sur nos tarifs, les licences agence ou les remises en volume ? Écrivez à notre équipe à tout moment. Nous vous guiderons avec grand plaisir vers l''offre idéale pour votre entreprise. Découvrez dès aujourd''hui l''ensemble de nos produits ci-dessous pour bien démarrer votre projet.</p></div>'::text)),\n updated_at = now()\n WHERE page_id = 8\n AND content#>>'{column_blocks,0,1,content,html_content}' LIKE '%Decouvrez notre selection premium%';\n\n -----------------------------------------------------------------------------\n -- 4. Page 9 (Privacy EN), Page 10 (Politique FR), Page 12 (Conditions FR) Readability\n -----------------------------------------------------------------------------\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Privacy Policy</h1>\r\n<p><em>Last updated: June 4, 2026</em></p>\r\n<p>NextBlock™ CMS (\"we\", \"us\", or \"our\") respects your privacy. We protect your personal information under Quebec''s Law 25, the federal PIPEDA act, and Canada''s Anti-Spam Legislation (CASL).</p>\r\n\r\n<h2>1. Person responsible for personal information</h2>\r\n<p>Our Privacy Officer oversees our compliance with privacy laws. You can reach them at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>2. What we collect</h2>\r\n<ul>\r\n <li><strong>Account information:</strong> Your name, email address, and login details when you register.</li>\r\n <li><strong>Usage and device data:</strong> Collected only with your consent through analytics tools.</li>\r\n <li><strong>Communications:</strong> Messages you send us and your newsletter choices.</li>\r\n</ul>\r\n\r\n<h2>3. Why we collect it and your consent</h2>\r\n<p>We collect personal information for clear reasons: to provide our services, keep accounts secure, and reply to your messages. We use analytics and marketing tools only with your direct, opt-in consent. Under Law 25, optional cookies stay off until you choose to accept them. You can withdraw your consent at any time.</p>\r\n\r\n<h2>4. Cookies and tracking technologies</h2>\r\n<p>Essential cookies keep the site running and require no consent. Analytics and marketing cookies load only after you opt in via our cookie banner. We record your choice to honor it and follow privacy rules.</p>\r\n\r\n<h2>5. Disclosure and sharing</h2>\r\n<p>We do not sell your personal data. We share it only with trusted service providers who help us run the platform under strict confidentiality agreements, or when required by law.</p>\r\n\r\n<h2>6. Retention</h2>\r\n<p>We keep personal information only as long as needed for the purposes described above or as required by law. After that, we securely delete or anonymize your data.</p>\r\n\r\n<h2>7. Your rights</h2>\r\n<p>Under privacy laws, you have the right to view, correct, and delete your personal information. You can also withdraw consent or ask for a portable copy of your data. To use these rights, write to our Privacy Officer at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>8. Commercial electronic messages (CASL)</h2>\r\n<p>We send commercial emails only with your permission. Every email clearly identifies us and includes an easy unsubscribe link that takes effect right away.</p>\r\n\r\n<h2>9. Safeguards</h2>\r\n<p>We use strong technical and physical protections to keep your data safe. These include encrypted connections and strict access controls to prevent loss, theft, and unauthorized access.</p>\r\n\r\n<h2>10. Open-source software</h2>\r\n<p>NextBlock™ CMS is free open-source software under the AGPLv3 license. When you self-host NextBlock, you control your own server. You are responsible for the personal data on your deployment, and this policy is a helpful model you can adapt.</p>\r\n\r\n<h2>11. Changes to this policy</h2>\r\n<p>We may update this policy over time. We announce important changes on our site and update the revision date at the top.</p>\r\n\r\n<h2>12. Contact us</h2>\r\n<p>Have questions or complaints? Contact NextBlock™ at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. You can also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.</p>\r\n'::text)),\n updated_at = now()\n WHERE page_id = 9\n AND content->>'html_content' LIKE '%respects your privacy and is committed%';\n\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Politique de confidentialité</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n<p>NextBlock™ CMS (« nous ») respecte votre vie privée. Nous protégeons vos renseignements personnels selon la Loi 25 du Québec, la loi fédérale LPRPDE et les règles canadiennes anti-pourriel (LCAP).</p>\r\n\r\n<h2>1. Responsable de la protection des renseignements personnels</h2>\r\n<p>Notre responsable veille au respect des règles de confidentialité. Vous pouvez lui écrire à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>2. Renseignements que nous recueillons</h2>\r\n<ul>\r\n <li><strong>Renseignements de compte :</strong> Votre nom, votre courriel et vos accès lors de l''inscription.</li>\r\n <li><strong>Données d''utilisation et d''appareil :</strong> Recueillies avec votre accord via nos outils de mesure.</li>\r\n <li><strong>Communications :</strong> Vos messages reçus et vos choix de suivi par courriel.</li>\r\n</ul>\r\n\r\n<h2>3. Finalités et consentement</h2>\r\n<p>Nous recueillons vos données pour des motifs clairs : faire fonctionner nos services, sécuriser les accès et vous répondre. Les outils d''analyse et de suivi ne s''activent qu''avec votre accord clair. Selon la Loi 25, les témoins optionnels restent coupés tant que vous ne les acceptez pas. Vous pouvez retirer votre accord en tout temps.</p>\r\n\r\n<h2>4. Témoins et technologies de suivi</h2>\r\n<p>Les témoins essentiels assurent le bon fonctionnement du site. Ils ne demandent aucun accord préalable. Les témoins d''analyse se chargent seulement après votre choix sur notre bandeau. Nous gardons votre choix en mémoire pour le respecter.</p>\r\n\r\n<h2>5. Communication à des tiers</h2>\r\n<p>Nous ne vendons jamais vos données personnelles. Nous les partageons uniquement avec des prestataires de confiance qui nous aident à faire tourner le site sous contrat de secret, ou si la loi l''impose.</p>\r\n\r\n<h2>6. Conservation</h2>\r\n<p>Nous gardons vos données seulement le temps utile pour les buts décrits ou selon la loi. Ensuite, nous les effaçons ou nous les rendons anonymes de façon sûre.</p>\r\n\r\n<h2>7. Vos droits</h2>\r\n<p>Vous avez le droit de lire, de corriger et de faire effacer vos données. Vous pouvez aussi retirer votre accord ou demander une copie de vos données. Pour exercer vos droits, écrivez à notre responsable à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\r\n<p>Nous envoyons des courriels informatifs seulement avec votre accord. Chaque courriel montre notre nom et propose un lien simple pour vous désabonner d''un clic.</p>\r\n\r\n<h2>9. Mesures de sécurité</h2>\r\n<p>Nous utilisons des moyens techniques et physiques solides pour garder vos données en sûreté. Cela comprend des échanges chiffrés et un contrôle strict des accès contre toute fuite ou vol.</p>\r\n\r\n<h2>10. Logiciel libre</h2>\r\n<p>NextBlock™ CMS est un logiciel libre sous licence AGPLv3. Si vous hébergez NextBlock vous-même, vous gérez votre propre serveur. Vous êtes responsable des données sur votre instance, et ce texte est un modèle que vous pouvez adapter.</p>\r\n\r\n<h2>11. Modifications</h2>\r\n<p>Nous pouvons mettre à jour ce texte au fil du temps. Les changements notables seront affichés sur le site avec la nouvelle date en tête de page.</p>\r\n\r\n<h2>12. Nous joindre</h2>\r\n<p>Une question ou un avis ? Écrivez à NextBlock™ à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi joindre la Commission d''accès à l''information du Québec.</p>\r\n'::text)),\n updated_at = now()\n WHERE page_id = 10\n AND content->>'html_content' LIKE '%respecte votre vie privée et s''engage%';\n\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Conditions d''utilisation</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n\r\n<h2>1. Acceptation des conditions</h2>\r\n<p>En utilisant le CMS NextBlock™ et les services associés (les « Services »), vous acceptez ces conditions d''utilisation. Si vous refusez ces règles, veuillez ne pas utiliser les Services.</p>\r\n\r\n<h2>2. Logiciel libre et à code source ouvert</h2>\r\n<p>NextBlock™ CMS est un logiciel libre sous <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou toute version ultérieure. Vous pouvez lancer, étudier, copier et faire évoluer le code selon cette licence. Le texte complet se trouve avec le code et en ligne sur <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>\r\n<p>Droit d''auteur © 2025 NextBlock™ CMS.</p>\r\n\r\n<h2>3. Disponibilité du code source</h2>\r\n<p>Selon l''article 13 de la licence AGPL-3.0, si vous modifiez NextBlock™ CMS et le mettez en ligne pour des usagers sur un réseau, vous devez offrir l''accès libre et sans frais au code source de votre version modifiée.</p>\r\n\r\n<h2>4. Marques de commerce</h2>\r\n<p>La licence AGPL-3.0 donne de larges droits sur le code. Mais <strong>elle ne donne aucun droit</strong> sur nos noms et marques. Les termes « NextBlock™ », NextBlock™ CMS et les logos restent notre bien exclusif. Ils ne peuvent être repris sans accord écrit préalable.</p>\r\n\r\n<h2>5. Comptes et utilisation acceptable</h2>\r\n<p>Si vous ouvrez un compte, vous gardez la garde de vos accès et de toute action faite sous votre nom. Vous devez nous prévenir vite en cas d''usage non permis. Vous vous engagez à ne pas bloquer les Services, ne pas forcer les accès et ne pas agir contre la loi.</p>\r\n\r\n<h2>6. Absence de garantie</h2>\r\n<p>Selon l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou tacite. Cela inclut les garanties de vente ou d''usage pour un besoin précis. Vous prenez sur vous les risques liés au bon emploi du logiciel.</p>\r\n\r\n<h2>7. Limitation de responsabilité</h2>\r\n<p>Selon l''article 16 de l''AGPL-3.0 et dans la limite permise par la loi, aucun auteur ou tiers modifiant le code ne peut être tenu pour responsable de vos pertes ou dommages liés à l''usage du logiciel.</p>\r\n\r\n<h2>8. Droit applicable</h2>\r\n<p>Ces conditions suivent les lois de la province de Québec et les lois du Canada applicables. Rien ici ne réduit vos droits stricts de consommateur selon ces lois.</p>\r\n\r\n<h2>9. Modifications</h2>\r\n<p>Nous pouvons adapter ces règles au fil du temps. Les changements notables passeront sur le site. Votre usage continu vaut accord avec les nouvelles règles.</p>\r\n\r\n<h2>10. Nous joindre</h2>\r\n<p>Une question sur ces conditions ? Écrivez à NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n'::text)),\n updated_at = now()\n WHERE page_id = 12\n AND content->>'html_content' LIKE '%En accédant à NextBlock™ CMS et aux services%';\n\nEND $$;\n"
185
+ "sql": "-- Migration: 00000000000033_seed_seo_score_optimizations_p2.sql\r\n-- Description: Complete 100/100 Page SEO optimization for shop/boutique, legal pages, and setup/updating posts.\r\n-- Safety: Forward-only, content-guarded updates that only touch matching seeded copy.\r\n\r\nDO $$\r\nBEGIN\r\n -----------------------------------------------------------------------------\r\n -- 1. Post Metadata (Post 8 & Post 9)\r\n -----------------------------------------------------------------------------\r\n UPDATE public.posts\r\n SET meta_description = 'A single command keeps your NextBlock install updated on Docker, Supabase, or a cloned repo. Learn how it works and keeps your data safe.',\r\n updated_at = now()\r\n WHERE slug = 'how-updating-works'\r\n AND meta_description LIKE 'Update NextBlock in one step%';\r\n\r\n UPDATE public.posts\r\n SET meta_title = 'Mises à jour NextBlock : une commande pour chaque site',\r\n meta_description = 'Une seule commande met à jour NextBlock sur Docker, Supabase ou un dépôt cloné. Découvrez le fonctionnement et la protection de vos données.',\r\n updated_at = now()\r\n WHERE slug = 'comment-fonctionnent-les-mises-a-jour'\r\n AND meta_title LIKE 'Mettre à jour NextBlock%';\r\n\r\n -----------------------------------------------------------------------------\r\n -- 2. Post 4 Setup FR Card Headings (H3 -> H2 to prevent heading level skip)\r\n -----------------------------------------------------------------------------\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Voici quatre façons de démarrer, <strong>classées de la plus simple à la plus technique</strong> : la première tient en un clic, la dernière donne le code source complet pour celles et ceux qui veulent participer à NextBlock. Elles aboutissent toutes au même endroit — un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur.</p>\r\n\r\n<div class=''mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400''>\r\n <span class=''flex-shrink-0''>Le plus simple</span>\r\n <span class=''h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400''></span>\r\n <span class=''flex-shrink-0''>Le plus de contrôle</span>\r\n</div>\r\n\r\n<div class=''grid gap-5 md:grid-cols-2 my-6''>\r\n <a href=''#one-click-vercel'' class=''block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white''>1</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-blue-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Le plus simple &middot; un clic</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déployer sur Vercel</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un site de production en ligne avec une base de données gérée. Pas de terminal, aucun compte à configurer, rien à copier.</p>\r\n </a>\r\n <a href=''#npm-docker'' class=''block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white''>2</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-amber-500''></span><span class=''h-1.5 w-5 rounded-full bg-amber-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Facile &middot; 100 % local</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un seul prompt génère un projet et démarre toute la pile — base de données, auth, stockage, CMS — sur votre machine. Aucun compte cloud.</p>\r\n </a>\r\n <a href=''#npm-cloud'' class=''block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white''>3</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Intermédiaire &middot; votre propre cloud</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Générez une app autonome sur votre propre projet Supabase géré et Cloudflare R2, prête à déployer partout.</p>\r\n </a>\r\n <a href=''#git-clone'' class=''block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white''>4</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Avancé &middot; pour les contributeurs</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Pour celles et ceux qui veulent aider à construire NextBlock (Docker fonctionne ici aussi).</p>\r\n </a>\r\n</div>\r\n\r\n<p class=''text-sm text-slate-500 dark:text-slate-400''>Vous ne savez pas par où commencer ? Descendez la liste — choisissez la première option dont vous avez déjà les prérequis. La plupart des gens devraient commencer par <a href=''#one-click-vercel''>Vercel</a>.</p>\r\n\r\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\r\n <img src=''/images/included.webp'' alt=''Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation'' class=''w-full h-auto object-cover'' />\r\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\r\n</figure>\r\n\r\n<h2 id=''one-click-vercel''>Option 1 : Déploiement Vercel en un clic</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200''>Étape 1 &middot; Le plus simple</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour un site en ligne avec le minimum d''effort — pas de terminal, aucun compte à configurer.</span></p>\r\n<p>Le moyen le plus rapide d''obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\r\n<ol class=''space-y-2''>\r\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\r\n <li><strong>Nommez le dépôt — et rendez-le Public.</strong> À la première étape sur Vercel, vous choisissez le nom du dépôt ; réglez sa visibilité sur <strong>Public</strong> plutôt que Privé. Un dépôt public est ce qui débloque les mises à jour automatiques en un clic plus tard — c''est donc le choix recommandé.</li>\r\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\r\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l''assistant de configuration.</li>\r\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\r\n</ol>\r\n<div class=''my-8''>\r\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel &rarr;</a>\r\n</div>\r\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Recommandé &middot; rendez le dépôt public</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Quand Vercel vous demande de nommer le nouveau dépôt, choisissez <strong>Public</strong>. C''est gratuit, et c''est ce qui permet à l''étape <strong>Connect GitHub</strong> du tableau de bord d''installer un workflow quotidien qui garde votre site synchronisé avec la dernière version de NextBlock. Vous pourrez toujours passer un dépôt privé en public plus tard dans GitHub — mais démarrer en public est le chemin le plus simple vers les mises à jour automatiques.</p>\r\n</div>\r\n<div class=''rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Zéro configuration</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Aucune variable d''environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s''exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\r\n</div>\r\n\r\n<h2 id=''npm-docker''>Option 2 : npm create nextblock &rarr; Docker (100 % local)</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200''>Étape 2 &middot; Facile</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour tout essayer sur votre machine sans aucun compte cloud. Nécessite Docker Desktop.</span></p>\r\n<p>Faites tourner NextBlock entièrement sur votre machine, sans aucun compte cloud. Le CLI génère une application Next.js autonome puis démarre toute la pile dans Docker pour vous : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\r\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a> (npm inclus) et <a href=''https://www.docker.com/products/docker-desktop/'' target=''_blank'' rel=''noopener''>Docker Desktop</a>, et assurez-vous que Docker Desktop est démarré.</p>\r\n<pre><code>npm create nextblock@latest mon-site</code></pre>\r\n<ol class=''space-y-2''>\r\n <li><strong>Choisissez le profil d''hébergement.</strong> Au premier prompt, choisissez <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, puis confirmez que Docker Desktop est installé et démarré.</li>\r\n <li><strong>Laissez faire.</strong> Le CLI copie le template, installe les dépendances, génère des clés sécurisées et démarre toute la pile avec une seule commande Docker — sans aucune question. Le premier lancement télécharge les images et construit l''app, comptez donc quelques minutes ; chaque migration de base de données est appliquée automatiquement au démarrage de la pile.</li>\r\n <li><strong>Ouvrez <code>http://localhost:3000</code></strong> — vous êtes redirigé vers l''assistant de configuration. Comme la base de données et le stockage MinIO sont déjà connectés, les étapes de connexion et de stockage sont ignorées : il ne reste qu''à créer votre administrateur (confirmé instantanément, sans email). Vous arrivez dans le tableau de bord du CMS.</li>\r\n</ol>\r\n<div class=''rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Commandes du quotidien</p>\r\n <pre class=''mt-4 mb-0''><code># reconstruire et redémarrer la pile\r\nnpm run docker:up\r\n\r\n# arrêter la pile (vos données persistent dans les volumes Docker)\r\nnpm run docker:down\r\n\r\n# suivre les logs de l''application\r\nnpm run docker:logs</code></pre>\r\n <p class=''mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200''>Le mode Docker n''est proposé que dans le prompt interactif — ne passez pas <code>--yes</code>, qui force le mode cloud géré ci-dessous.</p>\r\n</div>\r\n\r\n<h2 id=''npm-cloud''>Option 3 : npm create nextblock &rarr; votre propre Supabase + Cloudflare</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200''>Étape 3 &middot; Intermédiaire</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour construire votre propre site sur du cloud géré. Nécessite un projet Supabase et un bucket R2.</span></p>\r\n<p>Le meilleur point de départ pour construire votre propre site sur du cloud géré. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — reliée à un projet Supabase et un bucket Cloudflare R2 que vous contrôlez, et elle se déploie partout où Next.js tourne.</p>\r\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a>, créez un projet gratuit sur <a href=''https://supabase.com'' target=''_blank'' rel=''noopener''>supabase.com</a>, et configurez un bucket <a href=''https://developers.cloudflare.com/r2/'' target=''_blank'' rel=''noopener''>Cloudflare R2</a> pour vos images et fichiers.</p>\r\n<pre><code>npm create nextblock@latest mon-site\r\ncd mon-site\r\nnpm run dev</code></pre>\r\n<p>Au premier prompt, choisissez <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> et nommez votre projet. Ouvrez ensuite <code>http://localhost:3000/setup</code> et laissez l''assistant faire le travail :</p>\r\n<ol class=''space-y-2''>\r\n <li><strong>Connectez Supabase</strong> — collez l''URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d''accès personnel pour que l''assistant applique le schéma de base de données à votre place.</li>\r\n <li><strong>Ajoutez Cloudflare R2</strong> — saisissez votre identifiant de compte R2, le nom du bucket, la clé d''accès (access key ID), la clé secrète et l''URL publique du bucket pour servir vos images et fichiers.</li>\r\n <li><strong>Créez votre administrateur</strong> — l''assistant applique toutes les migrations, génère les secrets de l''application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l''application.</li>\r\n</ol>\r\n<div class=''rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Modules premium</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Besoin d''une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l''êtes : <code>npx create-nextblock activate ecommerce</code></p>\r\n</div>\r\n\r\n<h2 id=''git-clone''>Option 4 : Cloner le dépôt</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200''>Étape 4 &middot; Avancé</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour les contributeurs et les équipes qui veulent personnaliser la plateforme. Nécessite Node.js et git.</span></p>\r\n<p>Faites tourner le monorepo Nx complet : l''application CMS, tous les packages partagés, le code du CLI et la documentation. C''est le chemin de celles et ceux qui veulent participer au projet — contributeurs, auteurs de plugins et équipes qui personnalisent la plateforme elle-même.</p>\r\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\r\ncd nextblock\r\nnpm install\r\nnpx nx serve nextblock</code></pre>\r\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l''API de management Supabase — sans CLI Supabase.</p>\r\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Vous préférez rester local ? Docker fonctionne ici aussi</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Pas besoin de comptes cloud pour développer sur le monorepo. Avec Docker Desktop démarré, une seule commande lance la même pile auto-hébergée que l''option 2 — Postgres, auth, stockage et le CMS — directement depuis votre clone :</p>\r\n <pre class=''mt-4 mb-0''><code>npm run docker:setup</code></pre>\r\n <p class=''mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200''>Quand c''est terminé, ouvrez <code>http://localhost:3000</code> et créez votre administrateur. Notez que le monorepo n''a pas de <code>npm run dev</code> — utilisez <code>npx nx serve nextblock</code> (port 4200) pour le chemin cloud.</p>\r\n</div>\r\n\r\n<h2 id=''after-install''>Après l''installation : vos 10 premières minutes</h2>\r\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\r\n<ul class=''space-y-2''>\r\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\r\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\r\n <li><strong>Configurez l''email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\r\n <li><strong>Extras optionnels</strong> — connectez vos outils d''analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\r\n</ul>\r\n<p>Ensuite, découvrez comment la plateforme s''articule dans <a href=''/article/comment-nextblock-fonctionne''>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href=''/article/guide-commerce-nextblock''>guide Commerce</a>.</p>\r\n\r\n<h2 id=''faq''>FAQ d''installation</h2>\r\n<h3>Que dois-je installer ?</h3>\r\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> en mode Docker : <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20+</a> et Docker Desktop. Pour le mode cloud géré : Node.js 20+ ainsi qu''un projet Supabase et un bucket Cloudflare R2. Pour le dépôt cloné : Node.js 20+ et git (ajoutez Docker Desktop si vous voulez faire tourner la pile locale).</p>\r\n<h3>NextBlock est-il gratuit ?</h3>\r\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l''e-commerce et Cortex AI sont optionnels et s''activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\r\n<h3>Ai-je besoin d''un compte Supabase ?</h3>\r\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour le chemin cloud géré <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker — via le mode Docker du CLI ou <code>npm run docker:setup</code> dans le clone — aucun compte cloud n''est nécessaire.</p>\r\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\r\n<p>Non. L''assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l''opération est toujours sans risque.</p>\r\n<h3>Puis-je changer de chemin plus tard ?</h3>\r\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd''hui et déployer sur Vercel demain. NextBlock se déploie comme n''importe quelle app Next.js.</p>\r\n<h3>Comment mettre à jour NextBlock ?</h3>\r\n<p>Sur Vercel, l''étape Connect GitHub de la checklist active une synchronisation quotidienne automatique (cela nécessite un dépôt public). Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s''appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\r\n\r\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5''>\r\n <p class=''mt-0 text-2xl font-semibold text-slate-900 dark:text-white''>Prêt à vous lancer ?</p>\r\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\r\n <div class=''mt-5 flex flex-wrap justify-center gap-3''>\r\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel</a>\r\n <a href=''https://github.com/nextblock-cms/nextblock'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50''>Voir sur GitHub</a>\r\n </div>\r\n</div>'::text)),\r\n updated_at = now()\r\n WHERE post_id = 4\r\n AND content->>'html_content' LIKE '%NextBlock est un CMS open source et%';\r\n\r\n -----------------------------------------------------------------------------\r\n -- 3. Page 7 (Shop EN) & Page 8 (Boutique FR) Expanded Body Content (>= 300 words)\r\n -----------------------------------------------------------------------------\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{column_blocks,0,1,content,html_content}', to_jsonb('<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Discover our selection of official commercial add-ons and developer tools for NextBlock CMS.</p><div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''><h2 class=''text-2xl font-bold text-white mb-4''>Power Up Your Web Projects</h2><p>Welcome to the official NextBlock™ digital store. Here you can purchase licenses for our premium packages, including NextBlock™ Commerce and NextBlock™ Cortex AI. Every commercial license helps fund full-time open-source development on our core CMS while giving your team advanced tools to launch high-performance websites faster.</p><p>Our premium modules are designed to feel native from day one. You get clean code that fits right into your existing NextBlock project without third-party plugins or complex configuration. When you purchase a license from our store, you receive instant access to everything you need:</p><ul class=''space-y-2 list-disc pl-5''><li><strong>Full Source Code:</strong> Inspect, customize, and adapt the modules to your exact requirements.</li><li><strong>Perpetual Use:</strong> Use the software for your project with full peace of mind.</li><li><strong>Automated Updates:</strong> Enjoy smooth updates that match each new release of NextBlock and Next.js.</li><li><strong>Secure Checkout:</strong> Payments are processed safely with Stripe and Freemius with instant receipt generation.</li></ul><p>Whether you need multi-currency store features, automated sales tax calculations, or AI block generation in your editor, our modules deliver tested solutions that keep your Lighthouse scores at 100%.</p><p>All purchases include dedicated onboarding resources, detailed developer docs, and friendly technical support. If you ever run into an issue or need help wiring up a provider webhook, our core engineers are ready to assist you. We also offer a thirty-day refund policy so you can try our tools risk-free.</p><p>Have questions about license tiers, volume pricing, team seats, or custom agency usage? Contact our support team anytime. Our team is here to answer your questions and help your developers succeed. We are happy to help you pick the best plan for your company. Browse our featured products below to get started today.</p></div>'::text)),\r\n updated_at = now()\r\n WHERE page_id = 7\r\n AND content#>>'{column_blocks,0,1,content,html_content}' LIKE '%Discover our premium selection%';\r\n\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{column_blocks,0,1,content,html_content}', to_jsonb('<p style=\"text-align: center; color: var(--background); opacity: 0.9\">Découvrez nos extensions officielles et nos outils pour développeurs conçus pour le CMS NextBlock.</p><div class=''mt-8 text-slate-300 space-y-4 max-w-3xl mx-auto text-left''><h2 class=''text-2xl font-bold text-white mb-4''>Accélérez vos projets web</h2><p>Bienvenue sur la boutique officielle de NextBlock™. Vous pouvez acheter ici des licences pour nos modules professionnels, comme NextBlock™ Commerce et NextBlock™ Cortex AI. Chaque achat aide à financer le travail open-source sur le cœur du CMS. Il donne aussi à votre équipe des outils de pointe pour créer des sites rapides et fiables.</p><p>Nos modules premium s''intègrent sans effort à votre projet existant. Vous profitez d''un code propre et bien testé, sans plugin externe lourd ni réglage complexe. En choisissant nos outils, vous profitez immédiatement de nombreux avantages :</p><ul class=''space-y-2 list-disc pl-5''><li><strong>Code source complet :</strong> Lisez, adaptez et faites évoluer chaque bloc selon vos besoins métier.</li><li><strong>Licence perpétuelle :</strong> Utilisez le code sur votre projet en toute sérénité.</li><li><strong>Mises à jour suivies :</strong> Recevez les nouvelles versions au rythme de Next.js et de NextBlock.</li><li><strong>Paiement sécurisé :</strong> Les achats passent par Stripe et Freemius avec facture instantanée.</li></ul><p>Vous voulez vendre dans plusieurs devises ? Vous avez besoin du calcul automatique des taxes ? Vous voulez générer des blocs avec l''IA ? Nos modules offrent des solutions prêtes à l''emploi qui préservent vos scores Lighthouse à 100%.</p><p>Chaque commande donne accès à une documentation claire et à notre support technique. Si vous avez besoin d''aide pour brancher un webhook ou un mode de paiement, nos développeurs vous répondent rapidement. Notre équipe est à vos côtés pour vous faire gagner du temps. Nous proposons aussi une garantie satisfait ou remboursé de trente jours.</p><p>Vous avez des questions sur nos tarifs, les licences agence ou les remises en volume ? Écrivez à notre équipe à tout moment. Nous vous guiderons avec grand plaisir vers l''offre idéale pour votre entreprise. Découvrez dès aujourd''hui l''ensemble de nos produits ci-dessous pour bien démarrer votre projet.</p></div>'::text)),\r\n updated_at = now()\r\n WHERE page_id = 8\r\n AND content#>>'{column_blocks,0,1,content,html_content}' LIKE '%Decouvrez notre selection premium%';\r\n\r\n -----------------------------------------------------------------------------\r\n -- 4. Page 9 (Privacy EN), Page 10 (Politique FR), Page 12 (Conditions FR) Readability\r\n -----------------------------------------------------------------------------\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Privacy Policy</h1>\r\n<p><em>Last updated: June 4, 2026</em></p>\r\n<p>NextBlock™ CMS (\"we\", \"us\", or \"our\") respects your privacy. We protect your personal information under Quebec''s Law 25, the federal PIPEDA act, and Canada''s Anti-Spam Legislation (CASL).</p>\r\n\r\n<h2>1. Person responsible for personal information</h2>\r\n<p>Our Privacy Officer oversees our compliance with privacy laws. You can reach them at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>2. What we collect</h2>\r\n<ul>\r\n <li><strong>Account information:</strong> Your name, email address, and login details when you register.</li>\r\n <li><strong>Usage and device data:</strong> Collected only with your consent through analytics tools.</li>\r\n <li><strong>Communications:</strong> Messages you send us and your newsletter choices.</li>\r\n</ul>\r\n\r\n<h2>3. Why we collect it and your consent</h2>\r\n<p>We collect personal information for clear reasons: to provide our services, keep accounts secure, and reply to your messages. We use analytics and marketing tools only with your direct, opt-in consent. Under Law 25, optional cookies stay off until you choose to accept them. You can withdraw your consent at any time.</p>\r\n\r\n<h2>4. Cookies and tracking technologies</h2>\r\n<p>Essential cookies keep the site running and require no consent. Analytics and marketing cookies load only after you opt in via our cookie banner. We record your choice to honor it and follow privacy rules.</p>\r\n\r\n<h2>5. Disclosure and sharing</h2>\r\n<p>We do not sell your personal data. We share it only with trusted service providers who help us run the platform under strict confidentiality agreements, or when required by law.</p>\r\n\r\n<h2>6. Retention</h2>\r\n<p>We keep personal information only as long as needed for the purposes described above or as required by law. After that, we securely delete or anonymize your data.</p>\r\n\r\n<h2>7. Your rights</h2>\r\n<p>Under privacy laws, you have the right to view, correct, and delete your personal information. You can also withdraw consent or ask for a portable copy of your data. To use these rights, write to our Privacy Officer at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>8. Commercial electronic messages (CASL)</h2>\r\n<p>We send commercial emails only with your permission. Every email clearly identifies us and includes an easy unsubscribe link that takes effect right away.</p>\r\n\r\n<h2>9. Safeguards</h2>\r\n<p>We use strong technical and physical protections to keep your data safe. These include encrypted connections and strict access controls to prevent loss, theft, and unauthorized access.</p>\r\n\r\n<h2>10. Open-source software</h2>\r\n<p>NextBlock™ CMS is free open-source software under the AGPLv3 license. When you self-host NextBlock, you control your own server. You are responsible for the personal data on your deployment, and this policy is a helpful model you can adapt.</p>\r\n\r\n<h2>11. Changes to this policy</h2>\r\n<p>We may update this policy over time. We announce important changes on our site and update the revision date at the top.</p>\r\n\r\n<h2>12. Contact us</h2>\r\n<p>Have questions or complaints? Contact NextBlock™ at <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. You can also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE page_id = 9\r\n AND content->>'html_content' LIKE '%respects your privacy and is committed%';\r\n\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Politique de confidentialité</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n<p>NextBlock™ CMS (« nous ») respecte votre vie privée. Nous protégeons vos renseignements personnels selon la Loi 25 du Québec, la loi fédérale LPRPDE et les règles canadiennes anti-pourriel (LCAP).</p>\r\n\r\n<h2>1. Responsable de la protection des renseignements personnels</h2>\r\n<p>Notre responsable veille au respect des règles de confidentialité. Vous pouvez lui écrire à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>2. Renseignements que nous recueillons</h2>\r\n<ul>\r\n <li><strong>Renseignements de compte :</strong> Votre nom, votre courriel et vos accès lors de l''inscription.</li>\r\n <li><strong>Données d''utilisation et d''appareil :</strong> Recueillies avec votre accord via nos outils de mesure.</li>\r\n <li><strong>Communications :</strong> Vos messages reçus et vos choix de suivi par courriel.</li>\r\n</ul>\r\n\r\n<h2>3. Finalités et consentement</h2>\r\n<p>Nous recueillons vos données pour des motifs clairs : faire fonctionner nos services, sécuriser les accès et vous répondre. Les outils d''analyse et de suivi ne s''activent qu''avec votre accord clair. Selon la Loi 25, les témoins optionnels restent coupés tant que vous ne les acceptez pas. Vous pouvez retirer votre accord en tout temps.</p>\r\n\r\n<h2>4. Témoins et technologies de suivi</h2>\r\n<p>Les témoins essentiels assurent le bon fonctionnement du site. Ils ne demandent aucun accord préalable. Les témoins d''analyse se chargent seulement après votre choix sur notre bandeau. Nous gardons votre choix en mémoire pour le respecter.</p>\r\n\r\n<h2>5. Communication à des tiers</h2>\r\n<p>Nous ne vendons jamais vos données personnelles. Nous les partageons uniquement avec des prestataires de confiance qui nous aident à faire tourner le site sous contrat de secret, ou si la loi l''impose.</p>\r\n\r\n<h2>6. Conservation</h2>\r\n<p>Nous gardons vos données seulement le temps utile pour les buts décrits ou selon la loi. Ensuite, nous les effaçons ou nous les rendons anonymes de façon sûre.</p>\r\n\r\n<h2>7. Vos droits</h2>\r\n<p>Vous avez le droit de lire, de corriger et de faire effacer vos données. Vous pouvez aussi retirer votre accord ou demander une copie de vos données. Pour exercer vos droits, écrivez à notre responsable à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\r\n<p>Nous envoyons des courriels informatifs seulement avec votre accord. Chaque courriel montre notre nom et propose un lien simple pour vous désabonner d''un clic.</p>\r\n\r\n<h2>9. Mesures de sécurité</h2>\r\n<p>Nous utilisons des moyens techniques et physiques solides pour garder vos données en sûreté. Cela comprend des échanges chiffrés et un contrôle strict des accès contre toute fuite ou vol.</p>\r\n\r\n<h2>10. Logiciel libre</h2>\r\n<p>NextBlock™ CMS est un logiciel libre sous licence AGPLv3. Si vous hébergez NextBlock vous-même, vous gérez votre propre serveur. Vous êtes responsable des données sur votre instance, et ce texte est un modèle que vous pouvez adapter.</p>\r\n\r\n<h2>11. Modifications</h2>\r\n<p>Nous pouvons mettre à jour ce texte au fil du temps. Les changements notables seront affichés sur le site avec la nouvelle date en tête de page.</p>\r\n\r\n<h2>12. Nous joindre</h2>\r\n<p>Une question ou un avis ? Écrivez à NextBlock™ à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi joindre la Commission d''accès à l''information du Québec.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE page_id = 10\r\n AND content->>'html_content' LIKE '%respecte votre vie privée et s''engage%';\r\n\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Conditions d''utilisation</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n\r\n<h2>1. Acceptation des conditions</h2>\r\n<p>En utilisant le CMS NextBlock™ et les services associés (les « Services »), vous acceptez ces conditions d''utilisation. Si vous refusez ces règles, veuillez ne pas utiliser les Services.</p>\r\n\r\n<h2>2. Logiciel libre et à code source ouvert</h2>\r\n<p>NextBlock™ CMS est un logiciel libre sous <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou toute version ultérieure. Vous pouvez lancer, étudier, copier et faire évoluer le code selon cette licence. Le texte complet se trouve avec le code et en ligne sur <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>\r\n<p>Droit d''auteur © 2025 NextBlock™ CMS.</p>\r\n\r\n<h2>3. Disponibilité du code source</h2>\r\n<p>Selon l''article 13 de la licence AGPL-3.0, si vous modifiez NextBlock™ CMS et le mettez en ligne pour des usagers sur un réseau, vous devez offrir l''accès libre et sans frais au code source de votre version modifiée.</p>\r\n\r\n<h2>4. Marques de commerce</h2>\r\n<p>La licence AGPL-3.0 donne de larges droits sur le code. Mais <strong>elle ne donne aucun droit</strong> sur nos noms et marques. Les termes « NextBlock™ », NextBlock™ CMS et les logos restent notre bien exclusif. Ils ne peuvent être repris sans accord écrit préalable.</p>\r\n\r\n<h2>5. Comptes et utilisation acceptable</h2>\r\n<p>Si vous ouvrez un compte, vous gardez la garde de vos accès et de toute action faite sous votre nom. Vous devez nous prévenir vite en cas d''usage non permis. Vous vous engagez à ne pas bloquer les Services, ne pas forcer les accès et ne pas agir contre la loi.</p>\r\n\r\n<h2>6. Absence de garantie</h2>\r\n<p>Selon l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou tacite. Cela inclut les garanties de vente ou d''usage pour un besoin précis. Vous prenez sur vous les risques liés au bon emploi du logiciel.</p>\r\n\r\n<h2>7. Limitation de responsabilité</h2>\r\n<p>Selon l''article 16 de l''AGPL-3.0 et dans la limite permise par la loi, aucun auteur ou tiers modifiant le code ne peut être tenu pour responsable de vos pertes ou dommages liés à l''usage du logiciel.</p>\r\n\r\n<h2>8. Droit applicable</h2>\r\n<p>Ces conditions suivent les lois de la province de Québec et les lois du Canada applicables. Rien ici ne réduit vos droits stricts de consommateur selon ces lois.</p>\r\n\r\n<h2>9. Modifications</h2>\r\n<p>Nous pouvons adapter ces règles au fil du temps. Les changements notables passeront sur le site. Votre usage continu vaut accord avec les nouvelles règles.</p>\r\n\r\n<h2>10. Nous joindre</h2>\r\n<p>Une question sur ces conditions ? Écrivez à NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE page_id = 12\r\n AND content->>'html_content' LIKE '%En accédant à NextBlock™ CMS et aux services%';\r\n\r\nEND $$;\r\n"
186
186
  },
187
187
  {
188
188
  "version": "00000000000034",
189
189
  "name": "00000000000034_seed_seo_french_legal_readability.sql",
190
- "sql": "-- Migration: 00000000000034_seed_seo_french_legal_readability.sql\n-- Description: Optimize French legal pages (politique de confidentialité & conditions d'utilisation) readability to achieve 100/100 Page SEO score.\n-- Safety: Forward-only, content-guarded updates that only apply to default seeded content.\n\nDO $$\nBEGIN\n -- Page 10: Politique de confidentialité FR (Block 92)\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Politique de confidentialité</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n<p>NextBlock™ CMS (« nous ») respecte votre vie privée. Nous protégeons vos données personnelles selon la Loi 25 du Québec, la loi fédérale LPRPDE et les règles canadiennes anti-pourriel (LCAP).</p>\r\n\r\n<h2>1. Responsable de la protection des données personnelles</h2>\r\n<p>Notre responsable veille au respect des règles de confidentialité. Vous pouvez lui écrire à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>2. Renseignements que nous recueillons</h2>\r\n<ul>\r\n <li><strong>Renseignements de compte</strong> &mdash; nom, adresse courriel et identifiants lors de l''inscription.</li>\r\n <li><strong>Données d''utilisation et d''appareil</strong> &mdash; recueillies uniquement avec votre consentement au moyen de outils web.</li>\r\n <li><strong>Communications</strong> &mdash; les messages que vous nous envoyez et vos préférences marketing.</li>\r\n</ul>\r\n\r\n<h2>3. Finalités et consentement</h2>\r\n<p>Nous recueillons vos données pour des motifs clairs : faire fonctionner nos services, sécuriser les accès et vous répondre. Les outils d''analyse et de suivi ne s''activent qu''avec votre accord clair. Selon la Loi 25, les témoins optionnels restent coupés tant que vous ne les acceptez pas. Vous pouvez retirer votre accord en tout temps.</p>\r\n\r\n<h2>4. Témoins et technologies de suivi</h2>\r\n<p>Les témoins essentiels assurent le bon usage du site. Ils ne demandent aucun accord préalable. Les témoins d''analyse se chargent seulement après votre choix sur notre bandeau. Nous gardons votre choix en mémoire pour le respecter.</p>\r\n\r\n<h2>5. Partage des données</h2>\r\n<p>Nous ne vendons pas vos données personnelles. Nous ne les communiquons qu''à des fournisseurs qui nous aident à exploiter la plateforme, sous obligation de confidentialité, ou lorsque la loi l''exige.</p>\r\n\r\n<h2>6. Conservation</h2>\r\n<p>Nous ne conservons les données personnelles que le temps nécessaire aux fins décrites ou exigé par la loi, après quoi ils sont détruits ou anonymisés sans risque.</p>\r\n\r\n<h2>7. Vos droits</h2>\r\n<p>Vous avez le droit de lire, de corriger et de faire effacer vos données. Vous pouvez aussi retirer votre accord ou demander une copie de vos données. Pour exercer vos droits, écrivez à notre responsable à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\r\n<p>Nous n''envoyons des messages électroniques commerciaux qu''avec votre consentement. Chaque message nous identifie et comporte un mécanisme de désabonnement fonctionnel que nous respectons rapidement.</p>\r\n\r\n<h2>9. Mesures de sécurité</h2>\r\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées &mdash; dont le chiffrement en transit et le contrôle des accès &mdash; pour protéger vos données.</p>\r\n\r\n<h2>10. Logiciel libre</h2>\r\n<p>NextBlock™ CMS est un logiciel libre sous licence AGPLv3. Si vous hébergez NextBlock vous-même, vous gérez votre propre serveur. Vous êtes responsable des données sur votre instance, et ce texte est un modèle que vous pouvez adapter.</p>\r\n\r\n<h2>11. Modifications</h2>\r\n<p>Nous pouvons mettre à jour cette politique. Les changements importants seront communiqués sur le site et la date de mise à jour sera révisée.</p>\r\n\r\n<h2>12. Nous joindre</h2>\r\n<p>Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi vous adresser à la Commission d''accès à l''information du Québec.</p>\r\n'::text)),\n updated_at = now()\n WHERE id = 92\n AND page_id = 10\n AND content->>'html_content' LIKE '%Politique de confidentialité%';\n\n -- Page 12: Conditions d'utilisation FR (Block 94)\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Règles du service</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n\r\n<h2>1. Acceptation des conditions</h2>\r\n<p>En utilisant le CMS NextBlock™ et les services associés (les « Services »), vous acceptez ces règles du service. Si vous refusez ces règles, veuillez ne pas utiliser les Services.</p>\r\n\r\n<h2>2. Logiciel libre et à code source ouvert</h2>\r\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou, à votre choix, toute version ultérieure. Vous êtes libre d''exécuter, d''étudier, de partager et de modifier le logiciel selon les termes de cette licence. Une copie de la licence est fournie avec le logiciel et est aussi disponible à <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>\r\n<p>Droit d''auteur © 2025 NextBlock™ CMS.</p>\r\n\r\n<h2>3. Disponibilité du code source</h2>\r\n<p>Conformément à l''article 13 de l''AGPL-3.0, si vous exploitez une version modifiée de NextBlock™ CMS et la rendez accessible à des utilisateurs sur un réseau, vous devez offrir clairement à ces utilisateurs l''accès au code source correspondant de votre version modifiée, gratuitement, par un moyen usuel de copie de logiciels.</p>\r\n\r\n<h2>4. Marques de commerce</h2>\r\n<p>L''AGPL-3.0 accorde de larges droits sur le code source du logiciel, mais <strong>n''accorde aucun droit</strong> sur nos noms commerciaux, marques de commerce ou marques de service. « NextBlock™ », le nom NextBlock™ CMS et les logos associés demeurent notre propriété et ne peuvent être utilisés d''une manière laissant entendre une approbation ou une affiliation sans notre autorisation écrite préalable.</p>\r\n\r\n<h2>5. Comptes et utilisation acceptable</h2>\r\n<p>Si vous créez un compte, vous êtes responsable de la protection de vos identifiants et de toute activité effectuée à partir de votre compte, et vous vous engagez à nous aviser rapidement de toute utilisation non autorisée. Vous vous engagez à ne pas détourner les Services, notamment en tentant de les perturber, d''y accéder sans autorisation ou de les utiliser à des fins illégales.</p>\r\n\r\n<h2>6. Absence de garantie</h2>\r\n<p>Selon l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou tacite. Cela inclut les garanties de vente ou d''usage pour un besoin précis. Vous prenez sur vous les risques liés au bon emploi du logiciel.</p>\r\n\r\n<h2>7. Limitation de devoir</h2>\r\n<p>Comme l''énonce l''article 16 de l''AGPL-3.0, et dans toute la mesure permise par la loi applicable, en aucun cas un titulaire de droits d''auteur ou toute autre partie qui modifie ou transmet le logiciel ne saurait être tenu responsable envers vous de dommages, y compris tout dommage général, spécial, accessoire ou consécutif liés à l''usage du code.</p>\r\n\r\n<h2>8. Droit applicable</h2>\r\n<p>Ces conditions suivent les lois de la province de Québec et les lois du Canada applicables. Rien ici ne réduit vos droits stricts de consommateur selon ces lois.</p>\r\n\r\n<h2>9. Modifications</h2>\r\n<p>Nous pouvons réviser ces conditions de temps à autre. Les changements importants seront communiqués au moyen des Services, et l''utilisation continue des Services après leur entrée en vigueur vaut acceptation.</p>\r\n\r\n<h2>10. Nous joindre</h2>\r\n<p>Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n'::text)),\n updated_at = now()\n WHERE id = 94\n AND page_id = 12\n AND content->>'html_content' LIKE '%Conditions d''utilisation%';\n\nEND $$;\n"
190
+ "sql": "-- Migration: 00000000000034_seed_seo_french_legal_readability.sql\r\n-- Description: Optimize French legal pages (politique de confidentialité & conditions d'utilisation) readability to achieve 100/100 Page SEO score.\r\n-- Safety: Forward-only, content-guarded updates that only apply to default seeded content.\r\n\r\nDO $$\r\nBEGIN\r\n -- Page 10: Politique de confidentialité FR (Block 92)\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Politique de confidentialité</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n<p>NextBlock™ CMS (« nous ») respecte votre vie privée. Nous protégeons vos données personnelles selon la Loi 25 du Québec, la loi fédérale LPRPDE et les règles canadiennes anti-pourriel (LCAP).</p>\r\n\r\n<h2>1. Responsable de la protection des données personnelles</h2>\r\n<p>Notre responsable veille au respect des règles de confidentialité. Vous pouvez lui écrire à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>2. Renseignements que nous recueillons</h2>\r\n<ul>\r\n <li><strong>Renseignements de compte</strong> &mdash; nom, adresse courriel et identifiants lors de l''inscription.</li>\r\n <li><strong>Données d''utilisation et d''appareil</strong> &mdash; recueillies uniquement avec votre consentement au moyen de outils web.</li>\r\n <li><strong>Communications</strong> &mdash; les messages que vous nous envoyez et vos préférences marketing.</li>\r\n</ul>\r\n\r\n<h2>3. Finalités et consentement</h2>\r\n<p>Nous recueillons vos données pour des motifs clairs : faire fonctionner nos services, sécuriser les accès et vous répondre. Les outils d''analyse et de suivi ne s''activent qu''avec votre accord clair. Selon la Loi 25, les témoins optionnels restent coupés tant que vous ne les acceptez pas. Vous pouvez retirer votre accord en tout temps.</p>\r\n\r\n<h2>4. Témoins et technologies de suivi</h2>\r\n<p>Les témoins essentiels assurent le bon usage du site. Ils ne demandent aucun accord préalable. Les témoins d''analyse se chargent seulement après votre choix sur notre bandeau. Nous gardons votre choix en mémoire pour le respecter.</p>\r\n\r\n<h2>5. Partage des données</h2>\r\n<p>Nous ne vendons pas vos données personnelles. Nous ne les communiquons qu''à des fournisseurs qui nous aident à exploiter la plateforme, sous obligation de confidentialité, ou lorsque la loi l''exige.</p>\r\n\r\n<h2>6. Conservation</h2>\r\n<p>Nous ne conservons les données personnelles que le temps nécessaire aux fins décrites ou exigé par la loi, après quoi ils sont détruits ou anonymisés sans risque.</p>\r\n\r\n<h2>7. Vos droits</h2>\r\n<p>Vous avez le droit de lire, de corriger et de faire effacer vos données. Vous pouvez aussi retirer votre accord ou demander une copie de vos données. Pour exercer vos droits, écrivez à notre responsable à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\r\n<p>Nous n''envoyons des messages électroniques commerciaux qu''avec votre consentement. Chaque message nous identifie et comporte un mécanisme de désabonnement fonctionnel que nous respectons rapidement.</p>\r\n\r\n<h2>9. Mesures de sécurité</h2>\r\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées &mdash; dont le chiffrement en transit et le contrôle des accès &mdash; pour protéger vos données.</p>\r\n\r\n<h2>10. Logiciel libre</h2>\r\n<p>NextBlock™ CMS est un logiciel libre sous licence AGPLv3. Si vous hébergez NextBlock vous-même, vous gérez votre propre serveur. Vous êtes responsable des données sur votre instance, et ce texte est un modèle que vous pouvez adapter.</p>\r\n\r\n<h2>11. Modifications</h2>\r\n<p>Nous pouvons mettre à jour cette politique. Les changements importants seront communiqués sur le site et la date de mise à jour sera révisée.</p>\r\n\r\n<h2>12. Nous joindre</h2>\r\n<p>Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi vous adresser à la Commission d''accès à l''information du Québec.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE id = 92\r\n AND page_id = 10\r\n AND content->>'html_content' LIKE '%Politique de confidentialité%';\r\n\r\n -- Page 12: Conditions d'utilisation FR (Block 94)\r\n UPDATE public.blocks\r\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Règles du service</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n\r\n<h2>1. Acceptation des conditions</h2>\r\n<p>En utilisant le CMS NextBlock™ et les services associés (les « Services »), vous acceptez ces règles du service. Si vous refusez ces règles, veuillez ne pas utiliser les Services.</p>\r\n\r\n<h2>2. Logiciel libre et à code source ouvert</h2>\r\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou, à votre choix, toute version ultérieure. Vous êtes libre d''exécuter, d''étudier, de partager et de modifier le logiciel selon les termes de cette licence. Une copie de la licence est fournie avec le logiciel et est aussi disponible à <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>\r\n<p>Droit d''auteur © 2025 NextBlock™ CMS.</p>\r\n\r\n<h2>3. Disponibilité du code source</h2>\r\n<p>Conformément à l''article 13 de l''AGPL-3.0, si vous exploitez une version modifiée de NextBlock™ CMS et la rendez accessible à des utilisateurs sur un réseau, vous devez offrir clairement à ces utilisateurs l''accès au code source correspondant de votre version modifiée, gratuitement, par un moyen usuel de copie de logiciels.</p>\r\n\r\n<h2>4. Marques de commerce</h2>\r\n<p>L''AGPL-3.0 accorde de larges droits sur le code source du logiciel, mais <strong>n''accorde aucun droit</strong> sur nos noms commerciaux, marques de commerce ou marques de service. « NextBlock™ », le nom NextBlock™ CMS et les logos associés demeurent notre propriété et ne peuvent être utilisés d''une manière laissant entendre une approbation ou une affiliation sans notre autorisation écrite préalable.</p>\r\n\r\n<h2>5. Comptes et utilisation acceptable</h2>\r\n<p>Si vous créez un compte, vous êtes responsable de la protection de vos identifiants et de toute activité effectuée à partir de votre compte, et vous vous engagez à nous aviser rapidement de toute utilisation non autorisée. Vous vous engagez à ne pas détourner les Services, notamment en tentant de les perturber, d''y accéder sans autorisation ou de les utiliser à des fins illégales.</p>\r\n\r\n<h2>6. Absence de garantie</h2>\r\n<p>Selon l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou tacite. Cela inclut les garanties de vente ou d''usage pour un besoin précis. Vous prenez sur vous les risques liés au bon emploi du logiciel.</p>\r\n\r\n<h2>7. Limitation de devoir</h2>\r\n<p>Comme l''énonce l''article 16 de l''AGPL-3.0, et dans toute la mesure permise par la loi applicable, en aucun cas un titulaire de droits d''auteur ou toute autre partie qui modifie ou transmet le logiciel ne saurait être tenu responsable envers vous de dommages, y compris tout dommage général, spécial, accessoire ou consécutif liés à l''usage du code.</p>\r\n\r\n<h2>8. Droit applicable</h2>\r\n<p>Ces conditions suivent les lois de la province de Québec et les lois du Canada applicables. Rien ici ne réduit vos droits stricts de consommateur selon ces lois.</p>\r\n\r\n<h2>9. Modifications</h2>\r\n<p>Nous pouvons réviser ces conditions de temps à autre. Les changements importants seront communiqués au moyen des Services, et l''utilisation continue des Services après leur entrée en vigueur vaut acceptation.</p>\r\n\r\n<h2>10. Nous joindre</h2>\r\n<p>Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n'::text)),\r\n updated_at = now()\r\n WHERE id = 94\r\n AND page_id = 12\r\n AND content->>'html_content' LIKE '%Conditions d''utilisation%';\r\n\r\nEND $$;\r\n"
191
191
  },
192
192
  {
193
193
  "version": "00000000000035",
194
194
  "name": "00000000000035_digital_products_seo_optimizations.sql",
195
- "sql": "-- Migration: 00000000000035_digital_products_seo_optimizations.sql\n-- Purpose: Set meta_title and meta_description for the 2 real digital products (EN + FR)\n-- and optimize their block heading levels and copy readability to achieve 100/100 SEO scores.\n-- Safety: Append-only, forward-only, content-guarded updates.\n\nBEGIN;\n\n-- 1. Digital Products SEO Metadata (Titles & Descriptions)\n-- NextBlock Cortex AI - Cortex AI License (EN)\nUPDATE public.products\nSET meta_title = 'NextBlock Cortex AI License | Native AI Content Engine',\n meta_description = 'Add native AI copywriting, real-time block translation, and structure refactoring to your NextBlock editor with our BYOK Cortex AI license.',\n updated_at = NOW()\nWHERE id = 'd48a4bb4-5119-4576-9c1d-32c404dd84f8';\n\n-- Licence NextBlock™ Cortex AI (FR)\nUPDATE public.products\nSET meta_title = 'Licence NextBlock Cortex AI | Moteur IA pour Éditeur',\n meta_description = 'Intégrez la rédaction assistée par IA, la traduction de blocs et l''optimisation de structure dans NextBlock grâce à la licence Cortex AI.',\n updated_at = NOW()\nWHERE id = '69848224-a6d6-43c9-9748-f41a8e8e424b';\n\n-- NextBlock Commerce Pro - Commerce License (EN)\nUPDATE public.products\nSET meta_title = 'NextBlock Commerce Pro | Global Next.js Storefronts',\n meta_description = 'Launch high-performance digital and physical stores with headless Stripe checkout, Freemius licensing, and multi-currency pricing in NextBlock.',\n updated_at = NOW()\nWHERE id = 'ed86bd77-da61-4e6f-826c-956f9a55b7aa';\n\n-- Licence NextBlock™ Commerce Pro (FR)\nUPDATE public.products\nSET meta_title = 'Licence NextBlock Commerce Pro | E-Commerce Next.js',\n meta_description = 'Créez des boutiques Next.js performantes avec paiement Stripe headless, licences logicielles Freemius et gestion multi-devises dans NextBlock.',\n updated_at = NOW()\nWHERE id = '5c9e1b99-eeed-4fe0-adfa-31e81004c2f9';\n\n-- 2. Digital Products Block Content & Heading Optimizations\n-- Block ID 39\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Moteur E-Commerce d''Entreprise</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Faites de Next.js une Boutique Mondiale.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Commerce Pro est un moteur de boutique moderne conçu pour NextBlock. Il gère vos catalogues physiques et vos produits numériques avec rapidité. Tout tourne sur votre propre serveur avec un contrôle total de vos données.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">Pourquoi choisir Commerce Pro</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Multi-Devises</strong> — Taux de change en direct, règles d''arrondis et détection automatique du pays de l''acheteur.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Taxes automatiques</strong> — Prise en charge native de Stripe Tax pour respecter les règles fiscales locales.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Catalogue hybride</strong> — Vendez des produits physiques avec gestion de stock et des licences logicielles.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 39;\n\n-- Block ID 41\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">💳</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Checkout Stripe Headless</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Paiement direct et sécurisé pour un ou plusieurs articles. Les commandes se valident automatiquement grâce aux webhooks Stripe.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🔑</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Licences Numériques Freemius</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Distribuez vos logiciels, activez vos clés et gérez les périodes d''essai SaaS. Tout fonctionne directement avec Freemius sans souci.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🛍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Merchandising Visuel</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Concevez vos fiches produits, mettez en valeur vos promotions et créez vos rayons directement dans l''éditeur de blocs.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 41;\n\n-- Block ID 42\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#064e3b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Sous le capot</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Conçu pour la production à grande échelle</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Commerce Pro a été pensé dès le début pour les boutiques actives. Chaque route API est mise en cache, et les requêtes SQL sont optimisées.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Que vous vendiez des logiciels ou des biens réels, votre site reste fluide. Il absorbe les pics de trafic sans ralentissement.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Gestion des commandes</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Suivez chaque commande du panier à la livraison. Mises à jour d''état, e-mails de confirmation et retours sont gérés d''office.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Catégories de produits</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Organisez vos articles avec des URLs claires, des photos et des filtres de langues. Pilotez tout depuis le tableau de bord du CMS.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Boutiques multilingues</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Traduisez vos articles et vos rayons dans toutes les langues. Les stocks sont partagés tout en gardant des métadonnées SEO propres.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 42;\n\n-- Block ID 43\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#022c22\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Prêt à lancer votre boutique ?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\\">Commencez à vendre dès aujourd''hui avec Commerce Pro. Multi-devises, conforme aux taxes et rapide dès l''installation.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\",\"size\":\"lg\",\"text\":\"Acheter Commerce Pro\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 43;\n\n-- Block ID 49\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">Couche d''Intelligence IA</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Boostez votre éditeur avec l''IA native.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Cortex AI apporte la puissance des grands modèles de langage dans votre éditeur. Rédigez des articles, améliorez vos textes et traduisez des pages en quelques clics. Les blocs créés restent stables, rapides et fidèles à votre charte.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">OpenRouter et architecture BYOK</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Apportez votre clé API</strong> — Maîtrisez vos coûts avec vos propres clés OpenRouter. Aucun frais caché.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Contexte structuré</strong> — L''IA comprend la mise en page et les colonnes pour des sorties toujours précises.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Liberté de modèle</strong> — Choisissez librement entre Claude, GPT et les modèles open-source sans changer de code.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 49;\n\n-- Block ID 51\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">✍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Rédaction en un clic</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Rédigez des guides, ajustez vos titres et créez des appels à l''action. Cortex AI adopte votre ton et écrit un texte clair.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🔄</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Refactorisation de structure</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Ajustez vos colonnes, ajoutez des grilles et réorganisez vos sections. Cortex AI génère des données valides sans effort.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🌐</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Traduction automatique</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Traduisez vos pages complètes entre le français et l''anglais. Cortex AI conserve vos mises en page et styles intacts.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 51;\n\n-- Block ID 52\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">Comment ça marche</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Une IA qui comprend les blocs</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">La plupart des outils IA créent du texte brut sans structure. Cortex AI a été pensé pour l''éditeur de NextBlock. Il analyse vos blocs, vos colonnes et vos options visuelles.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Cette approche évite les erreurs de balisage et les données invalides. Votre équipe obtient des rendus élégants sur mobile et ordinateur.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Génération de contenu</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Produisez du contenu percutant pour vos pages et articles. L''assistant respecte votre style et délivre des textes fluides.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Optimisation SEO</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Rédigez des titres et méta descriptions de qualité. Cortex AI vérifie la lisibilité et les mots clés avant publication.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Conception axée sur la vie privée</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Vos requêtes vont directement à votre fournisseur d''IA via votre clé privée. Vos données ne sont jamais stockées ni revendues.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 52;\n\n-- Block ID 53\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Prêt à ajouter l''IA à votre éditeur ?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\\">Libérez la puissance de la création de contenu avec l''IA. Connectez votre clé, protégez vos données et rédigez vite.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"size\":\"lg\",\"text\":\"Acheter Cortex AI\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 53;\n\n-- Block ID 65\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Enterprise E-Commerce Engine</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Turn Next.js Into a Global Storefront.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Commerce Pro is a fast store engine built for NextBlock. It helps teams sell physical goods and digital downloads with ease. Everything runs on your own server with total data ownership and zero extra fees.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">Why Teams Choose Commerce Pro</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Multi-Currency Pricing</strong> — Live exchange rates, clear charm pricing, and automatic visitor country detection.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Simple Tax Rules</strong> — Native Stripe Tax support calculates local taxes automatically during checkout.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Hybrid Product Catalog</strong> — Sell physical goods with stock counts alongside digital software license keys.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 65;\n\n-- Block ID 67\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">💳</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Headless Stripe Checkout</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Fast checkout flows for single or multiple items. Orders process automatically through secure webhooks with zero friction.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🔑</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Freemius Digital Licensing</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Deliver software files, issue license keys, and manage SaaS free trials easily. Everything connects directly to Freemius.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🛍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Visual Merchandising</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Design custom product pages, highlight special deals, and build shopping grids right inside the visual block editor.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 67;\n\n-- Block ID 68\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#064e3b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Under The Hood</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Built for Production Scale</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Commerce Pro was built from day one for busy storefronts. Every API path is cached near your users, and database queries use fast indexes.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Whether you sell digital downloads or retail goods, your storefront loads fast. It handles high traffic spikes without slow downs or crashes.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Order Management</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Track customer orders from cart to shipping. Automatic status updates, buyer receipts, and refunds work right out of the box.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Product Categories</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Organize products with clean web addresses, category images, and full multi-language tags. Manage it all from the CMS dashboard.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Multi-Language Storefronts</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Translate products and category pages into any language. Each language shares inventory while keeping its own SEO metadata.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 68;\n\n-- Block ID 69\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#022c22\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Ready to launch your storefront?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\\">Start selling today with Commerce Pro. It is multi-currency, tax-compliant, and lightning-fast out of the box.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"#\",\"size\":\"lg\",\"text\":\"Purchase Commerce Pro\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 69;\n\n-- Block ID 70\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">AI Intelligence Layer</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Supercharge Your Editor with AI.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Cortex AI brings powerful language models directly into your editor. You can create content, edit blog drafts, and translate full pages in seconds. It writes clean block data rather than raw text, keeping your layouts stable and fast.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">OpenRouter &amp; BYOK Architecture</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Bring Your Own Key</strong> — You control costs using your own OpenRouter tokens. There are no hidden fees or markups.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Block-Aware Prompts</strong> — Models receive structured context. They see headings, columns, and layout rules for accurate outputs.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Model Flexibility</strong> — Switch freely between Claude, GPT, and open-source models without changing code.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 70;\n\n-- Block ID 72\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">✍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">One-Click Copywriting</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Write blog posts, polish headlines, and craft calls to action. Cortex AI matches your brand voice and writes clear prose with ease.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🔄</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Structure Refactoring</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Reorganize layout grids, split columns, and reorder sections. Cortex AI produces valid block schemas that render safely every time.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🌐</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Automated Translation</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Translate entire pages between languages with one click. Cortex AI keeps your styles, nested sections, and media links intact.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 72;\n\n-- Block ID 73\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">How It Works</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">AI That Understands Blocks</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Most AI tools only generate flat text strings. Cortex AI is built specifically for NextBlock editors. It reads your full page structure, column arrays, and style options.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">This deep connection prevents broken markup and invalid data. Your team gets clean results that look great on desktop and mobile screens right away.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Content Generation</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Create engaging copy for marketing pages and help docs. The assistant follows your editorial style guide and produces clean text.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">SEO Optimization</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Draft concise meta titles and meta descriptions automatically. Cortex AI checks keyword density and reading ease before you publish.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Privacy-First Design</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Your content goes directly to your chosen provider via private API keys. We never store, log, or train on your private data.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 73;\n\n-- Block ID 74\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Ready to add AI to your editor?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\\">Unlock the full power of AI-driven content creation. Connect your key, protect your privacy, and edit faster today.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"#\",\"size\":\"lg\",\"text\":\"Purchase Cortex AI\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 74;\n\nCOMMIT;\n"
195
+ "sql": "-- Migration: 00000000000035_digital_products_seo_optimizations.sql\r\n-- Purpose: Set meta_title and meta_description for the 2 real digital products (EN + FR)\r\n-- and optimize their block heading levels and copy readability to achieve 100/100 SEO scores.\r\n-- Safety: Append-only, forward-only, content-guarded updates.\r\n\r\nBEGIN;\r\n\r\n-- 1. Digital Products SEO Metadata (Titles & Descriptions)\r\n-- NextBlock Cortex AI - Cortex AI License (EN)\r\nUPDATE public.products\r\nSET meta_title = 'NextBlock Cortex AI License | Native AI Content Engine',\r\n meta_description = 'Add native AI copywriting, real-time block translation, and structure refactoring to your NextBlock editor with our BYOK Cortex AI license.',\r\n updated_at = NOW()\r\nWHERE id = 'd48a4bb4-5119-4576-9c1d-32c404dd84f8';\r\n\r\n-- Licence NextBlock™ Cortex AI (FR)\r\nUPDATE public.products\r\nSET meta_title = 'Licence NextBlock Cortex AI | Moteur IA pour Éditeur',\r\n meta_description = 'Intégrez la rédaction assistée par IA, la traduction de blocs et l''optimisation de structure dans NextBlock grâce à la licence Cortex AI.',\r\n updated_at = NOW()\r\nWHERE id = '69848224-a6d6-43c9-9748-f41a8e8e424b';\r\n\r\n-- NextBlock Commerce Pro - Commerce License (EN)\r\nUPDATE public.products\r\nSET meta_title = 'NextBlock Commerce Pro | Global Next.js Storefronts',\r\n meta_description = 'Launch high-performance digital and physical stores with headless Stripe checkout, Freemius licensing, and multi-currency pricing in NextBlock.',\r\n updated_at = NOW()\r\nWHERE id = 'ed86bd77-da61-4e6f-826c-956f9a55b7aa';\r\n\r\n-- Licence NextBlock™ Commerce Pro (FR)\r\nUPDATE public.products\r\nSET meta_title = 'Licence NextBlock Commerce Pro | E-Commerce Next.js',\r\n meta_description = 'Créez des boutiques Next.js performantes avec paiement Stripe headless, licences logicielles Freemius et gestion multi-devises dans NextBlock.',\r\n updated_at = NOW()\r\nWHERE id = '5c9e1b99-eeed-4fe0-adfa-31e81004c2f9';\r\n\r\n-- 2. Digital Products Block Content & Heading Optimizations\r\n-- Block ID 39\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Moteur E-Commerce d''Entreprise</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Faites de Next.js une Boutique Mondiale.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Commerce Pro est un moteur de boutique moderne conçu pour NextBlock. Il gère vos catalogues physiques et vos produits numériques avec rapidité. Tout tourne sur votre propre serveur avec un contrôle total de vos données.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">Pourquoi choisir Commerce Pro</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Multi-Devises</strong> — Taux de change en direct, règles d''arrondis et détection automatique du pays de l''acheteur.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Taxes automatiques</strong> — Prise en charge native de Stripe Tax pour respecter les règles fiscales locales.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Catalogue hybride</strong> — Vendez des produits physiques avec gestion de stock et des licences logicielles.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 39;\r\n\r\n-- Block ID 41\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">💳</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Checkout Stripe Headless</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Paiement direct et sécurisé pour un ou plusieurs articles. Les commandes se valident automatiquement grâce aux webhooks Stripe.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🔑</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Licences Numériques Freemius</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Distribuez vos logiciels, activez vos clés et gérez les périodes d''essai SaaS. Tout fonctionne directement avec Freemius sans souci.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🛍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Merchandising Visuel</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Concevez vos fiches produits, mettez en valeur vos promotions et créez vos rayons directement dans l''éditeur de blocs.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 41;\r\n\r\n-- Block ID 42\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#064e3b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Sous le capot</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Conçu pour la production à grande échelle</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Commerce Pro a été pensé dès le début pour les boutiques actives. Chaque route API est mise en cache, et les requêtes SQL sont optimisées.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Que vous vendiez des logiciels ou des biens réels, votre site reste fluide. Il absorbe les pics de trafic sans ralentissement.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Gestion des commandes</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Suivez chaque commande du panier à la livraison. Mises à jour d''état, e-mails de confirmation et retours sont gérés d''office.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Catégories de produits</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Organisez vos articles avec des URLs claires, des photos et des filtres de langues. Pilotez tout depuis le tableau de bord du CMS.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Boutiques multilingues</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Traduisez vos articles et vos rayons dans toutes les langues. Les stocks sont partagés tout en gardant des métadonnées SEO propres.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 42;\r\n\r\n-- Block ID 43\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#022c22\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Prêt à lancer votre boutique ?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\\">Commencez à vendre dès aujourd''hui avec Commerce Pro. Multi-devises, conforme aux taxes et rapide dès l''installation.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\",\"size\":\"lg\",\"text\":\"Acheter Commerce Pro\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 43;\r\n\r\n-- Block ID 49\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">Couche d''Intelligence IA</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Boostez votre éditeur avec l''IA native.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Cortex AI apporte la puissance des grands modèles de langage dans votre éditeur. Rédigez des articles, améliorez vos textes et traduisez des pages en quelques clics. Les blocs créés restent stables, rapides et fidèles à votre charte.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">OpenRouter et architecture BYOK</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Apportez votre clé API</strong> — Maîtrisez vos coûts avec vos propres clés OpenRouter. Aucun frais caché.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Contexte structuré</strong> — L''IA comprend la mise en page et les colonnes pour des sorties toujours précises.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Liberté de modèle</strong> — Choisissez librement entre Claude, GPT et les modèles open-source sans changer de code.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 49;\r\n\r\n-- Block ID 51\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">✍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Rédaction en un clic</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Rédigez des guides, ajustez vos titres et créez des appels à l''action. Cortex AI adopte votre ton et écrit un texte clair.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🔄</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Refactorisation de structure</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Ajustez vos colonnes, ajoutez des grilles et réorganisez vos sections. Cortex AI génère des données valides sans effort.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🌐</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Traduction automatique</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Traduisez vos pages complètes entre le français et l''anglais. Cortex AI conserve vos mises en page et styles intacts.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 51;\r\n\r\n-- Block ID 52\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">Comment ça marche</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Une IA qui comprend les blocs</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">La plupart des outils IA créent du texte brut sans structure. Cortex AI a été pensé pour l''éditeur de NextBlock. Il analyse vos blocs, vos colonnes et vos options visuelles.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Cette approche évite les erreurs de balisage et les données invalides. Votre équipe obtient des rendus élégants sur mobile et ordinateur.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Génération de contenu</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Produisez du contenu percutant pour vos pages et articles. L''assistant respecte votre style et délivre des textes fluides.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Optimisation SEO</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Rédigez des titres et méta descriptions de qualité. Cortex AI vérifie la lisibilité et les mots clés avant publication.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Conception axée sur la vie privée</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Vos requêtes vont directement à votre fournisseur d''IA via votre clé privée. Vos données ne sont jamais stockées ni revendues.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 52;\r\n\r\n-- Block ID 53\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Prêt à ajouter l''IA à votre éditeur ?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\\">Libérez la puissance de la création de contenu avec l''IA. Connectez votre clé, protégez vos données et rédigez vite.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"size\":\"lg\",\"text\":\"Acheter Cortex AI\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 53;\r\n\r\n-- Block ID 65\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Enterprise E-Commerce Engine</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Turn Next.js Into a Global Storefront.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Commerce Pro is a fast store engine built for NextBlock. It helps teams sell physical goods and digital downloads with ease. Everything runs on your own server with total data ownership and zero extra fees.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">Why Teams Choose Commerce Pro</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Multi-Currency Pricing</strong> — Live exchange rates, clear charm pricing, and automatic visitor country detection.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Simple Tax Rules</strong> — Native Stripe Tax support calculates local taxes automatically during checkout.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Hybrid Product Catalog</strong> — Sell physical goods with stock counts alongside digital software license keys.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 65;\r\n\r\n-- Block ID 67\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">💳</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Headless Stripe Checkout</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Fast checkout flows for single or multiple items. Orders process automatically through secure webhooks with zero friction.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🔑</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Freemius Digital Licensing</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Deliver software files, issue license keys, and manage SaaS free trials easily. Everything connects directly to Freemius.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🛍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Visual Merchandising</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Design custom product pages, highlight special deals, and build shopping grids right inside the visual block editor.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 67;\r\n\r\n-- Block ID 68\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#064e3b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Under The Hood</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Built for Production Scale</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Commerce Pro was built from day one for busy storefronts. Every API path is cached near your users, and database queries use fast indexes.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Whether you sell digital downloads or retail goods, your storefront loads fast. It handles high traffic spikes without slow downs or crashes.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Order Management</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Track customer orders from cart to shipping. Automatic status updates, buyer receipts, and refunds work right out of the box.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Product Categories</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Organize products with clean web addresses, category images, and full multi-language tags. Manage it all from the CMS dashboard.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Multi-Language Storefronts</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Translate products and category pages into any language. Each language shares inventory while keeping its own SEO metadata.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 68;\r\n\r\n-- Block ID 69\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#022c22\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Ready to launch your storefront?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\\">Start selling today with Commerce Pro. It is multi-currency, tax-compliant, and lightning-fast out of the box.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"#\",\"size\":\"lg\",\"text\":\"Purchase Commerce Pro\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 69;\r\n\r\n-- Block ID 70\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">AI Intelligence Layer</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Supercharge Your Editor with AI.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Cortex AI brings powerful language models directly into your editor. You can create content, edit blog drafts, and translate full pages in seconds. It writes clean block data rather than raw text, keeping your layouts stable and fast.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">OpenRouter &amp; BYOK Architecture</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Bring Your Own Key</strong> — You control costs using your own OpenRouter tokens. There are no hidden fees or markups.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Block-Aware Prompts</strong> — Models receive structured context. They see headings, columns, and layout rules for accurate outputs.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Model Flexibility</strong> — Switch freely between Claude, GPT, and open-source models without changing code.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 70;\r\n\r\n-- Block ID 72\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">✍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">One-Click Copywriting</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Write blog posts, polish headlines, and craft calls to action. Cortex AI matches your brand voice and writes clear prose with ease.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🔄</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Structure Refactoring</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Reorganize layout grids, split columns, and reorder sections. Cortex AI produces valid block schemas that render safely every time.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🌐</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Automated Translation</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Translate entire pages between languages with one click. Cortex AI keeps your styles, nested sections, and media links intact.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 72;\r\n\r\n-- Block ID 73\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">How It Works</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">AI That Understands Blocks</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Most AI tools only generate flat text strings. Cortex AI is built specifically for NextBlock editors. It reads your full page structure, column arrays, and style options.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">This deep connection prevents broken markup and invalid data. Your team gets clean results that look great on desktop and mobile screens right away.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Content Generation</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Create engaging copy for marketing pages and help docs. The assistant follows your editorial style guide and produces clean text.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">SEO Optimization</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Draft concise meta titles and meta descriptions automatically. Cortex AI checks keyword density and reading ease before you publish.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Privacy-First Design</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Your content goes directly to your chosen provider via private API keys. We never store, log, or train on your private data.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 73;\r\n\r\n-- Block ID 74\r\nUPDATE public.blocks\r\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Ready to add AI to your editor?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\\">Unlock the full power of AI-driven content creation. Connect your key, protect your privacy, and edit faster today.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"#\",\"size\":\"lg\",\"text\":\"Purchase Cortex AI\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\r\n updated_at = NOW()\r\nWHERE id = 74;\r\n\r\nCOMMIT;\r\n"
196
196
  },
197
197
  {
198
198
  "version": "00000000000036",
199
199
  "name": "00000000000036_seed_posts_missing_seo_metadata.sql",
200
- "sql": "-- Migration: 00000000000036_seed_posts_missing_seo_metadata.sql\n-- Purpose: Set meta_title and meta_description for remaining seeded blog posts\n-- (how-nextblock-works, comment-nextblock-fonctionne, nextblock-commerce-guide, guide-commerce-nextblock)\n-- so all 9 seeded posts achieve a 100/100 SEO score.\n-- Safety: Append-only, forward-only, content-guarded updates.\n\nBEGIN;\n\nUPDATE public.posts\n SET meta_title = 'How NextBlock™ Works: Architecture & Under the Hood',\n meta_description = 'Explore the architecture behind NextBlock: monorepo packages, typed block registry, Supabase database, and modern Next.js editor stack.',\n updated_at = NOW()\n WHERE slug = 'how-nextblock-works'\n AND meta_title IS NULL;\n\nUPDATE public.posts\n SET meta_title = 'Comment fonctionne NextBlock™ : Architecture et Moteur',\n meta_description = 'Découvrez les fondations de NextBlock : monorepo, registre de blocs typés, base de données Supabase et moteur d''édition Next.js moderne.',\n updated_at = NOW()\n WHERE slug = 'comment-nextblock-fonctionne'\n AND meta_title IS NULL;\n\nUPDATE public.posts\n SET meta_title = 'The Complete NextBlock™ Commerce Guide | Headless Store',\n meta_description = 'Build global e-commerce with NextBlock: manage products, configure multi-currency pricing, and accept payments with headless Stripe checkout.',\n updated_at = NOW()\n WHERE slug = 'nextblock-commerce-guide'\n AND meta_title IS NULL;\n\nUPDATE public.posts\n SET meta_title = 'Guide Complet du Commerce avec NextBlock™ | E-Commerce',\n meta_description = 'Développez votre boutique avec NextBlock : gestion de produits, tarifs multi-devises et tunnel de paiement headless avec Stripe Checkout.',\n updated_at = NOW()\n WHERE slug = 'guide-commerce-nextblock'\n AND meta_title IS NULL;\n\nCOMMIT;\n"
200
+ "sql": "-- Migration: 00000000000036_seed_posts_missing_seo_metadata.sql\r\n-- Purpose: Set meta_title and meta_description for remaining seeded blog posts\r\n-- (how-nextblock-works, comment-nextblock-fonctionne, nextblock-commerce-guide, guide-commerce-nextblock)\r\n-- so all 9 seeded posts achieve a 100/100 SEO score.\r\n-- Safety: Append-only, forward-only, content-guarded updates.\r\n\r\nBEGIN;\r\n\r\nUPDATE public.posts\r\n SET meta_title = 'How NextBlock™ Works: Architecture & Under the Hood',\r\n meta_description = 'Explore the architecture behind NextBlock: monorepo packages, typed block registry, Supabase database, and modern Next.js editor stack.',\r\n updated_at = NOW()\r\n WHERE slug = 'how-nextblock-works'\r\n AND meta_title IS NULL;\r\n\r\nUPDATE public.posts\r\n SET meta_title = 'Comment fonctionne NextBlock™ : Architecture et Moteur',\r\n meta_description = 'Découvrez les fondations de NextBlock : monorepo, registre de blocs typés, base de données Supabase et moteur d''édition Next.js moderne.',\r\n updated_at = NOW()\r\n WHERE slug = 'comment-nextblock-fonctionne'\r\n AND meta_title IS NULL;\r\n\r\nUPDATE public.posts\r\n SET meta_title = 'The Complete NextBlock™ Commerce Guide | Headless Store',\r\n meta_description = 'Build global e-commerce with NextBlock: manage products, configure multi-currency pricing, and accept payments with headless Stripe checkout.',\r\n updated_at = NOW()\r\n WHERE slug = 'nextblock-commerce-guide'\r\n AND meta_title IS NULL;\r\n\r\nUPDATE public.posts\r\n SET meta_title = 'Guide Complet du Commerce avec NextBlock™ | E-Commerce',\r\n meta_description = 'Développez votre boutique avec NextBlock : gestion de produits, tarifs multi-devises et tunnel de paiement headless avec Stripe Checkout.',\r\n updated_at = NOW()\r\n WHERE slug = 'guide-commerce-nextblock'\r\n AND meta_title IS NULL;\r\n\r\nCOMMIT;\r\n"
201
+ },
202
+ {
203
+ "version": "00000000000037",
204
+ "name": "00000000000037_reposition_marketing_and_cortex_mcp.sql",
205
+ "sql": "-- 00000000000037_reposition_marketing_and_cortex_mcp.sql\n--\n-- Repositions the seeded marketing content around NextBlock's differentiator: an\n-- AI-native website builder where Cortex AI connects to the operator's OWN AI\n-- subscription over MCP (Model Context Protocol), leaving the team with a\n-- production Next.js 16 + Supabase site and a visual CMS rather than disposable\n-- prototype code. Four pieces plus one repair:\n--\n-- 1. Home page (EN, slug 'home'): meta title/description and four section\n-- blocks rewritten in place — the hero, the \"Key Features\" section (now the\n-- prototype-vs-production comparison), the \"Built with the Best\" section\n-- (now the three-step MCP walkthrough) and the Cortex AI promo. Sections that\n-- 032 already tuned (Commerce, ecosystem, community, contact) are untouched.\n-- 2. Cortex AI product (EN, slug 'nextblock-cortex-ai-cortex-ai-license'):\n-- title, meta, short description and the five description sections, now\n-- leading with the MCP execution model and the five MCP-contract tools\n-- (get_database_schema, generate_jsonb_layout, update_site_navigation,\n-- query_site_analytics, search_stock_media — the names the registry\n-- actually exposes; see libs/cortex/src/lib/mcp-tool-registry.ts).\n-- 3. The \"How Updating NextBlock Works\" EN/FR posts get a dedicated feature\n-- image (public/images/update_nextblock.webp) instead of the shared\n-- extensibility artwork.\n-- 4. A new EN post, 'cortex-ai-mcp-connection-guide', walks through connecting\n-- Claude Code and Cursor to /api/mcp. Its body is stored as a JSON-stringified\n-- Tiptap document in blocks.content.html_content — the Notion editor's native\n-- format, which the text renderer, the editor and the SEO engine all accept.\n-- (00000000000038 replaces that body with the styled, illustrated version.)\n-- 5. Repair: 00000000000035 updated blocks by hard-coded id. On installs whose\n-- ids drifted from the baseline (the hourly-reset sandbox is one), those ids\n-- belonged to other rows, so French Commerce Pro product copy landed on the\n-- EN/FR home pages (replacing the Live Demo promo) and on the first block of\n-- the French install guide, which therefore rendered empty. The repair is\n-- scoped by content signature + parent, so it is a no-op where 035 hit the\n-- rows it meant to.\n--\n-- Every page, post and product touched here grades 100/100 in the built-in SEO\n-- engine (libs/utils/src/lib/seo) with these focus keyphrases, which the CMS\n-- audit panel does not persist and therefore has to be typed in by the editor:\n-- home → \"AI website builder CMS\"\n-- product → \"Cortex AI MCP server\"\n-- post → \"connect Claude to NextBlock CMS\"\n--\n-- Forward-only and idempotent by construction: every UPDATE is guarded by the\n-- seeded copy it replaces (or by the copy it writes), inserts use NOT EXISTS /\n-- ON CONFLICT, and nothing is keyed by a numeric id. blocks.content is JSONB\n-- written with dollar-quoting (as in 006/009/020) so the HTML keeps ordinary\n-- single-quoted attributes. Keep the word \"sandbox\" out of this filename.\n--\n-- After adding this file: npm run generate:migrations-bundle && npm run\n-- generate:sandbox && npm run sync:create-nextblock.\n\nDO $body$\nDECLARE\n v_en integer;\n v_fr integer;\n v_home integer;\n v_accueil integer;\n v_setup_fr integer;\n v_cortex uuid;\n v_update_media uuid;\n v_post_media uuid;\n v_post integer;\nBEGIN\n SELECT id INTO v_en FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n ---------------------------------------------------------------------------\n -- 0. Media rows for the two bundled images (public/images/*.webp).\n -- media.object_key is UNIQUE, so the row is keyed by that and the id is\n -- resolved back rather than assumed (the sandbox reset upserts by key too).\n ---------------------------------------------------------------------------\n INSERT INTO public.media (id, uploader_id, file_name, object_key, file_path, folder, file_type, size_bytes, description, width, height)\n VALUES ('3f9c2a7e-6d41-4b8a-9c5e-2a1b7d4e8f60'::uuid, NULL, 'update_nextblock.webp', 'images/update_nextblock.webp', 'images/update_nextblock.webp', 'images', 'image/webp', 577246, 'NextBlock CMS release update and platform architecture overview', 2664, 1568)\n ON CONFLICT (object_key) DO UPDATE\n SET description = COALESCE(NULLIF(public.media.description, ''), EXCLUDED.description),\n width = COALESCE(public.media.width, EXCLUDED.width),\n height = COALESCE(public.media.height, EXCLUDED.height),\n updated_at = now();\n\n INSERT INTO public.media (id, uploader_id, file_name, object_key, file_path, folder, file_type, size_bytes, description, width, height)\n VALUES ('8b2e5d1c-9a47-4f3b-8e6d-1c7a2b9d4e50'::uuid, NULL, 'cortex_post.webp', 'images/cortex_post.webp', 'images/cortex_post.webp', 'images', 'image/webp', 745752, 'Diagram of Cortex AI Model Context Protocol MCP connection to external LLMs', 2664, 1568)\n ON CONFLICT (object_key) DO UPDATE\n SET description = COALESCE(NULLIF(public.media.description, ''), EXCLUDED.description),\n width = COALESCE(public.media.width, EXCLUDED.width),\n height = COALESCE(public.media.height, EXCLUDED.height),\n updated_at = now();\n\n SELECT id INTO v_update_media FROM public.media WHERE object_key = 'images/update_nextblock.webp' LIMIT 1;\n SELECT id INTO v_post_media FROM public.media WHERE object_key = 'images/cortex_post.webp' LIMIT 1;\n\n ---------------------------------------------------------------------------\n -- 1. Home page (EN)\n ---------------------------------------------------------------------------\n SELECT id INTO v_home\n FROM public.pages\n WHERE slug = 'home' AND language_id = v_en\n ORDER BY id\n LIMIT 1;\n\n IF v_home IS NOT NULL THEN\n UPDATE public.pages\n SET meta_title = 'NextBlock: AI Website Builder CMS for Production Sites',\n meta_description = 'Build websites fast with the AI website builder CMS that runs on your own AI plan over MCP. Prompt-driven pages land in a production Next.js 16 visual CMS.',\n updated_at = now()\n WHERE id = v_home\n AND (meta_title IS NULL OR meta_title = '' OR meta_title = 'NextBlock: AI Website Builder CMS for Production Sites');\n\n -- 5a. Repair: drop French Commerce Pro product copy that 035's id-keyed\n -- UPDATEs wrote over the Live Demo promo on drifted installs. The real\n -- promo carries the nb-sandbox-promo sentinel and is never matched.\n DELETE FROM public.blocks\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text NOT LIKE '%nb-sandbox-promo%'\n AND (content::text LIKE '%Sous le capot%'\n OR content::text LIKE '%Prêt à lancer votre boutique%'\n OR content::text LIKE '%Moteur E-Commerce d''Entreprise%'\n OR content::text LIKE '%Checkout Stripe Headless%');\n\n -- Hero (order 0): \"Build Blazing-Fast Websites.\" → prompt-to-production H1.\n UPDATE public.blocks\n SET content = $nbhero${\"is_hero\":true,\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#1e293b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h1 class='text-5xl md:text-6xl font-extrabold tracking-tight text-white text-center leading-tight'>Prompt to <span class='relative inline-block mx-1 group'><span class='absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-400 translate-y-1 md:translate-y-2 transform -skew-x-12 rounded-sm shadow-lg group-hover:skew-x-0 transition-transform duration-300 ease-out'></span><span class='relative text-white italic px-1'>Production</span></span>.<br class='md:hidden' /> The AI Website Builder CMS.</h1>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4 leading-relaxed'>NextBlock is the open-source AI website builder CMS for teams that want more than a prototype. Point Claude Code, Cursor, or ChatGPT at your site over MCP and prompt the pages you need. You keep a live Next.js 16 and Supabase website with a visual editor your team owns on day two.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/article/how-to-setup-nextblock\",\"size\":\"lg\",\"text\":\"Get Started\",\"variant\":\"default\",\"position\":\"center\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/article/cortex-ai-mcp-connection-guide\",\"size\":\"lg\",\"text\":\"Connect Claude or Cursor\",\"variant\":\"outline\",\"position\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-wrap justify-center gap-6 text-sm uppercase tracking-wide text-slate-400 mt-8'><a href='https://github.com/nextblock-cms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>GitHub</a><a href='https://x.com/NextBlockCMS' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>X</a><a href='https://www.linkedin.com/in/nextblock/' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>LinkedIn</a><a href='https://dev.to/nextblockcms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>Dev.to</a><a href='https://www.npmjs.com/~nextblockcms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>npm</a></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='p-10 border border-white/10 rounded-3xl bg-white/5 backdrop-blur-xl shadow-2xl relative overflow-hidden group'><div class='absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500'></div><div class='relative z-10'><p class='text-xs text-white uppercase tracking-widest font-semibold mb-2'>Why teams switch</p><p class='text-3xl font-bold text-white mb-2'>Production on day two.</p><p class='text-base text-slate-300 mb-6'>Lovable, Bolt, and v0 hand you prototype code. NextBlock hands you a live site and a CMS.</p><ul class='space-y-3 text-sm text-slate-200'><li><span class='text-blue-400 mr-2'>&#10003;</span> Your own AI subscription over MCP, with no token markup.</li><li><span class='text-blue-400 mr-2'>&#10003;</span> Next.js 16 and Supabase with a Notion-style block editor.</li><li><span class='text-blue-400 mr-2'>&#10003;</span> Blocks stored as JSONB, so publishing never needs a redeploy.</li></ul><div class='mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg'><div class='relative w-full aspect-video'><iframe class='absolute inset-0 h-full w-full border-0' src='https://www.youtube-nocookie.com/embed/71MyfoL4YVM?si=jtlDXV6cSC8rDgz0' title='NextBlock demo video' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' referrerpolicy='strict-origin-when-cross-origin' loading='lazy' allowfullscreen></iframe></div></div></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}$nbhero$::jsonb,\n updated_at = now()\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text LIKE '%Blazing-Fast%';\n\n -- \"Key Features: The Three Pillars\" → why an AI website builder CMS beats prototype code.\n UPDATE public.blocks\n SET content = $nbwhy${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"none\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"text_content\":\"Why an AI Website Builder CMS Beats Prototype Code.\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-lg text-muted-foreground text-center max-w-3xl mx-auto mb-10'>Prompt builders like Lovable, Bolt, and v0 are great at a first draft. Then they hand you code you must host, patch, and rebuild for every copy change. NextBlock takes the same prompts and turns them into a website your whole team can run.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='grid gap-6 md:grid-cols-3'><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>🧩</div><h3 class='text-lg font-bold text-foreground mb-2'>Prompt to CMS, Not Just Code.</h3><p class='text-sm text-muted-foreground leading-relaxed'>External AI agents scaffold real layouts as typed blocks. Editors then tweak copy, images, and sections in a Notion-style editor. Nobody writes a prompt or touches code to change a headline.</p></div><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>🔌</div><h3 class='text-lg font-bold text-foreground mb-2'>Bring Your Own AI Subscription via MCP.</h3><p class='text-sm text-muted-foreground leading-relaxed'>Connect Claude Code, Cursor, ChatGPT, or Gemini straight to /api/mcp. You pay for the AI plan you already have, not a SaaS token markup.</p></div><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>⚡</div><h3 class='text-lg font-bold text-foreground mb-2'>Zero-Redeploy JSONB Architecture.</h3><p class='text-sm text-muted-foreground leading-relaxed'>Generated blocks land in PostgreSQL as validated JSONB. Pages publish in seconds with 100/100 Lighthouse defaults and no rebuild.</p></div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='mt-10 grid gap-4 md:grid-cols-2'><div class='rounded-2xl border border-rose-200 bg-rose-50/60 p-6 dark:border-rose-900/60 dark:bg-rose-950/30'><p class='text-xs uppercase tracking-[0.2em] font-semibold text-rose-600 dark:text-rose-300 mb-2'>Prototype tools</p><p class='text-base font-semibold text-foreground mb-1'>A codebase to babysit.</p><p class='text-sm text-muted-foreground'>Every edit is a new prompt or a pull request. There is no editor, no drafts, and no revision history for the people who own the content.</p></div><div class='rounded-2xl border border-emerald-200 bg-emerald-50/60 p-6 dark:border-emerald-900/60 dark:bg-emerald-950/30'><p class='text-xs uppercase tracking-[0.2em] font-semibold text-emerald-600 dark:text-emerald-300 mb-2'>NextBlock</p><p class='text-base font-semibold text-foreground mb-1'>A website plus a CMS.</p><p class='text-sm text-muted-foreground'>AI drafts the layout once. Your team edits it forever with Live Drafts, revisions, translations, and SEO checks built in.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1}}$nbwhy$::jsonb,\n updated_at = now()\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text LIKE '%Key Features: The Three Pillars%';\n\n -- \"Built with the Best.\" → how the MCP connection works (keeps the stack strip).\n UPDATE public.blocks\n SET content = $nbmcp${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e293b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-cyan-300 font-semibold text-center mb-3'>Model Context Protocol</p><h2 class='text-3xl md:text-4xl font-extrabold text-white text-center mb-4'>How the MCP Connection Works.</h2><p class='text-lg text-slate-300 text-center max-w-3xl mx-auto mb-10'>Cortex AI exposes your CMS as an MCP server at /api/mcp. Any agent that speaks the standard can read your schema and write pages. Three steps take you from a blank site to published pages.</p><div class='grid gap-6 md:grid-cols-3'><div class='rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur'><p class='text-xs uppercase tracking-[0.25em] text-cyan-300 font-semibold mb-3'>Step 1</p><h3 class='text-lg font-bold text-white mb-2'>Register /api/mcp.</h3><p class='text-sm text-slate-300 leading-relaxed'>Mint a scoped token in CMS Settings and paste one JSON block into Claude Code, Cursor, or VS Code. Local dev needs no token at all.</p></div><div class='rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur'><p class='text-xs uppercase tracking-[0.25em] text-cyan-300 font-semibold mb-3'>Step 2</p><h3 class='text-lg font-bold text-white mb-2'>Prompt the layout.</h3><p class='text-sm text-slate-300 leading-relaxed'>Ask for a landing page. The agent calls generate_jsonb_layout and the result lands as a Live Draft made of validated blocks.</p></div><div class='rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur'><p class='text-xs uppercase tracking-[0.25em] text-cyan-300 font-semibold mb-3'>Step 3</p><h3 class='text-lg font-bold text-white mb-2'>Edit and publish.</h3><p class='text-sm text-slate-300 leading-relaxed'>Editors refine the draft in the visual editor and hit publish. The page goes live from PostgreSQL with no redeploy.</p></div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='mt-12 grid gap-8 md:grid-cols-2'><div class='rounded-2xl border border-white/10 bg-slate-950/60 p-6'><h3 class='text-base font-bold text-white mb-4'>For Writers and Editors</h3><ul class='space-y-3 text-sm text-slate-300'><li><strong class='text-white'>Notion-style editor.</strong> Slash commands, drag and drop, and inline AI on every block.</li><li><strong class='text-white'>Live Drafts.</strong> Stage AI output safely, then publish when it reads right.</li><li><strong class='text-white'>Revisions.</strong> Restore any version of a page or post with one click.</li><li><strong class='text-white'>Built-in SEO checks.</strong> Headings, keyphrases, readability, and metadata graded as you type.</li></ul></div><div class='rounded-2xl border border-white/10 bg-slate-950/60 p-6'><h3 class='text-base font-bold text-white mb-4'>For Developers</h3><ul class='space-y-3 text-sm text-slate-300'><li><strong class='text-white'>Next.js 16 core.</strong> Server Components, ISR, and edge caching out of the box.</li><li><strong class='text-white'>Supabase backend.</strong> Postgres, auth, storage, and row-level security you can read.</li><li><strong class='text-white'>Typed block registry.</strong> Every block has a Zod schema, so AI output is validated before it renders.</li><li><strong class='text-white'>Open source.</strong> Self-host on Docker or deploy to Vercel in one click.</li></ul></div></div><p class='mt-10 text-center text-xs uppercase tracking-[0.25em] text-slate-400'>Built with Next.js, React, Supabase, Stripe, Tailwind, Tiptap, Vercel, and Nx.</p>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1}}$nbmcp$::jsonb,\n updated_at = now()\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text LIKE '%Built with the Best%';\n\n -- Cortex AI promo: \"Supercharge Your Content with AI.\" → one engine, two ways to connect.\n UPDATE public.blocks\n SET content = $nbcortexpromo${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4'>Now Available — Cortex AI</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>One AI Engine.<br/>Two Ways to Connect.</h2><p class='text-lg text-slate-300 max-w-2xl leading-relaxed mb-4'>Inside the dashboard, Cortex AI routes to any model through your own OpenRouter key. Outside it, the same tools run as an MCP server for Claude Code, Cursor, ChatGPT, and Gemini.</p><p class='text-base text-slate-400 max-w-2xl leading-relaxed'>Layouts stage as Live Drafts, tokens are scoped read or write, and nothing goes live until an editor publishes it.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/article/cortex-ai-mcp-connection-guide\",\"size\":\"lg\",\"text\":\"Read the MCP Setup Guide →\",\"variant\":\"default\",\"position\":\"left\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"size\":\"lg\",\"text\":\"Get a License\",\"variant\":\"outline\",\"position\":\"left\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-1'><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>29</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>Typed agent tools</p></div><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>MCP</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>Streamable HTTP at /api/mcp</p></div><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>BYOK</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>Your keys, your models</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}$nbcortexpromo$::jsonb,\n updated_at = now()\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text LIKE '%AI Copilot%'\n AND content::text LIKE '%Supercharge%';\n END IF;\n\n -- 5b. Same repair on the FR home page ('accueil'); its copy is otherwise left alone.\n SELECT id INTO v_accueil\n FROM public.pages\n WHERE slug = 'accueil' AND language_id = v_fr\n ORDER BY id\n LIMIT 1;\n\n IF v_accueil IS NOT NULL THEN\n DELETE FROM public.blocks\n WHERE page_id = v_accueil\n AND block_type = 'section'\n AND content::text NOT LIKE '%nb-sandbox-promo%'\n AND (content::text LIKE '%Sous le capot%'\n OR content::text LIKE '%Prêt à lancer votre boutique%'\n OR content::text LIKE '%Moteur E-Commerce d''Entreprise%'\n OR content::text LIKE '%Checkout Stripe Headless%');\n END IF;\n\n -- 5c. Restore the French install guide body (033's copy) where 035 replaced it\n -- with a section-shaped payload the text renderer cannot display.\n SELECT id INTO v_setup_fr\n FROM public.posts\n WHERE slug = 'comment-configurer-nextblock' AND language_id = v_fr\n ORDER BY id\n LIMIT 1;\n\n IF v_setup_fr IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_build_object('html_content', '<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Voici quatre façons de démarrer, <strong>classées de la plus simple à la plus technique</strong> : la première tient en un clic, la dernière donne le code source complet pour celles et ceux qui veulent participer à NextBlock. Elles aboutissent toutes au même endroit — un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur.</p>\r\n\r\n<div class=''mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400''>\r\n <span class=''flex-shrink-0''>Le plus simple</span>\r\n <span class=''h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400''></span>\r\n <span class=''flex-shrink-0''>Le plus de contrôle</span>\r\n</div>\r\n\r\n<div class=''grid gap-5 md:grid-cols-2 my-6''>\r\n <a href=''#one-click-vercel'' class=''block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white''>1</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-blue-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Le plus simple &middot; un clic</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déployer sur Vercel</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un site de production en ligne avec une base de données gérée. Pas de terminal, aucun compte à configurer, rien à copier.</p>\r\n </a>\r\n <a href=''#npm-docker'' class=''block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white''>2</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-amber-500''></span><span class=''h-1.5 w-5 rounded-full bg-amber-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Facile &middot; 100 % local</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Docker</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un seul prompt génère un projet et démarre toute la pile — base de données, auth, stockage, CMS — sur votre machine. Aucun compte cloud.</p>\r\n </a>\r\n <a href=''#npm-cloud'' class=''block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white''>3</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-violet-500''></span><span class=''h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Intermédiaire &middot; votre propre cloud</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock &rarr; Supabase + R2</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Générez une app autonome sur votre propre projet Supabase géré et Cloudflare R2, prête à déployer partout.</p>\r\n </a>\r\n <a href=''#git-clone'' class=''block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <div class=''flex items-center justify-between mb-4''>\r\n <span class=''flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white''>4</span>\r\n <span class=''inline-flex items-center gap-1''><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span><span class=''h-1.5 w-5 rounded-full bg-emerald-500''></span></span>\r\n </div>\r\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Avancé &middot; pour les contributeurs</p>\r\n <h2 class=''mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h2>\r\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Pour celles et ceux qui veulent aider à construire NextBlock (Docker fonctionne ici aussi).</p>\r\n </a>\r\n</div>\r\n\r\n<p class=''text-sm text-slate-500 dark:text-slate-400''>Vous ne savez pas par où commencer ? Descendez la liste — choisissez la première option dont vous avez déjà les prérequis. La plupart des gens devraient commencer par <a href=''#one-click-vercel''>Vercel</a>.</p>\r\n\r\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\r\n <img src=''/images/included.webp'' alt=''Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation'' class=''w-full h-auto object-cover'' />\r\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\r\n</figure>\r\n\r\n<h2 id=''one-click-vercel''>Option 1 : Déploiement Vercel en un clic</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200''>Étape 1 &middot; Le plus simple</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour un site en ligne avec le minimum d''effort — pas de terminal, aucun compte à configurer.</span></p>\r\n<p>Le moyen le plus rapide d''obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\r\n<ol class=''space-y-2''>\r\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\r\n <li><strong>Nommez le dépôt — et rendez-le Public.</strong> À la première étape sur Vercel, vous choisissez le nom du dépôt ; réglez sa visibilité sur <strong>Public</strong> plutôt que Privé. Un dépôt public est ce qui débloque les mises à jour automatiques en un clic plus tard — c''est donc le choix recommandé.</li>\r\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\r\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l''assistant de configuration.</li>\r\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\r\n</ol>\r\n<div class=''my-8''>\r\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel &rarr;</a>\r\n</div>\r\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Recommandé &middot; rendez le dépôt public</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Quand Vercel vous demande de nommer le nouveau dépôt, choisissez <strong>Public</strong>. C''est gratuit, et c''est ce qui permet à l''étape <strong>Connect GitHub</strong> du tableau de bord d''installer un workflow quotidien qui garde votre site synchronisé avec la dernière version de NextBlock. Vous pourrez toujours passer un dépôt privé en public plus tard dans GitHub — mais démarrer en public est le chemin le plus simple vers les mises à jour automatiques.</p>\r\n</div>\r\n<div class=''rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Zéro configuration</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Aucune variable d''environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s''exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\r\n</div>\r\n\r\n<h2 id=''npm-docker''>Option 2 : npm create nextblock &rarr; Docker (100 % local)</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200''>Étape 2 &middot; Facile</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour tout essayer sur votre machine sans aucun compte cloud. Nécessite Docker Desktop.</span></p>\r\n<p>Faites tourner NextBlock entièrement sur votre machine, sans aucun compte cloud. Le CLI génère une application Next.js autonome puis démarre toute la pile dans Docker pour vous : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\r\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a> (npm inclus) et <a href=''https://www.docker.com/products/docker-desktop/'' target=''_blank'' rel=''noopener''>Docker Desktop</a>, et assurez-vous que Docker Desktop est démarré.</p>\r\n<pre><code>npm create nextblock@latest mon-site</code></pre>\r\n<ol class=''space-y-2''>\r\n <li><strong>Choisissez le profil d''hébergement.</strong> Au premier prompt, choisissez <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, puis confirmez que Docker Desktop est installé et démarré.</li>\r\n <li><strong>Laissez faire.</strong> Le CLI copie le template, installe les dépendances, génère des clés sécurisées et démarre toute la pile avec une seule commande Docker — sans aucune question. Le premier lancement télécharge les images et construit l''app, comptez donc quelques minutes ; chaque migration de base de données est appliquée automatiquement au démarrage de la pile.</li>\r\n <li><strong>Ouvrez <code>http://localhost:3000</code></strong> — vous êtes redirigé vers l''assistant de configuration. Comme la base de données et le stockage MinIO sont déjà connectés, les étapes de connexion et de stockage sont ignorées : il ne reste qu''à créer votre administrateur (confirmé instantanément, sans email). Vous arrivez dans le tableau de bord du CMS.</li>\r\n</ol>\r\n<div class=''rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Commandes du quotidien</p>\r\n <pre class=''mt-4 mb-0''><code># reconstruire et redémarrer la pile\r\nnpm run docker:up\r\n\r\n# arrêter la pile (vos données persistent dans les volumes Docker)\r\nnpm run docker:down\r\n\r\n# suivre les logs de l''application\r\nnpm run docker:logs</code></pre>\r\n <p class=''mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200''>Le mode Docker n''est proposé que dans le prompt interactif — ne passez pas <code>--yes</code>, qui force le mode cloud géré ci-dessous.</p>\r\n</div>\r\n\r\n<h2 id=''npm-cloud''>Option 3 : npm create nextblock &rarr; votre propre Supabase + Cloudflare</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200''>Étape 3 &middot; Intermédiaire</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour construire votre propre site sur du cloud géré. Nécessite un projet Supabase et un bucket R2.</span></p>\r\n<p>Le meilleur point de départ pour construire votre propre site sur du cloud géré. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — reliée à un projet Supabase et un bucket Cloudflare R2 que vous contrôlez, et elle se déploie partout où Next.js tourne.</p>\r\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a>, créez un projet gratuit sur <a href=''https://supabase.com'' target=''_blank'' rel=''noopener''>supabase.com</a>, et configurez un bucket <a href=''https://developers.cloudflare.com/r2/'' target=''_blank'' rel=''noopener''>Cloudflare R2</a> pour vos images et fichiers.</p>\r\n<pre><code>npm create nextblock@latest mon-site\r\ncd mon-site\r\nnpm run dev</code></pre>\r\n<p>Au premier prompt, choisissez <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> et nommez votre projet. Ouvrez ensuite <code>http://localhost:3000/setup</code> et laissez l''assistant faire le travail :</p>\r\n<ol class=''space-y-2''>\r\n <li><strong>Connectez Supabase</strong> — collez l''URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d''accès personnel pour que l''assistant applique le schéma de base de données à votre place.</li>\r\n <li><strong>Ajoutez Cloudflare R2</strong> — saisissez votre identifiant de compte R2, le nom du bucket, la clé d''accès (access key ID), la clé secrète et l''URL publique du bucket pour servir vos images et fichiers.</li>\r\n <li><strong>Créez votre administrateur</strong> — l''assistant applique toutes les migrations, génère les secrets de l''application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l''application.</li>\r\n</ol>\r\n<div class=''rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Modules premium</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Besoin d''une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l''êtes : <code>npx create-nextblock activate ecommerce</code></p>\r\n</div>\r\n\r\n<h2 id=''git-clone''>Option 4 : Cloner le dépôt</h2>\r\n<p class=''mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm''><span class=''inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200''>Étape 4 &middot; Avancé</span><span class=''text-slate-500 dark:text-slate-400''>Idéal pour les contributeurs et les équipes qui veulent personnaliser la plateforme. Nécessite Node.js et git.</span></p>\r\n<p>Faites tourner le monorepo Nx complet : l''application CMS, tous les packages partagés, le code du CLI et la documentation. C''est le chemin de celles et ceux qui veulent participer au projet — contributeurs, auteurs de plugins et équipes qui personnalisent la plateforme elle-même.</p>\r\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\r\ncd nextblock\r\nnpm install\r\nnpx nx serve nextblock</code></pre>\r\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l''API de management Supabase — sans CLI Supabase.</p>\r\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\r\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Vous préférez rester local ? Docker fonctionne ici aussi</p>\r\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Pas besoin de comptes cloud pour développer sur le monorepo. Avec Docker Desktop démarré, une seule commande lance la même pile auto-hébergée que l''option 2 — Postgres, auth, stockage et le CMS — directement depuis votre clone :</p>\r\n <pre class=''mt-4 mb-0''><code>npm run docker:setup</code></pre>\r\n <p class=''mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200''>Quand c''est terminé, ouvrez <code>http://localhost:3000</code> et créez votre administrateur. Notez que le monorepo n''a pas de <code>npm run dev</code> — utilisez <code>npx nx serve nextblock</code> (port 4200) pour le chemin cloud.</p>\r\n</div>\r\n\r\n<h2 id=''after-install''>Après l''installation : vos 10 premières minutes</h2>\r\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\r\n<ul class=''space-y-2''>\r\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\r\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\r\n <li><strong>Configurez l''email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\r\n <li><strong>Extras optionnels</strong> — connectez vos outils d''analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\r\n</ul>\r\n<p>Ensuite, découvrez comment la plateforme s''articule dans <a href=''/article/comment-nextblock-fonctionne''>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href=''/article/guide-commerce-nextblock''>guide Commerce</a>.</p>\r\n\r\n<h2 id=''faq''>FAQ d''installation</h2>\r\n<h3>Que dois-je installer ?</h3>\r\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> en mode Docker : <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20+</a> et Docker Desktop. Pour le mode cloud géré : Node.js 20+ ainsi qu''un projet Supabase et un bucket Cloudflare R2. Pour le dépôt cloné : Node.js 20+ et git (ajoutez Docker Desktop si vous voulez faire tourner la pile locale).</p>\r\n<h3>NextBlock est-il gratuit ?</h3>\r\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l''e-commerce et Cortex AI sont optionnels et s''activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\r\n<h3>Ai-je besoin d''un compte Supabase ?</h3>\r\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour le chemin cloud géré <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker — via le mode Docker du CLI ou <code>npm run docker:setup</code> dans le clone — aucun compte cloud n''est nécessaire.</p>\r\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\r\n<p>Non. L''assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l''opération est toujours sans risque.</p>\r\n<h3>Puis-je changer de chemin plus tard ?</h3>\r\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd''hui et déployer sur Vercel demain. NextBlock se déploie comme n''importe quelle app Next.js.</p>\r\n<h3>Comment mettre à jour NextBlock ?</h3>\r\n<p>Sur Vercel, l''étape Connect GitHub de la checklist active une synchronisation quotidienne automatique (cela nécessite un dépôt public). Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s''appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\r\n\r\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5''>\r\n <p class=''mt-0 text-2xl font-semibold text-slate-900 dark:text-white''>Prêt à vous lancer ?</p>\r\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\r\n <div class=''mt-5 flex flex-wrap justify-center gap-3''>\r\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel</a>\r\n <a href=''https://github.com/nextblock-cms/nextblock'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50''>Voir sur GitHub</a>\r\n </div>\r\n</div>'::text),\n updated_at = now()\n WHERE post_id = v_setup_fr\n AND block_type = 'text'\n AND \"order\" = 0\n AND NOT (content ? 'html_content')\n AND content::text LIKE '%Checkout Stripe Headless%';\n END IF;\n\n ---------------------------------------------------------------------------\n -- 2. Cortex AI product (EN)\n ---------------------------------------------------------------------------\n SELECT id INTO v_cortex\n FROM public.products\n WHERE slug = 'nextblock-cortex-ai-cortex-ai-license' AND language_id = v_en\n LIMIT 1;\n\n IF v_cortex IS NOT NULL THEN\n UPDATE public.products\n SET title = 'NextBlock™ Cortex AI MCP Server & Copilot License',\n updated_at = now()\n WHERE id = v_cortex\n AND title IN ('NextBlock™ Cortex AI - Cortex AI License', 'NextBlock™ Cortex AI MCP Server & Copilot License');\n\n UPDATE public.products\n SET meta_title = 'Cortex AI MCP Server — Connect Claude & Cursor to Your CMS',\n meta_description = 'Turn NextBlock into a Cortex AI MCP server. Connect Claude, Cursor, and ChatGPT to create layouts, inspect your schema, and manage content on your AI plan.',\n updated_at = now()\n WHERE id = v_cortex\n AND (meta_title IS NULL OR meta_title = ''\n OR meta_title = 'NextBlock Cortex AI License | Native AI Content Engine'\n OR meta_title = 'Cortex AI MCP Server — Connect Claude & Cursor to Your CMS');\n\n UPDATE public.products\n SET short_description = 'NextBlock™ Cortex AI is the AI layer for your CMS. Route any model through your own OpenRouter key in the dashboard, or register /api/mcp and let Claude Code, Cursor, and ChatGPT build layouts, inspect your schema, and manage content on the AI subscription you already pay for.',\n updated_at = now()\n WHERE id = v_cortex\n AND (short_description IS NULL OR short_description = ''\n OR short_description LIKE 'NextBlock™ Cortex AI License brings block-level machine intelligence%'\n OR short_description = 'NextBlock™ Cortex AI is the AI layer for your CMS. Route any model through your own OpenRouter key in the dashboard, or register /api/mcp and let Claude Code, Cursor, and ChatGPT build layouts, inspect your schema, and manage content on the AI subscription you already pay for.');\n\n -- The seeded description opens with the \"AI Intelligence Layer\" eyebrow (both\n -- the 035 and the sandbox-reset variants). Replace the whole section set once;\n -- the new copy uses a different eyebrow, so a re-run is a no-op.\n IF EXISTS (\n SELECT 1 FROM public.blocks\n WHERE product_id = v_cortex\n AND content::text LIKE '%AI Intelligence Layer%'\n ) THEN\n DELETE FROM public.blocks WHERE product_id = v_cortex;\n\n INSERT INTO public.blocks (product_id, language_id, block_type, content, \"order\") VALUES\n (v_cortex, v_en, 'section', $nbcx0${\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#312e81\",\"position\":35},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4'>MCP-Native AI Layer</p><h2 class='text-3xl md:text-5xl font-extrabold text-white leading-tight mb-5'>Your CMS as a Cortex AI MCP Server.</h2><p class='text-base md:text-lg text-slate-200 leading-relaxed mb-6'>Cortex AI runs two ways. In the dashboard it routes to any model through your own OpenRouter key. Over MCP it turns NextBlock into a server that Claude Code, Cursor, and ChatGPT operate from their own chat window.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Read the MCP Setup Guide →\",\"url\":\"/article/cortex-ai-mcp-connection-guide\",\"variant\":\"default\",\"size\":\"lg\",\"position\":\"left\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8'><h3 class='text-lg font-bold text-white mb-5'>Bring Your Own AI Subscription</h3><ul class='space-y-4 text-sm leading-relaxed text-slate-300'><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>No token markup</strong> — use Claude Pro, ChatGPT Plus, Cursor, or Gemini Advanced by registering /api/mcp. You pay your provider, not us.</span></li><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>Dashboard BYOK</strong> — OpenRouter routing with your own key, so you pick the model and keep the bill.</span></li><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>Scoped tokens</strong> — mint read-only or write tokens in CMS Settings and revoke them any time.</span></li><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>Live Drafts</strong> — generated layouts stage as drafts. Nothing goes live until an editor publishes it.</span></li></ul></div>\"}}]]}$nbcx0$::jsonb, 0),\n (v_cortex, v_en, 'section', $nbcx1${\"container_type\":\"container\",\"background\":{\"type\":\"theme\",\"theme\":\"muted\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":4},\"column_gap\":\"lg\",\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"vertical_alignment\":\"center\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>5</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>MCP contract tools</span></p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>29</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>Typed agent tools</span></p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>0 %</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>Token markup</span></p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>2</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>Ways to connect</span></p>\"}}]]}$nbcx1$::jsonb, 1),\n (v_cortex, v_en, 'section', $nbcx2${\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"vertical_alignment\":\"stretch\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>get_database_schema</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Returns every table the agent may read or change, with columns, keys, and read-only flags. The model plans against real structure, not guesses.</p></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>query_site_analytics</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Reads revenue, order counts, status breakdowns, and top products over a date range. Read-only, so it is safe on any token.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>generate_jsonb_layout</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Turns a prompt into a complete page layout. Blocks are validated against the NextBlock schema and staged as a Live Draft.</p></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>search_stock_media</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Finds free stock photos on Pexels and Unsplash with alt text and credits. Drop a result straight into an image block.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>update_site_navigation</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Adds, renames, or reorders header menu items per locale. Append to keep the current menu or replace it in one call.</p></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>and 24 more</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Create posts and products, translate pages, upload media, manage themes and scripts. Every tool is typed and scoped.</p></div>\"}}]]}$nbcx2$::jsonb, 2),\n (v_cortex, v_en, 'section', $nbcx3${\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"180deg\",\"stops\":[{\"color\":\"#020617\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4'>How It Works</p><h3 class='text-2xl md:text-3xl font-extrabold text-white mb-4'>One Registry, Standard Transport.</h3><p class='text-slate-300 leading-relaxed mb-5'>The MCP server speaks Streamable HTTP at /api/mcp. Your client posts JSON-RPC messages and gets typed results back. There is no SDK to install and no proxy in the middle.</p><ul class='space-y-3 text-sm text-slate-400'><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Bearer tokens are stored as SHA-256 hashes and shown once.</span></li><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Localhost trust lets a dev server skip the token while you build.</span></li><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Read-only tokens never see a mutating tool in the list.</span></li><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Copy-paste config for Claude Code, Cursor, VS Code, and Claude Desktop.</span></li></ul>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='space-y-4'><div class='p-5 rounded-xl border border-slate-700 bg-slate-900'><h4 class='text-sm font-bold text-white mb-1'>Dashboard BYOK via OpenRouter</h4><p class='text-xs text-slate-400 leading-relaxed'>Pick any model, from Claude to Gemini to open weights, with one key. Costs stay on your OpenRouter bill and switch with a toggle.</p></div><div class='p-5 rounded-xl border border-slate-700 bg-slate-900'><h4 class='text-sm font-bold text-white mb-1'>Editor Copilot</h4><p class='text-xs text-slate-400 leading-relaxed'>An inline toolbar rewrites copy, refactors columns, and translates whole pages. Output is valid block data, so layouts never break.</p></div><div class='p-5 rounded-xl border border-slate-700 bg-slate-900'><h4 class='text-sm font-bold text-white mb-1'>Privacy-First Design</h4><p class='text-xs text-slate-400 leading-relaxed'>Requests go straight to your provider with your key. NextBlock never stores, logs, or trains on your content.</p></div></div>\"}}]]}$nbcx3$::jsonb, 3),\n (v_cortex, v_en, 'section', $nbcx4${\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"none\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"vertical_alignment\":\"center\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Ready to connect your AI to your CMS?\",\"textAlign\":\"center\",\"textColor\":\"background\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6'>One license unlocks the dashboard copilot and the MCP server. Bring your own key or your own subscription, and keep your data yours.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Purchase Cortex AI\",\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"variant\":\"secondary\",\"size\":\"lg\",\"position\":\"center\"}}]]}$nbcx4$::jsonb, 4);\n END IF;\n END IF;\n\n ---------------------------------------------------------------------------\n -- 3. Feature image for the updating guide (EN + FR twins)\n ---------------------------------------------------------------------------\n IF v_update_media IS NOT NULL THEN\n UPDATE public.posts\n SET feature_image_id = v_update_media,\n updated_at = now()\n WHERE slug IN ('how-updating-works', 'comment-fonctionnent-les-mises-a-jour')\n AND (feature_image_id IS NULL\n OR feature_image_id = '641ddf75-5c90-41df-8b83-e7c298f30a6a'::uuid -- seeded extensibility.webp\n OR feature_image_id = v_update_media);\n END IF;\n\n ---------------------------------------------------------------------------\n -- 4. New post: Connect Claude to NextBlock CMS via MCP\n ---------------------------------------------------------------------------\n INSERT INTO public.posts (\n language_id, author_id, title, slug, label, excerpt, subtitle, status,\n published_at, meta_title, meta_description, feature_image_id, version,\n translation_group_id\n )\n SELECT\n v_en, NULL,\n 'Connect Claude to NextBlock CMS via MCP, from Cursor or Claude Code',\n 'cortex-ai-mcp-connection-guide',\n 'Cortex AI',\n 'Register /api/mcp in Claude Code or Cursor and let your own AI subscription read the schema, draft layouts, and manage pages in NextBlock.',\n 'A hands-on guide to the Cortex AI MCP server: what it fixes, which tools it exposes, and the exact config for localhost and production.',\n 'published', now(),\n 'Connect Claude to NextBlock CMS via MCP | Cursor Guide',\n 'Learn how to connect Claude to NextBlock CMS with Model Context Protocol. Register /api/mcp in Claude Code or Cursor to inspect schemas and build layouts.',\n v_post_media, 1, 'a7c4e2d1-3b58-4f96-9e0a-6d2c8b1f5a73'::uuid\n WHERE NOT EXISTS (\n SELECT 1 FROM public.posts WHERE language_id = v_en AND slug = 'cortex-ai-mcp-connection-guide'\n );\n\n SELECT id INTO v_post\n FROM public.posts\n WHERE language_id = v_en AND slug = 'cortex-ai-mcp-connection-guide'\n ORDER BY id\n LIMIT 1;\n\n IF v_post IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks WHERE post_id = v_post\n ) THEN\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n VALUES (v_post, v_en, 'text', $nbmcpguide${\"html_content\":\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"This guide shows you how to connect Claude to NextBlock CMS with an open standard instead of a vendor plugin. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Cortex AI turns your site into a Model Context Protocol server. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Claude Code, Cursor, ChatGPT, and Gemini can then read your database schema, draft page layouts, and update navigation from their own chat window. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"You pay for the AI subscription you already have, with no token markup in between.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Every prompt becomes a structured database record rather than a pile of generated code. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Your editors keep a visual CMS, your developers keep a clean Next.js 16 app, and nothing goes live until someone hits publish.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The Problem with Traditional AI Web Builders\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Prompt builders such as Lovable, Bolt, and v0 are impressive on day one. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"You describe a page and get a working React app in minutes. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The trouble starts on day two.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The output is disposable code with no content layer. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Every headline change is another prompt or another pull request. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"There are no drafts, no revisions, no translations, and no SEO checks for the people who own the words. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Marketing teams end up waiting on engineers, and engineers end up babysitting code nobody designed.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"NextBlock takes the opposite view. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"AI should scaffold the site once, and a real CMS should run it forever. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The MCP connection is how those two worlds meet.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"How Cortex AI Uses Model Context Protocol (MCP)\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Model Context Protocol is an open standard for giving AI agents tools. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"A client such as Claude Code lists the tools a server offers, calls them with typed arguments, and reads typed results back. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Cortex AI ships that server inside your NextBlock install at \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"/api/mcp\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The endpoint speaks Streamable HTTP. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Your client posts JSON-RPC messages over a normal HTTPS request, and the server answers in the same response. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"There is no long-lived SSE stream to babysit and no SDK to install on the server side. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The same 29 typed tools that power the dashboard copilot are exposed over the wire, so the agent in your editor and the agent in your CMS never drift apart.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Two safety rules apply to every call. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Layout tools stage their output as a Live Draft, so nothing reaches visitors until an editor publishes it. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And every token carries a scope, so a read-only token never even sees a tool that could change data.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Available MCP Database Tools\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Five tool names form the public MCP contract. Each one forwards to a tested Cortex AI executor.\\\"}]},{\\\"type\\\":\\\"bulletList\\\",\\\"content\\\":[{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"get_database_schema\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" returns every table the agent may read or change, with columns, primary keys, and read-only flags.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"generate_jsonb_layout\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" turns a prompt into a full page layout, validates each block against the NextBlock schema, and stages it as a Live Draft.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"update_site_navigation\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" adds, renames, or reorders header menu items per locale.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"query_site_analytics\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" reads revenue, order counts, status breakdowns, and top products over a date range.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"search_stock_media\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" finds free stock photos with alt text and photographer credits ready for an image block.\\\"}]}]}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Beyond the contract, the registry includes tools to create posts and products, translate pages, upload media, and manage themes and scripts. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Three resources expose the database, block, and custom block schemas, and three prompts cover page building, cloning from a URL, and translation.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Step by Step: Connect Claude to NextBlock CMS in Cursor and Claude Code\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The server is off by default because it is a remote write surface onto live content. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Turning it on takes three steps.\\\"}]},{\\\"type\\\":\\\"orderedList\\\",\\\"attrs\\\":{\\\"start\\\":1},\\\"content\\\":[{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Open \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"CMS Settings → Cortex AI\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and enable the MCP server access card.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Mint a token with the scope you need. Read-only is enough for planning and audits.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Copy the generated config for your client. The card renders one snippet for Claude Code, Cursor, VS Code, and Claude Desktop.\\\"}]}]}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Claude Code needs \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"\\\\\\\"type\\\\\\\": \\\\\\\"http\\\\\\\"\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" in the entry, or it skips the server without a warning. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Cursor infers the transport from the URL and needs no type field. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"VS Code uses a top-level \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"servers\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" key and prompts for the token instead of storing it.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Localhost Configuration Without a Token\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"While you build, turn on \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Trust localhost without a token\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" in the same settings card. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"A dev server on your machine then accepts loopback calls with no header at all. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Standalone installs run on port 3000, and the monorepo dev server runs on port 4200.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Add this block to \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".mcp.json\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" in your project root for Claude Code:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"json\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"{\\\\n \\\\\\\"mcpServers\\\\\\\": {\\\\n \\\\\\\"nextblock\\\\\\\": {\\\\n \\\\\\\"type\\\\\\\": \\\\\\\"http\\\\\\\",\\\\n \\\\\\\"url\\\\\\\": \\\\\\\"http://localhost:3000/api/mcp\\\\\\\"\\\\n }\\\\n }\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The CLI form does the same thing in one line: \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"claude mcp add --transport http nextblock http://localhost:3000/api/mcp\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Localhost trust is a development affordance only. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"It is ignored in production builds, because a proxy can spoof the host header.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Production Authentication via Bearer Tokens\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"In production every client sends a bearer token. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Tokens start with \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"nbmcp_\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", are shown once at mint time, and are stored only as SHA-256 hashes. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Revoking one is a single click, and the same value can never be minted again.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Add this to \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".cursor/mcp.json\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" for Cursor, or drop the same entry into \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".mcp.json\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" with a type field for Claude Code:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"json\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"{\\\\n \\\\\\\"mcpServers\\\\\\\": {\\\\n \\\\\\\"nextblock\\\\\\\": {\\\\n \\\\\\\"url\\\\\\\": \\\\\\\"https://your-site.com/api/mcp\\\\\\\",\\\\n \\\\\\\"headers\\\\\\\": { \\\\\\\"Authorization\\\\\\\": \\\\\\\"Bearer nbmcp_your_token\\\\\\\" }\\\\n }\\\\n }\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Once the client connects, ask for something concrete. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Try \\\\\\\"inspect the database schema and draft a pricing page with three tiers.\\\\\\\" \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The agent calls the schema tool, then the layout tool, and your CMS shows a new Live Draft ready for review.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Zero-Redeploy Production Rendering\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The reason this works is where the output lands. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Cortex AI writes strict JSONB block records into PostgreSQL, not HTML strings and not source files. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Each block has a Zod schema, so a bad field is rejected before it is stored.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"At request time Next.js 16 renders those records with Server Components and caches the result at the edge. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"There is no build step between publish and live, and no HTML sanitizer tax on every render. \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The 100/100 Lighthouse defaults you get on an empty site are the same ones you get after the agent has drafted fifty pages.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Connect your agent today with the \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Cortex AI license\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":null,\\\"class\\\":null}}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and read \\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"how NextBlock works under the hood\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"/article/how-nextblock-works\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null}}]},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" if you want the full picture of the block registry that makes it safe.\\\"}]}]}\"}$nbmcpguide$::jsonb, 0);\n END IF;\nEND\n$body$;\n\n-- Revision baseline for the new post, mirroring 00000000000016 section 3b (and\n-- 00000000000020's trailer): 016 back-filled a 'snapshot' revision only for posts\n-- that existed when it ran, so a post seeded later needs its own restore point.\nINSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)\nSELECT\n po.id,\n NULL::uuid,\n po.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', po.title,\n 'slug', po.slug,\n 'language_id', po.language_id,\n 'status', po.status,\n 'meta_title', po.meta_title,\n 'meta_description', po.meta_description,\n 'custom_canonical', po.custom_canonical,\n 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', po.feature_image_id,\n 'label', po.label,\n 'excerpt', po.excerpt,\n 'subtitle', po.subtitle\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.post_id = po.id\n ), '[]'::jsonb)\n )\n FROM public.posts po\n WHERE po.slug = 'cortex-ai-mcp-connection-guide'\n AND NOT EXISTS (\n SELECT 1 FROM public.post_revisions r\n WHERE r.post_id = po.id\n AND r.revision_type = 'snapshot'\n AND r.version <= po.version\n )\nON CONFLICT (post_id, version) DO NOTHING;\n"
206
+ },
207
+ {
208
+ "version": "00000000000038",
209
+ "name": "00000000000038_mcp_guide_visual_refresh.sql",
210
+ "sql": "-- 00000000000038_mcp_guide_visual_refresh.sql\n--\n-- Visual refresh of the 'cortex-ai-mcp-connection-guide' post that 00000000000037\n-- seeded. 037 stored the body as a plain Tiptap JSON document, which renders as a\n-- wall of prose; this replaces it with the styled HTML every other seeded article\n-- uses — summary cards, a prototype-vs-production comparison, a card per MCP tool\n-- with its scope, numbered setup steps, terminal-style code panels, callouts, stat\n-- tiles and a closing CTA — and adds a rendered architecture diagram\n-- (public/images/mcp-prompt-to-production.webp, 2000x1025) as a captioned figure.\n--\n-- It is a separate file because 037 was already applied (and recorded) on the\n-- sandbox before the redesign; Supabase matches history by version, so editing\n-- 037 in place would have been skipped there silently.\n--\n-- Idempotent: the media row upserts on media.object_key; the body UPDATE is guarded\n-- on the seeded Tiptap JSON shape (starts with the doc node and carries 037's\n-- opening sentence), so once replaced — or once an editor has rewritten the post —\n-- it no longer matches. The SEO grade stays 100/100 with the focus keyphrase\n-- \"connect Claude to NextBlock CMS\" (verified with libs/utils/src/lib/seo).\n--\n-- After adding this file: npm run generate:migrations-bundle && npm run\n-- generate:sandbox && npm run sync:create-nextblock.\n\nDO $body$\nDECLARE\n v_en integer;\n v_post integer;\nBEGIN\n SELECT id INTO v_en FROM public.languages WHERE code = 'en' LIMIT 1;\n\n ---------------------------------------------------------------------------\n -- 1. Media row for the inline diagram. Registered in resolveMediaUrl's bundled\n -- keys and the text renderer's known-image map, so it is served from\n -- public/images and optimised through next/image.\n ---------------------------------------------------------------------------\n INSERT INTO public.media (id, uploader_id, file_name, object_key, file_path, folder, file_type, size_bytes, description, width, height)\n VALUES ('5c7d9e2b-1a64-4f8c-9b3e-7d2a4c6e8f10'::uuid, NULL, 'mcp-prompt-to-production.webp', 'images/mcp-prompt-to-production.webp', 'images/mcp-prompt-to-production.webp', 'images', 'image/webp', 109680, 'Diagram of the prompt-to-production flow: an AI client calls the Cortex AI MCP server at /api/mcp, which writes validated JSONB blocks to PostgreSQL that Next.js 16 publishes without a redeploy', 2000, 1025)\n ON CONFLICT (object_key) DO UPDATE\n SET description = COALESCE(NULLIF(public.media.description, ''), EXCLUDED.description),\n width = COALESCE(public.media.width, EXCLUDED.width),\n height = COALESCE(public.media.height, EXCLUDED.height),\n updated_at = now();\n\n ---------------------------------------------------------------------------\n -- 2. Replace the seeded Tiptap JSON body with the styled HTML version.\n ---------------------------------------------------------------------------\n SELECT id INTO v_post\n FROM public.posts\n WHERE language_id = v_en AND slug = 'cortex-ai-mcp-connection-guide'\n ORDER BY id\n LIMIT 1;\n\n IF v_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = $nbmcpguide2${\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>This guide shows you how to connect Claude to NextBlock CMS with an open standard instead of a vendor plugin. Cortex AI turns your site into a Model Context Protocol server. Claude Code, Cursor, ChatGPT, and Gemini can then read your database schema, draft page layouts, and update navigation from their own chat window. You pay for the AI subscription you already have, with no token markup in between.</p><div class='grid gap-4 md:grid-cols-3 my-10'><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200'>One config block</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Setup in minutes.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Paste one JSON entry into Claude Code, Cursor, or VS Code. The CMS settings card writes it for you.</p></div><div class='rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200'>Your subscription</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>No token markup.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Claude Pro, ChatGPT Plus, Cursor, or Gemini Advanced do the thinking. NextBlock never resells tokens.</p></div><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Live Drafts</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Nothing ships by accident.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Every layout the agent writes lands as a draft. An editor reviews it and hits publish.</p></div></div><p>Every prompt becomes a structured database record rather than a pile of generated code. Your editors keep a visual CMS, your developers keep a clean Next.js 16 app, and nothing goes live until someone hits publish.</p><h2>The Problem with Traditional AI Web Builders</h2><p>Prompt builders such as Lovable, Bolt, and v0 are impressive on day one. You describe a page and get a working React app in minutes. The trouble starts on day two.</p><div class='grid gap-4 md:grid-cols-2 my-8'><div class='rounded-3xl border border-rose-200/70 bg-rose-50/70 p-6 dark:border-rose-500/20 dark:bg-rose-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-rose-700 dark:text-rose-200'>Prototype tools</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>A codebase to babysit.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>The output is disposable code with no content layer. Every headline change is another prompt or another pull request. There are no drafts, no revisions, no translations, and no SEO checks for the people who own the words.</p></div><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>NextBlock</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>A website plus a CMS.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>AI scaffolds the site once, and a real CMS runs it forever. Marketing edits copy in a Notion-style editor. Engineers keep a codebase nobody has to babysit.</p></div></div><p>NextBlock takes the opposite view from the prototype tools. AI should draft the site, and people should own it. The MCP connection is how those two worlds meet.</p><h2>How Cortex AI Uses Model Context Protocol (MCP)</h2><p>Model Context Protocol is an open standard for giving AI agents tools. A client such as Claude Code lists the tools a server offers, calls them with typed arguments, and reads typed results back. Cortex AI ships that server inside your NextBlock install at <code>/api/mcp</code>.</p><figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'><img src='/images/mcp-prompt-to-production.webp' alt='Diagram of the prompt-to-production flow: an AI client calls the Cortex AI MCP server at /api/mcp, which writes validated JSONB blocks to PostgreSQL that Next.js 16 publishes without a redeploy' class='w-full h-auto object-cover' /><figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>One prompt travels from your AI client, through the MCP server, into PostgreSQL as validated blocks, and out as a published page. No build step sits in between.</figcaption></figure><p>The endpoint speaks Streamable HTTP. Your client posts JSON-RPC messages over a normal HTTPS request, and the server answers in the same response. There is no long-lived SSE stream to babysit and no SDK to install on the server side. The same 29 typed tools that power the dashboard copilot are exposed over the wire, so the agent in your editor and the agent in your CMS never drift apart.</p><div class='grid gap-4 md:grid-cols-2 my-8'><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Safety rule one</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Layouts stage as Live Drafts.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Layout tools write drafts, so nothing reaches visitors until an editor publishes it.</p></div><div class='rounded-3xl border border-blue-200/70 bg-blue-50/70 p-6 dark:border-blue-500/20 dark:bg-blue-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Safety rule two</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Every token carries a scope.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>A read-only token never even sees a tool that could change data. Mutating tools are absent from its list.</p></div></div><h3>Available MCP Database Tools</h3><p>Five tool names form the public MCP contract. Each one forwards to a tested Cortex AI executor.</p><div class='grid gap-4 md:grid-cols-2 my-8'><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>get_database_schema</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-emerald-300 text-emerald-700 dark:border-emerald-500/40 dark:text-emerald-200'>read</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Returns every table the agent may read or change, with columns, primary keys, and read-only flags.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>generate_jsonb_layout</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-amber-300 text-amber-700 dark:border-amber-500/40 dark:text-amber-200'>write</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Turns a prompt into a full page layout, validates each block against the NextBlock schema, and stages it as a Live Draft.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>update_site_navigation</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-amber-300 text-amber-700 dark:border-amber-500/40 dark:text-amber-200'>write</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Adds, renames, or reorders header menu items per locale.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>query_site_analytics</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-emerald-300 text-emerald-700 dark:border-emerald-500/40 dark:text-emerald-200'>read</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Reads revenue, order counts, status breakdowns, and top products over a date range.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>search_stock_media</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-emerald-300 text-emerald-700 dark:border-emerald-500/40 dark:text-emerald-200'>read</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Finds free stock photos with alt text and photographer credits ready for an image block.</p></div><div class='rounded-3xl border border-dashed border-slate-300 p-5 dark:border-white/20'><p class='mt-0 mb-0 text-sm font-semibold text-slate-900 dark:text-white'>And 24 more.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>Create posts and products, translate pages, upload media, manage themes and scripts. Three resources expose the database, block, and custom block schemas.</p></div></div><h2>Step by Step: Connect Claude to NextBlock CMS in Cursor and Claude Code</h2><p>The server is off by default because it is a remote write surface onto live content. Turning it on takes three steps.</p><div class='grid gap-4 md:grid-cols-3 my-8'><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 font-mono text-3xl font-bold text-sky-500 dark:text-sky-300'>01</p><p class='mt-3 mb-0 text-lg font-semibold text-slate-900 dark:text-white'>Enable the server.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>Open <strong>CMS Settings → Cortex AI</strong> and switch on the MCP server access card.</p></div><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 font-mono text-3xl font-bold text-violet-500 dark:text-violet-300'>02</p><p class='mt-3 mb-0 text-lg font-semibold text-slate-900 dark:text-white'>Mint a token.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>Pick the scope you need. Read-only is enough for planning and audits.</p></div><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 font-mono text-3xl font-bold text-emerald-500 dark:text-emerald-300'>03</p><p class='mt-3 mb-0 text-lg font-semibold text-slate-900 dark:text-white'>Copy the config.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>The card renders one snippet each for Claude Code, Cursor, VS Code, and Claude Desktop.</p></div></div><p>Claude Code needs <code>\\\"type\\\": \\\"http\\\"</code> in the entry, or it skips the server without a warning. Cursor infers the transport from the URL and needs no type field. VS Code uses a top-level <code>servers</code> key and prompts for the token instead of storing it.</p><h3>Localhost Configuration Without a Token</h3><p>While you build, turn on <strong>Trust localhost without a token</strong> in the same settings card. A dev server on your machine then accepts loopback calls with no header at all. Standalone installs run on port 3000, and the monorepo dev server runs on port 4200.</p><p>Add this block to <code>.mcp.json</code> in your project root for Claude Code.</p><div class='my-8 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'><div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'><span class='h-3 w-3 rounded-full bg-red-400/70'></span><span class='h-3 w-3 rounded-full bg-yellow-400/70'></span><span class='h-3 w-3 rounded-full bg-green-400/70'></span><span class='ml-3 text-xs font-mono text-slate-400'>.mcp.json</span></div><pre class='m-0 overflow-x-auto rounded-none bg-transparent px-6 py-5 text-sm leading-6 text-slate-200'><code>{\\n \\\"mcpServers\\\": {\\n \\\"nextblock\\\": {\\n \\\"type\\\": \\\"http\\\",\\n \\\"url\\\": \\\"http://localhost:3000/api/mcp\\\"\\n }\\n }\\n}</code></pre></div><p>The CLI form does the same thing in one line.</p><div class='my-8 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'><div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'><span class='h-3 w-3 rounded-full bg-red-400/70'></span><span class='h-3 w-3 rounded-full bg-yellow-400/70'></span><span class='h-3 w-3 rounded-full bg-green-400/70'></span><span class='ml-3 text-xs font-mono text-slate-400'>terminal</span></div><pre class='m-0 overflow-x-auto rounded-none bg-transparent px-6 py-5 text-sm leading-6 text-slate-200'><code>claude mcp add --transport http nextblock http://localhost:3000/api/mcp</code></pre></div><div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Development only</p><p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Localhost trust is ignored in production builds, because a proxy can spoof the host header. Production always uses a token.</p></div><h3>Production Authentication via Bearer Tokens</h3><p>In production every client sends a bearer token. Tokens start with <code>nbmcp_</code>, are shown once at mint time, and are stored only as SHA-256 hashes. Revoking one is a single click, and the same value can never be minted again.</p><p>Add this to <code>.cursor/mcp.json</code> for Cursor, or drop the same entry into <code>.mcp.json</code> with a type field for Claude Code.</p><div class='my-8 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'><div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'><span class='h-3 w-3 rounded-full bg-red-400/70'></span><span class='h-3 w-3 rounded-full bg-yellow-400/70'></span><span class='h-3 w-3 rounded-full bg-green-400/70'></span><span class='ml-3 text-xs font-mono text-slate-400'>.cursor/mcp.json</span></div><pre class='m-0 overflow-x-auto rounded-none bg-transparent px-6 py-5 text-sm leading-6 text-slate-200'><code>{\\n \\\"mcpServers\\\": {\\n \\\"nextblock\\\": {\\n \\\"url\\\": \\\"https://your-site.com/api/mcp\\\",\\n \\\"headers\\\": { \\\"Authorization\\\": \\\"Bearer nbmcp_your_token\\\" }\\n }\\n }\\n}</code></pre></div><div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Try it</p><p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Once the client connects, ask for something concrete. Try &ldquo;inspect the database schema and draft a pricing page with three tiers.&rdquo; The agent calls the schema tool, then the layout tool, and your CMS shows a new Live Draft ready for review.</p></div><h2>Zero-Redeploy Production Rendering</h2><p>The reason this works is where the output lands. Cortex AI writes strict JSONB block records into PostgreSQL, not HTML strings and not source files. Each block has a Zod schema, so a bad field is rejected before it is stored.</p><div class='grid gap-4 md:grid-cols-3 my-8'><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 text-center dark:border-sky-500/20 dark:bg-sky-500/10'><p class='mt-0 mb-0 text-4xl font-extrabold text-slate-900 dark:text-white'>0</p><p class='mt-2 mb-0 text-xs font-semibold uppercase tracking-[0.2em] text-sky-700 dark:text-sky-200'>Rebuilds to publish</p></div><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 text-center dark:border-violet-500/20 dark:bg-violet-500/10'><p class='mt-0 mb-0 text-4xl font-extrabold text-slate-900 dark:text-white'>1</p><p class='mt-2 mb-0 text-xs font-semibold uppercase tracking-[0.2em] text-violet-700 dark:text-violet-200'>Zod schema per block</p></div><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 text-center dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='mt-0 mb-0 text-4xl font-extrabold text-slate-900 dark:text-white'>100</p><p class='mt-2 mb-0 text-xs font-semibold uppercase tracking-[0.2em] text-emerald-700 dark:text-emerald-200'>Lighthouse by default</p></div></div><p>At request time Next.js 16 renders those records with Server Components and caches the result at the edge. There is no build step between publish and live, and no HTML sanitizer tax on every render. The 100/100 Lighthouse defaults you get on an empty site are the same ones you get after the agent has drafted fifty pages.</p><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 my-10 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300'>Next step</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Connect your agent today.</p><p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>One license unlocks the dashboard copilot and the MCP server. Read how the block registry keeps all of it safe if you want the full picture.</p><div class='mt-5 flex flex-wrap gap-3'><a href='https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Get the Cortex AI license</a><a href='/article/how-nextblock-works' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>How NextBlock works</a></div></div>\"}$nbmcpguide2$::jsonb,\n updated_at = now()\n WHERE post_id = v_post\n AND block_type = 'text'\n AND \"order\" = 0\n AND content->>'html_content' LIKE '{\"type\":\"doc\"%'\n AND content->>'html_content' LIKE '%This guide shows you how to connect Claude to NextBlock CMS with an open standard%';\n END IF;\nEND\n$body$;\n"
211
+ },
212
+ {
213
+ "version": "00000000000039",
214
+ "name": "00000000000039_mcp_guide_diagram_dimensions.sql",
215
+ "sql": "-- 00000000000039_mcp_guide_diagram_dimensions.sql\n--\n-- 00000000000038 registered public/images/mcp-prompt-to-production.webp at 2000x1025.\n-- The rendered figure was blurry because next/image was told it displays at 768px,\n-- re-encoded it as quality-60 AVIF and the browser upscaled the result to the\n-- full article width. The asset is now re-rendered at 2560x1312\n-- and served unoptimized (see ClientTextBlockRenderer's knownCmsImages); this\n-- brings the media-library row in line with the file on disk. 038 is already\n-- applied, so this is a separate file.\n--\n-- Idempotent: keyed by media.object_key, guarded on the values differing.\n\nUPDATE public.media\n SET width = 2560,\n height = 1312,\n size_bytes = 156358,\n updated_at = now()\n WHERE object_key = 'images/mcp-prompt-to-production.webp'\n AND (width IS DISTINCT FROM 2560\n OR height IS DISTINCT FROM 1312\n OR size_bytes IS DISTINCT FROM 156358);\n"
216
+ },
217
+ {
218
+ "version": "00000000000040",
219
+ "name": "00000000000040_home_comparison_chart_free_cms.sql",
220
+ "sql": "-- 00000000000040_home_comparison_chart_free_cms.sql\n--\n-- Two marketing changes to the EN home page that 00000000000037 seeded:\n--\n-- 1. The prototype-tools-versus-NextBlock block under \"Why an AI Website Builder\n-- CMS Beats Prototype Code\" was two prose cards that did not read as a\n-- comparison. It is now a seven-row comparison chart (day-one output, editing,\n-- drafts and revisions, publishing, translations and SEO, AI costs, price)\n-- with the NextBlock column highlighted, followed by three pricing tiles.\n-- 2. The pricing story is explicit everywhere the AI is mentioned: the CMS is\n-- 100% free and open source, forever; Cortex AI is the one paid license, and\n-- it runs on the AI subscription the customer already has (no token markup).\n-- The hero card, the pricing tiles and the Cortex promo all carry it.\n--\n-- 037 is already applied on the sandbox, so this is a separate file. Each UPDATE\n-- is guarded on a phrase unique to the 037 copy (Production on day two. / A codebase to babysit. /\n-- One AI Engine) that the new copy does not contain, so re-runs are no-ops and a\n-- home page an editor has since customised is left alone. The page still grades\n-- 100/100 with the focus keyphrase \"AI website builder CMS\" (libs/utils/src/lib/seo).\n--\n-- After adding this file: npm run generate:migrations-bundle && npm run\n-- generate:sandbox && npm run sync:create-nextblock.\n\nDO $body$\nDECLARE\n v_en integer;\n v_home integer;\nBEGIN\n SELECT id INTO v_en FROM public.languages WHERE code = 'en' LIMIT 1;\n\n SELECT id INTO v_home\n FROM public.pages\n WHERE slug = 'home' AND language_id = v_en\n ORDER BY id\n LIMIT 1;\n\n IF v_home IS NULL THEN\n RETURN;\n END IF;\n\n -- Hero: same H1; subtitle now says free and open source; right card carries the\n -- \"Free CMS. Paid AI.\" message; primary CTA reads \"Start Free\".\n UPDATE public.blocks\n SET content = $nbhero2${\"is_hero\":true,\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#1e293b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h1 class='text-5xl md:text-6xl font-extrabold tracking-tight text-white text-center leading-tight'>Prompt to <span class='relative inline-block mx-1 group'><span class='absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-400 translate-y-1 md:translate-y-2 transform -skew-x-12 rounded-sm shadow-lg group-hover:skew-x-0 transition-transform duration-300 ease-out'></span><span class='relative text-white italic px-1'>Production</span></span>.<br class='md:hidden' /> The AI Website Builder CMS.</h1>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4 leading-relaxed'>NextBlock is the free, open-source AI website builder CMS for teams that want more than a prototype. Point Claude Code, Cursor, or ChatGPT at your site over MCP and prompt the pages you need. You keep a live Next.js 16 and Supabase website with a visual editor your team owns on day two.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/article/how-to-setup-nextblock\",\"size\":\"lg\",\"text\":\"Start Free\",\"variant\":\"default\",\"position\":\"center\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/article/cortex-ai-mcp-connection-guide\",\"size\":\"lg\",\"text\":\"Connect Claude or Cursor\",\"variant\":\"outline\",\"position\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-wrap justify-center gap-6 text-sm uppercase tracking-wide text-slate-400 mt-8'><a href='https://github.com/nextblock-cms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>GitHub</a><a href='https://x.com/NextBlockCMS' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>X</a><a href='https://www.linkedin.com/in/nextblock/' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>LinkedIn</a><a href='https://dev.to/nextblockcms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>Dev.to</a><a href='https://www.npmjs.com/~nextblockcms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>npm</a></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='p-10 border border-white/10 rounded-3xl bg-white/5 backdrop-blur-xl shadow-2xl relative overflow-hidden group'><div class='absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500'></div><div class='relative z-10'><p class='text-xs text-white uppercase tracking-widest font-semibold mb-2'>Free forever. AI optional.</p><p class='text-3xl font-bold text-white mb-2'>Free CMS. Paid AI. Your call.</p><p class='text-base text-slate-300 mb-6'>The CMS is 100% free and open source. Cortex AI is the one thing we sell, and it runs on the AI subscription you already have.</p><ul class='space-y-3 text-sm text-slate-200'><li><span class='text-blue-400 mr-2'>&#10003;</span> Unlimited pages, editors, and sites at no cost.</li><li><span class='text-blue-400 mr-2'>&#10003;</span> One Cortex AI license unlocks the copilot and the MCP server.</li><li><span class='text-blue-400 mr-2'>&#10003;</span> No token markup: your Claude or ChatGPT plan does the work.</li></ul><div class='mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg'><div class='relative w-full aspect-video'><iframe class='absolute inset-0 h-full w-full border-0' src='https://www.youtube-nocookie.com/embed/71MyfoL4YVM?si=jtlDXV6cSC8rDgz0' title='NextBlock demo video' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' referrerpolicy='strict-origin-when-cross-origin' loading='lazy' allowfullscreen></iframe></div></div></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}$nbhero2$::jsonb,\n updated_at = now()\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text LIKE '%Production on day two.%';\n\n -- \"Why\" section: comparison chart + pricing tiles replace the two prose cards.\n UPDATE public.blocks\n SET content = $nbwhy2${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"none\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"text_content\":\"Why an AI Website Builder CMS Beats Prototype Code.\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-lg text-muted-foreground text-center max-w-3xl mx-auto mb-10'>Prompt builders like Lovable, Bolt, and v0 are great at a first draft. Then they hand you code you must host, patch, and rebuild for every copy change. NextBlock takes the same prompts and turns them into a website your whole team can run.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='grid gap-6 md:grid-cols-3'><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>🧩</div><h3 class='text-lg font-bold text-foreground mb-2'>Prompt to CMS, Not Just Code.</h3><p class='text-sm text-muted-foreground leading-relaxed'>External AI agents scaffold real layouts as typed blocks. Editors then tweak copy, images, and sections in a Notion-style editor. Nobody writes a prompt or touches code to change a headline.</p></div><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>🔌</div><h3 class='text-lg font-bold text-foreground mb-2'>Bring Your Own AI Subscription via MCP.</h3><p class='text-sm text-muted-foreground leading-relaxed'>Connect Claude Code, Cursor, ChatGPT, or Gemini straight to /api/mcp. You pay for the AI plan you already have, not a SaaS token markup.</p></div><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>⚡</div><h3 class='text-lg font-bold text-foreground mb-2'>Zero-Redeploy JSONB Architecture.</h3><p class='text-sm text-muted-foreground leading-relaxed'>Generated blocks land in PostgreSQL as validated JSONB. Pages publish in seconds with 100/100 Lighthouse defaults and no rebuild.</p></div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='mt-12 overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900'><div class='flex flex-wrap items-end justify-between gap-3 px-5 pt-6 pb-4'><div><p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Side by side</p><p class='mt-1 text-xl font-semibold text-foreground'>Prototype tools versus NextBlock.</p></div><p class='text-xs text-slate-500 dark:text-slate-400'>Lovable, Bolt, and v0 compared on what happens after the first prompt.</p></div><div class='overflow-x-auto'><table class='w-full min-w-[640px] border-collapse'><thead><tr class='bg-slate-50 dark:bg-slate-950/60'><th scope='col' class='px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400'>What you get</th><th scope='col' class='px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-rose-600 dark:text-rose-300'>Lovable, Bolt, v0</th><th scope='col' class='bg-emerald-50/60 px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-200'>NextBlock</th></tr></thead><tbody><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Day one</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Prototype code you host and patch yourself.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>A live Next.js 16 and Supabase website.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Editing after launch</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>A new prompt or a pull request for every change.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>A Notion-style visual editor, no code needed.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Drafts and revisions</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Code history only, nothing for content.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Live Drafts and one-click restore of any page.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Publishing</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Rebuild and redeploy.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Instant, straight from PostgreSQL.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Translations and SEO</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Not built in.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Per-block translation and SEO graded as you type.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>AI costs</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Platform credits, bought from the tool.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Your own subscription over MCP, no markup.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Price of the platform</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>A monthly plan.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Free and open source, forever.</td></tr></tbody></table></div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='mt-10 grid gap-4 md:grid-cols-3'><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Free forever</p><p class='mt-3 text-xl font-semibold text-foreground'>The CMS costs nothing.</p><p class='mt-3 text-sm text-muted-foreground'>The AI website builder CMS is free and open source. Unlimited pages, editors, and sites, self-hosted or on Vercel, with no plan to outgrow.</p></div><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Cortex AI, when you want it</p><p class='mt-3 text-xl font-semibold text-foreground'>One license, two AI surfaces.</p><p class='mt-3 text-sm text-muted-foreground'>A paid Cortex AI license adds the dashboard copilot and the MCP server. Start without it and add it later. Your content never changes.</p></div><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200'>No token markup</p><p class='mt-3 text-xl font-semibold text-foreground'>Your plan does the work.</p><p class='mt-3 text-sm text-muted-foreground'>Cortex AI runs on the Claude, ChatGPT, Cursor, or Gemini subscription you already pay for. The license buys the connection, not the tokens.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1}}$nbwhy2$::jsonb,\n updated_at = now()\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text LIKE '%A codebase to babysit.%';\n\n -- Cortex promo: \"The CMS Is Free. The AI Is the Upgrade.\"\n UPDATE public.blocks\n SET content = $nbcortexpromo2${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4'>Now Available — Cortex AI</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>The CMS Is Free.<br/>The AI Is the Upgrade.</h2><p class='text-lg text-slate-300 max-w-2xl leading-relaxed mb-4'>NextBlock costs nothing and never will. Cortex AI is the one license we sell. It adds a copilot inside the dashboard and an MCP server outside it, so Claude Code, Cursor, ChatGPT, and Gemini can build your site.</p><p class='text-base text-slate-400 max-w-2xl leading-relaxed'>You bring the AI subscription you already pay for. The license buys the connection, never the tokens, and nothing goes live until an editor publishes it.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/article/cortex-ai-mcp-connection-guide\",\"size\":\"lg\",\"text\":\"Read the MCP Setup Guide →\",\"variant\":\"default\",\"position\":\"left\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"size\":\"lg\",\"text\":\"Get a License\",\"variant\":\"outline\",\"position\":\"left\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-1'><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>$0</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>The CMS, forever</p></div><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>1</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>License for copilot and MCP server</p></div><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>0 %</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>Token markup</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}$nbcortexpromo2$::jsonb,\n updated_at = now()\n WHERE page_id = v_home\n AND block_type = 'section'\n AND content::text LIKE '%One AI Engine%';\nEND\n$body$;\n\n-- The MCP guide's featured image (public/images/cortex_post.webp) was\n-- replaced with a re-cut 1672x941 version after 037\n-- registered it at 2664x1568. The post\n-- hero reserves its box from media.width/height, so bring the row in line with the\n-- file on disk. Guarded on the values differing.\nUPDATE public.media\n SET width = 1672,\n height = 941,\n size_bytes = 503790,\n updated_at = now()\n WHERE object_key = 'images/cortex_post.webp'\n AND (width IS DISTINCT FROM 1672\n OR height IS DISTINCT FROM 941\n OR size_bytes IS DISTINCT FROM 503790);\n"
221
+ },
222
+ {
223
+ "version": "00000000000041",
224
+ "name": "00000000000041_marketing_review_free_cms_trial.sql",
225
+ "sql": "-- 00000000000041_marketing_review_free_cms_trial.sql\n--\n-- Marketing review of every seeded surface that sells Cortex AI, after\n-- 037/038/040 were applied. Four changes, all copy:\n--\n-- 1. Pricing is stated wherever the AI is mentioned, not only at the bottom of\n-- the home page: the CMS is 100% free and open source, forever; Cortex AI\n-- (the in-editor AI plus the MCP server) is the one paid license, and it\n-- starts with a 30-day free trial with no credit card (matching\n-- products.trial_period_days / trial_requires_payment_method on the Cortex\n-- product). The hero now leads with \"deploy to Vercel in one click, up in\n-- ten minutes, try it yourself\"; the \"why\" tiles, the MCP walkthrough, the\n-- Cortex promo, the product page, the MCP guide and the articles page all\n-- carry the same message.\n-- 2. The word \"copilot\" is gone. The product has no copilot; it has Cortex AI,\n-- which runs inside the editor and as an MCP server. The product title\n-- becomes \"NextBlock™ Cortex AI MCP Server & AI Editor License\".\n-- 3. The MCP guide's prototype-vs-NextBlock prose cards become a comparison\n-- chart, and the rasterised diagram becomes a CSS/HTML flow built from\n-- cards inside the rich-text block (crisp at every size, theme-aware, no\n-- overflowing titles). Its media row is removed once nothing references it.\n-- 4. The articles page ('articles' / FR 'articles') gets a short hero: the\n-- ~300-word essay 032 put in the hero moves below the posts grid as a\n-- \"What the journal covers\" section with four topic cards, so the page\n-- keeps its word count and 100/100 grade without a wall of text up top.\n--\n-- Every UPDATE is guarded on a phrase unique to the currently applied copy that\n-- the replacement does not contain, so re-runs are no-ops and customised pages\n-- are left alone. Verified at 100/100 with libs/utils/src/lib/seo for the home\n-- page (\"AI website builder CMS\"), the product (\"Cortex AI MCP server\"), the\n-- guide (\"connect Claude to NextBlock CMS\") and both articles pages.\n--\n-- After adding this file: npm run generate:migrations-bundle && npm run\n-- generate:sandbox && npm run sync:create-nextblock. The sandbox reset route\n-- (enrichCortexAiProducts) mirrors the product sections and title.\n\nDO $body$\nDECLARE\n v_en integer;\n v_fr integer;\n v_home integer;\n v_cortex uuid;\n v_post integer;\n v_articles_en integer;\n v_articles_fr integer;\nBEGIN\n SELECT id INTO v_en FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n ---------------------------------------------------------------------------\n -- 1. Home page (EN)\n ---------------------------------------------------------------------------\n SELECT id INTO v_home FROM public.pages WHERE slug = 'home' AND language_id = v_en ORDER BY id LIMIT 1;\n\n IF v_home IS NOT NULL THEN\n UPDATE public.blocks SET content = $nbhero3${\"is_hero\":true,\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#1e293b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h1 class='text-5xl md:text-6xl font-extrabold tracking-tight text-white text-center leading-tight'>Prompt to <span class='relative inline-block mx-1 group'><span class='absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-400 translate-y-1 md:translate-y-2 transform -skew-x-12 rounded-sm shadow-lg group-hover:skew-x-0 transition-transform duration-300 ease-out'></span><span class='relative text-white italic px-1'>Production</span></span>.<br class='md:hidden' /> The AI Website Builder CMS.</h1>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4 leading-relaxed'>NextBlock is the free, open-source AI website builder CMS for teams that want more than a prototype. Add Cortex AI to point Claude Code, Cursor, or ChatGPT at your site over MCP, free for 30 days. You keep a live Next.js 16 and Supabase website with a visual editor your team owns on day two.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\",\"size\":\"lg\",\"text\":\"Deploy to Vercel in One Click\",\"variant\":\"default\",\"position\":\"center\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"size\":\"lg\",\"text\":\"Start the Free Cortex AI Trial\",\"variant\":\"outline\",\"position\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-wrap justify-center gap-6 text-sm uppercase tracking-wide text-slate-400 mt-8'><a href='https://github.com/nextblock-cms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>GitHub</a><a href='https://x.com/NextBlockCMS' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>X</a><a href='https://www.linkedin.com/in/nextblock/' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>LinkedIn</a><a href='https://dev.to/nextblockcms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>Dev.to</a><a href='https://www.npmjs.com/~nextblockcms' target='_blank' rel='noopener noreferrer' class='hover:text-white transition-colors'>npm</a></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='p-10 border border-white/10 rounded-3xl bg-white/5 backdrop-blur-xl shadow-2xl relative overflow-hidden group'><div class='absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500'></div><div class='relative z-10'><p class='text-xs text-white uppercase tracking-widest font-semibold mb-2'>Up in 10 minutes. Free to start.</p><p class='text-3xl font-bold text-white mb-2'>Try the whole stack for free.</p><p class='text-base text-slate-300 mb-6'>The CMS is free forever. Cortex AI, the paid AI layer that includes the MCP server, starts with a 30-day trial and no credit card. One click deploys to Vercel, and you are prompting your own site in ten minutes.</p><ul class='space-y-3 text-sm text-slate-200'><li><span class='text-blue-400 mr-2'>&#10003;</span> CMS: free and open source, with unlimited pages, editors, and sites.</li><li><span class='text-blue-400 mr-2'>&#10003;</span> Cortex AI: 30 days free, no credit card, then one license.</li><li><span class='text-blue-400 mr-2'>&#10003;</span> Bring your own AI subscription. No token markup, ever.</li></ul><div class='mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg'><div class='relative w-full aspect-video'><iframe class='absolute inset-0 h-full w-full border-0' src='https://www.youtube-nocookie.com/embed/71MyfoL4YVM?si=jtlDXV6cSC8rDgz0' title='NextBlock demo video' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' referrerpolicy='strict-origin-when-cross-origin' loading='lazy' allowfullscreen></iframe></div></div></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}$nbhero3$::jsonb, updated_at = now()\n WHERE page_id = v_home AND block_type = 'section' AND content::text LIKE '%Free CMS. Paid AI. Your call.%';\n\n UPDATE public.blocks SET content = $nbwhy3${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"none\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"text_content\":\"Why an AI Website Builder CMS Beats Prototype Code.\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-lg text-muted-foreground text-center max-w-3xl mx-auto mb-10'>Prompt builders like Lovable, Bolt, and v0 are great at a first draft. Then they hand you code you must host, patch, and rebuild for every copy change. NextBlock takes the same prompts and turns them into a website your whole team can run.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='grid gap-6 md:grid-cols-3'><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>🧩</div><h3 class='text-lg font-bold text-foreground mb-2'>Prompt to CMS, Not Just Code.</h3><p class='text-sm text-muted-foreground leading-relaxed'>External AI agents scaffold real layouts as typed blocks. Editors then tweak copy, images, and sections in a Notion-style editor. Nobody writes a prompt or touches code to change a headline.</p></div><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>🔌</div><h3 class='text-lg font-bold text-foreground mb-2'>Bring Your Own AI Subscription via MCP.</h3><p class='text-sm text-muted-foreground leading-relaxed'>Connect Claude Code, Cursor, ChatGPT, or Gemini straight to /api/mcp. You pay for the AI plan you already have, not a SaaS token markup. This is the Cortex AI layer: free for 30 days, no credit card.</p></div><div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-blue-300 dark:border-slate-800 dark:bg-slate-900'><div class='mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-xl dark:bg-blue-950'>⚡</div><h3 class='text-lg font-bold text-foreground mb-2'>Zero-Redeploy JSONB Architecture.</h3><p class='text-sm text-muted-foreground leading-relaxed'>Generated blocks land in PostgreSQL as validated JSONB. Pages publish in seconds with 100/100 Lighthouse defaults and no rebuild.</p></div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='mt-12 overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900'><div class='flex flex-wrap items-end justify-between gap-3 px-5 pt-6 pb-4'><div><p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Side by side</p><p class='mt-1 text-xl font-semibold text-foreground'>Prototype tools versus NextBlock.</p></div><p class='text-xs text-slate-500 dark:text-slate-400'>Lovable, Bolt, and v0 compared on what happens after the first prompt.</p></div><div class='overflow-x-auto'><table class='w-full min-w-[640px] border-collapse'><thead><tr class='bg-slate-50 dark:bg-slate-950/60'><th scope='col' class='px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400'>What you get</th><th scope='col' class='px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-rose-600 dark:text-rose-300'>Lovable, Bolt, v0</th><th scope='col' class='bg-emerald-50/60 px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-200'>NextBlock</th></tr></thead><tbody><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Day one</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Prototype code you host and patch yourself.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>A live Next.js 16 and Supabase website.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Editing after launch</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>A new prompt or a pull request for every change.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>A Notion-style visual editor, no code needed.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Drafts and revisions</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Code history only, nothing for content.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Live Drafts and one-click restore of any page.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Publishing</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Rebuild and redeploy.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Instant, straight from PostgreSQL.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Translations and SEO</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Not built in.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Per-block translation and SEO graded as you type.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>AI costs</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Platform credits, bought from the tool.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Your own subscription over MCP, no markup.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Price of the platform</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>A monthly plan.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Free and open source, forever. Cortex AI is the only paid add-on.</td></tr></tbody></table></div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='mt-10 grid gap-4 md:grid-cols-3'><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Free forever</p><p class='mt-3 text-xl font-semibold text-foreground'>The CMS costs nothing.</p><p class='mt-3 text-sm text-muted-foreground'>The AI website builder CMS is free and open source. Unlimited pages, editors, and sites, self-hosted or on Vercel, with no plan to outgrow.</p></div><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Cortex AI: 30-day free trial</p><p class='mt-3 text-xl font-semibold text-foreground'>Try the AI layer, then decide.</p><p class='mt-3 text-sm text-muted-foreground'>Cortex AI is the one paid license. It starts with a 30-day trial and no credit card, and it unlocks AI inside the editor plus the MCP server. Cancel by doing nothing.</p></div><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200'>No token markup</p><p class='mt-3 text-xl font-semibold text-foreground'>Your plan does the work.</p><p class='mt-3 text-sm text-muted-foreground'>Cortex AI runs on the Claude, ChatGPT, Cursor, or Gemini subscription you already pay for. The license buys the connection, not the tokens.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1}}$nbwhy3$::jsonb, updated_at = now()\n WHERE page_id = v_home AND block_type = 'section' AND content::text LIKE '%Cortex AI, when you want it%';\n\n UPDATE public.blocks SET content = $nbmcp2${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e293b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-cyan-300 font-semibold text-center mb-3'>Model Context Protocol</p><h2 class='text-3xl md:text-4xl font-extrabold text-white text-center mb-4'>How the MCP Connection Works.</h2><p class='text-lg text-slate-300 text-center max-w-3xl mx-auto mb-4'>Cortex AI exposes your CMS as an MCP server at /api/mcp. Any agent that speaks the standard can read your schema and write pages. The server is part of the Cortex AI license, and the 30-day trial needs no credit card.</p><p class='text-base text-slate-400 text-center max-w-2xl mx-auto mb-10'>Three steps take you from a fresh deploy to published pages, in about ten minutes.</p><div class='grid gap-6 md:grid-cols-3'><div class='rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur'><p class='text-xs uppercase tracking-[0.25em] text-cyan-300 font-semibold mb-3'>Step 1</p><h3 class='text-lg font-bold text-white mb-2'>Deploy and start the trial.</h3><p class='text-sm text-slate-300 leading-relaxed'>One click to Vercel, then start the free Cortex AI trial in CMS Settings. Mint a scoped token and paste one JSON block into Claude Code, Cursor, or VS Code.</p></div><div class='rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur'><p class='text-xs uppercase tracking-[0.25em] text-cyan-300 font-semibold mb-3'>Step 2</p><h3 class='text-lg font-bold text-white mb-2'>Prompt the layout.</h3><p class='text-sm text-slate-300 leading-relaxed'>Ask for a landing page. The agent calls generate_jsonb_layout and the result lands as a Live Draft made of validated blocks.</p></div><div class='rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur'><p class='text-xs uppercase tracking-[0.25em] text-cyan-300 font-semibold mb-3'>Step 3</p><h3 class='text-lg font-bold text-white mb-2'>Edit and publish.</h3><p class='text-sm text-slate-300 leading-relaxed'>Editors refine the draft in the visual editor and hit publish. The page goes live from PostgreSQL with no redeploy.</p></div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='mt-12 grid gap-8 md:grid-cols-2'><div class='rounded-2xl border border-white/10 bg-slate-950/60 p-6'><h3 class='text-base font-bold text-white mb-4'>For Writers and Editors</h3><ul class='space-y-3 text-sm text-slate-300'><li><strong class='text-white'>Notion-style editor.</strong> Slash commands, drag and drop, and inline AI on every block.</li><li><strong class='text-white'>Live Drafts.</strong> Stage AI output safely, then publish when it reads right.</li><li><strong class='text-white'>Revisions.</strong> Restore any version of a page or post with one click.</li><li><strong class='text-white'>Built-in SEO checks.</strong> Headings, keyphrases, readability, and metadata graded as you type.</li></ul></div><div class='rounded-2xl border border-white/10 bg-slate-950/60 p-6'><h3 class='text-base font-bold text-white mb-4'>For Developers</h3><ul class='space-y-3 text-sm text-slate-300'><li><strong class='text-white'>Next.js 16 core.</strong> Server Components, ISR, and edge caching out of the box.</li><li><strong class='text-white'>Supabase backend.</strong> Postgres, auth, storage, and row-level security you can read.</li><li><strong class='text-white'>Typed block registry.</strong> Every block has a Zod schema, so AI output is validated before it renders.</li><li><strong class='text-white'>Open source.</strong> Self-host on Docker or deploy to Vercel in one click.</li></ul></div></div><p class='mt-10 text-center text-xs uppercase tracking-[0.25em] text-slate-400'>Built with Next.js, React, Supabase, Stripe, Tailwind, Tiptap, Vercel, and Nx.</p>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1}}$nbmcp2$::jsonb, updated_at = now()\n WHERE page_id = v_home AND block_type = 'section' AND content::text LIKE '%Three steps take you from a blank site%';\n\n UPDATE public.blocks SET content = $nbcortexpromo3${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4'>Now Available — Cortex AI</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>The CMS Is Free.<br/>Cortex AI Is the Upgrade.</h2><p class='text-lg text-slate-300 max-w-2xl leading-relaxed mb-4'>NextBlock costs nothing and never will. Cortex AI is the one license we sell. It adds AI inside the editor and an MCP server outside it, so Claude Code, Cursor, ChatGPT, and Gemini can build your site.</p><p class='text-base text-slate-400 max-w-2xl leading-relaxed'>Try it free for 30 days with no credit card. You bring the AI subscription you already pay for. The license buys the connection, never the tokens, and nothing goes live until an editor publishes it.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"size\":\"lg\",\"text\":\"Start the 30-Day Free Trial →\",\"variant\":\"default\",\"position\":\"left\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/article/cortex-ai-mcp-connection-guide\",\"size\":\"lg\",\"text\":\"Read the MCP Setup Guide\",\"variant\":\"outline\",\"position\":\"left\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-1'><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>$0</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>The CMS, forever</p></div><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>30</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>Days of Cortex AI free, no card</p></div><div class='rounded-2xl border border-violet-500/30 bg-white/5 p-6 text-center'><p class='text-3xl font-extrabold text-white'>0 %</p><p class='text-xs uppercase tracking-wider text-violet-200 mt-1'>Token markup</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}$nbcortexpromo3$::jsonb, updated_at = now()\n WHERE page_id = v_home AND block_type = 'section' AND content::text LIKE '%The AI Is the Upgrade%';\n END IF;\n\n ---------------------------------------------------------------------------\n -- 2. Cortex AI product (EN)\n ---------------------------------------------------------------------------\n SELECT id INTO v_cortex FROM public.products WHERE slug = 'nextblock-cortex-ai-cortex-ai-license' AND language_id = v_en LIMIT 1;\n\n IF v_cortex IS NOT NULL THEN\n UPDATE public.products SET title = 'NextBlock™ Cortex AI MCP Server & AI Editor License', updated_at = now()\n WHERE id = v_cortex AND title IN ('NextBlock™ Cortex AI MCP Server & Copilot License', 'NextBlock™ Cortex AI MCP Server & AI Editor License');\n\n UPDATE public.products SET short_description = 'NextBlock™ Cortex AI is the AI layer for your free CMS. Route any model through your own OpenRouter key inside the editor, or register /api/mcp and let Claude Code, Cursor, and ChatGPT build layouts, inspect your schema, and manage content on the AI subscription you already pay for. Free for 30 days, no credit card.', updated_at = now()\n WHERE id = v_cortex\n AND (short_description IS NULL OR short_description = ''\n OR short_description = 'NextBlock™ Cortex AI is the AI layer for your CMS. Route any model through your own OpenRouter key in the dashboard, or register /api/mcp and let Claude Code, Cursor, and ChatGPT build layouts, inspect your schema, and manage content on the AI subscription you already pay for.'\n OR short_description = 'NextBlock™ Cortex AI is the AI layer for your free CMS. Route any model through your own OpenRouter key inside the editor, or register /api/mcp and let Claude Code, Cursor, and ChatGPT build layouts, inspect your schema, and manage content on the AI subscription you already pay for. Free for 30 days, no credit card.');\n\n IF EXISTS (SELECT 1 FROM public.blocks WHERE product_id = v_cortex AND content::text LIKE '%Editor Copilot%') THEN\n DELETE FROM public.blocks WHERE product_id = v_cortex;\n INSERT INTO public.blocks (product_id, language_id, block_type, content, \"order\") VALUES\n (v_cortex, v_en, 'section', $nbcx0v2${\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#312e81\",\"position\":35},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4'>MCP-Native AI Layer · 30-Day Free Trial</p><h2 class='text-3xl md:text-5xl font-extrabold text-white leading-tight mb-5'>Your CMS as a Cortex AI MCP Server.</h2><p class='text-base md:text-lg text-slate-200 leading-relaxed mb-4'>Cortex AI runs two ways. Inside the editor it routes to any model through your own OpenRouter key. Over MCP it turns NextBlock into a server that Claude Code, Cursor, and ChatGPT operate from their own chat window.</p><p class='text-base text-slate-300 leading-relaxed mb-6'>The CMS is free forever. Cortex AI is the one license we sell, and it starts with 30 days free and no credit card.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Read the MCP Setup Guide →\",\"url\":\"/article/cortex-ai-mcp-connection-guide\",\"variant\":\"default\",\"size\":\"lg\",\"position\":\"left\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8'><h3 class='text-lg font-bold text-white mb-5'>Bring Your Own AI Subscription</h3><ul class='space-y-4 text-sm leading-relaxed text-slate-300'><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>Free for 30 days</strong> — start the trial with no credit card. If you do nothing, it simply ends.</span></li><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>No token markup</strong> — use Claude Pro, ChatGPT Plus, Cursor, or Gemini Advanced by registering /api/mcp. You pay your provider, not us.</span></li><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>Editor BYOK</strong> — OpenRouter routing with your own key, so you pick the model and keep the bill.</span></li><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>Scoped tokens</strong> — mint read-only or write tokens in CMS Settings and revoke them any time.</span></li><li class='flex items-start gap-3'><span class='flex-shrink-0 w-6 h-6 rounded-full bg-violet-950 flex items-center justify-center text-violet-300 text-xs font-bold'>✓</span><span><strong class='text-white'>Live Drafts</strong> — generated layouts stage as drafts. Nothing goes live until an editor publishes it.</span></li></ul></div>\"}}]]}$nbcx0v2$::jsonb, 0),\n (v_cortex, v_en, 'section', $nbcx1v2${\"container_type\":\"container\",\"background\":{\"type\":\"theme\",\"theme\":\"muted\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":4},\"column_gap\":\"lg\",\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"vertical_alignment\":\"center\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>30</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>Days free, no card</span></p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>0 %</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>Token markup</span></p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>5</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>MCP contract tools</span></p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center'><span class='block text-2xl font-extrabold text-foreground'>29</span><span class='text-xs text-muted-foreground uppercase tracking-wider'>Typed agent tools</span></p>\"}}]]}$nbcx1v2$::jsonb, 1),\n (v_cortex, v_en, 'section', $nbcx2v2${\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"vertical_alignment\":\"stretch\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>get_database_schema</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Returns every table the agent may read or change, with columns, keys, and read-only flags. The model plans against real structure, not guesses.</p></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>query_site_analytics</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Reads revenue, order counts, status breakdowns, and top products over a date range. Read-only, so it is safe on any token.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>generate_jsonb_layout</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Turns a prompt into a complete page layout. Blocks are validated against the NextBlock schema and staged as a Live Draft.</p></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>search_stock_media</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Finds free stock photos on Pexels and Unsplash with alt text and credits. Drop a result straight into an image block.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>update_site_navigation</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Adds, renames, or reorders header menu items per locale. Append to keep the current menu or replace it in one call.</p></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950 dark:hover:border-violet-500 sm:p-7'><h3 class='text-base font-bold text-slate-900 dark:text-white mb-2'><code class='rounded bg-violet-50 px-2 py-1 text-sm text-violet-700 dark:bg-violet-950 dark:text-violet-200'>and 24 more</code></h3><p class='text-sm text-slate-600 dark:text-slate-300 leading-relaxed'>Create posts and products, translate pages, upload media, manage themes and scripts. Every tool is typed and scoped.</p></div>\"}}]]}$nbcx2v2$::jsonb, 2),\n (v_cortex, v_en, 'section', $nbcx3v2${\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"180deg\",\"stops\":[{\"color\":\"#020617\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4'>How It Works</p><h3 class='text-2xl md:text-3xl font-extrabold text-white mb-4'>One Registry, Standard Transport.</h3><p class='text-slate-300 leading-relaxed mb-5'>The MCP server speaks Streamable HTTP at /api/mcp. Your client posts JSON-RPC messages and gets typed results back. There is no SDK to install and no proxy in the middle.</p><ul class='space-y-3 text-sm text-slate-400'><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Bearer tokens are stored as SHA-256 hashes and shown once.</span></li><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Localhost trust lets a dev server skip the token while you build.</span></li><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Read-only tokens never see a mutating tool in the list.</span></li><li class='flex items-start gap-2.5'><span class='text-violet-400'>→</span><span>Copy-paste config for Claude Code, Cursor, VS Code, and Claude Desktop.</span></li></ul>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='space-y-4'><div class='p-5 rounded-xl border border-slate-700 bg-slate-900'><h4 class='text-sm font-bold text-white mb-1'>Editor BYOK via OpenRouter</h4><p class='text-xs text-slate-400 leading-relaxed'>Pick any model, from Claude to Gemini to open weights, with one key. Costs stay on your OpenRouter bill and switch with a toggle.</p></div><div class='p-5 rounded-xl border border-slate-700 bg-slate-900'><h4 class='text-sm font-bold text-white mb-1'>AI Inside the Editor</h4><p class='text-xs text-slate-400 leading-relaxed'>An inline toolbar rewrites copy, refactors columns, and translates whole pages. Output is valid block data, so layouts never break.</p></div><div class='p-5 rounded-xl border border-slate-700 bg-slate-900'><h4 class='text-sm font-bold text-white mb-1'>Privacy-First Design</h4><p class='text-xs text-slate-400 leading-relaxed'>Requests go straight to your provider with your key. NextBlock never stores, logs, or trains on your content.</p></div></div>\"}}]]}$nbcx3v2$::jsonb, 3),\n (v_cortex, v_en, 'section', $nbcx4v2${\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"none\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"vertical_alignment\":\"center\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Ready to connect your AI to your CMS?\",\"textAlign\":\"center\",\"textColor\":\"background\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6'>One license unlocks Cortex AI in the editor and the MCP server. Start with 30 days free and no credit card, bring your own AI subscription, and keep your data yours.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Start the 30-Day Free Trial\",\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"variant\":\"secondary\",\"size\":\"lg\",\"position\":\"center\"}}]]}$nbcx4v2$::jsonb, 4);\n END IF;\n END IF;\n\n ---------------------------------------------------------------------------\n -- 3. MCP guide body (EN) + retire the rasterised diagram's media row\n ---------------------------------------------------------------------------\n SELECT id INTO v_post FROM public.posts WHERE slug = 'cortex-ai-mcp-connection-guide' AND language_id = v_en ORDER BY id LIMIT 1;\n\n IF v_post IS NOT NULL THEN\n UPDATE public.blocks SET content = $nbmcpguide3${\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>This guide shows you how to connect Claude to NextBlock CMS with an open standard instead of a vendor plugin. Cortex AI turns your site into a Model Context Protocol server. Claude Code, Cursor, ChatGPT, and Gemini can then read your database schema, draft page layouts, and update navigation from their own chat window. You pay for the AI subscription you already have, with no token markup in between.</p><div class='grid gap-4 md:grid-cols-3 my-10'><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200'>One config block</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Setup in minutes.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Paste one JSON entry into Claude Code, Cursor, or VS Code. The CMS settings card writes it for you.</p></div><div class='rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200'>Free to try</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>30 days, no credit card.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>The CMS is free forever. Cortex AI, which includes the MCP server, starts with a 30-day free trial.</p></div><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Live Drafts</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Nothing ships by accident.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Every layout the agent writes lands as a draft. An editor reviews it and hits publish.</p></div></div><p>Every prompt becomes a structured database record rather than a pile of generated code. Your editors keep a visual CMS, your developers keep a clean Next.js 16 app, and nothing goes live until someone hits publish.</p><h2>The Problem with Traditional AI Web Builders</h2><p>Prompt builders such as Lovable, Bolt, and v0 are impressive on day one. You describe a page and get a working React app in minutes. The trouble starts on day two.</p><div class='not-prose my-8 overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900'><div class='flex flex-wrap items-end justify-between gap-3 px-5 pt-6 pb-4'><div><p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Side by side</p><p class='mt-1 text-xl font-semibold text-slate-900 dark:text-white'>Prototype tools versus NextBlock.</p></div><p class='text-xs text-slate-500 dark:text-slate-400'>What happens after the first prompt.</p></div><div class='overflow-x-auto'><table class='w-full min-w-[640px] border-collapse'><thead><tr class='bg-slate-50 dark:bg-slate-950/60'><th scope='col' class='px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400'>What you get</th><th scope='col' class='px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-rose-600 dark:text-rose-300'>Lovable, Bolt, v0</th><th scope='col' class='bg-emerald-50/60 px-5 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-200'>NextBlock</th></tr></thead><tbody><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Day one</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Disposable code with no content layer.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>A live Next.js 16 and Supabase website with a CMS.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Changing a headline</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Another prompt or another pull request.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>An editor types it in a Notion-style editor.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Drafts and revisions</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Code history only.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Live Drafts, revisions, and one-click restore.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Translations and SEO</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Not built in.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Per-block translation and SEO checks as you type.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>AI costs</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>Platform credits, bought from the tool.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Your own subscription over MCP, no markup.</td></tr><tr class='border-t border-slate-200 dark:border-slate-800'><th scope='row' class='px-5 py-4 text-left align-top text-sm font-semibold text-foreground'>Price</th><td class='px-5 py-4 align-top text-sm text-muted-foreground'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-xs font-bold text-rose-600 dark:bg-rose-500/20 dark:text-rose-300'>&#10007;</span>A monthly plan.</td><td class='bg-emerald-50/60 px-5 py-4 align-top text-sm text-foreground dark:bg-emerald-500/10'><span class='mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200'>&#10003;</span>Free CMS. Cortex AI after a 30-day free trial.</td></tr></tbody></table></div></div><p>NextBlock takes the opposite view from the prototype tools. AI should draft the site, and people should own it. The MCP connection is how those two worlds meet.</p><h2>How Cortex AI Uses Model Context Protocol (MCP)</h2><p>Model Context Protocol is an open standard for giving AI agents tools. A client such as Claude Code lists the tools a server offers, calls them with typed arguments, and reads typed results back. Cortex AI ships that server inside your NextBlock install at <code>/api/mcp</code>.</p><div class='not-prose my-12 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 p-6 shadow-2xl sm:p-8'><p class='text-[0.65rem] font-semibold uppercase tracking-[0.3em] text-indigo-300'>Model Context Protocol · NextBlock Cortex AI</p><p class='mt-2 text-2xl font-extrabold text-white sm:text-3xl'>From prompt to production, without a redeploy.</p><p class='mt-2 max-w-2xl text-sm text-slate-400'>Your AI client talks to the CMS over an open standard. Output lands as data, not as code you have to host.</p><div class='mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4'><div class='relative flex flex-col rounded-2xl border border-sky-400/40 bg-white/5 p-5'><span class='block h-1 w-12 rounded-full bg-sky-400'></span><p class='mt-4 text-[0.65rem] font-semibold uppercase tracking-[0.25em] text-sky-300'>Step 1 · Your AI client</p><p class='mt-2 text-lg font-bold leading-snug text-white'>Prompt</p><p class='mt-1 text-xs text-slate-400'>Any MCP client you already pay for.</p><div class='mt-4 flex-1 text-sm text-slate-200'><div class='flex flex-wrap gap-2'><span class='rounded-full border border-sky-400/40 bg-sky-400/10 px-3 py-1 text-xs font-semibold text-slate-100'>Claude Code</span><span class='rounded-full border border-sky-400/40 bg-sky-400/10 px-3 py-1 text-xs font-semibold text-slate-100'>Cursor</span><span class='rounded-full border border-sky-400/40 bg-sky-400/10 px-3 py-1 text-xs font-semibold text-slate-100'>ChatGPT</span><span class='rounded-full border border-sky-400/40 bg-sky-400/10 px-3 py-1 text-xs font-semibold text-slate-100'>Gemini</span></div><p class='mt-4 text-xs text-slate-300'>One JSON block in your client config registers the server. That is the setup.</p></div><p class='mt-5 rounded-lg bg-sky-400/10 px-3 py-2 text-center text-[0.7rem] font-semibold text-sky-300'>Your subscription · no token markup</p><span class='absolute -right-4 top-1/2 hidden -translate-y-1/2 text-xl text-slate-500 xl:block' aria-hidden='true'>→</span></div><div class='relative flex flex-col rounded-2xl border border-violet-400/40 bg-white/5 p-5'><span class='block h-1 w-12 rounded-full bg-violet-400'></span><p class='mt-4 text-[0.65rem] font-semibold uppercase tracking-[0.25em] text-violet-300'>Step 2 · /api/mcp</p><p class='mt-2 text-lg font-bold leading-snug text-white'>Cortex AI MCP server</p><p class='mt-1 text-xs text-slate-400'>Streamable HTTP · bearer token · scoped.</p><div class='mt-4 flex-1 text-sm text-slate-200'><div class='space-y-1.5'><code class='block rounded-md bg-white/5 px-3 py-1.5 font-mono text-xs text-violet-100'>get_database_schema</code><code class='block rounded-md bg-white/5 px-3 py-1.5 font-mono text-xs text-violet-100'>generate_jsonb_layout</code><code class='block rounded-md bg-white/5 px-3 py-1.5 font-mono text-xs text-violet-100'>update_site_navigation</code><code class='block rounded-md bg-white/5 px-3 py-1.5 font-mono text-xs text-violet-100'>query_site_analytics</code><code class='block rounded-md bg-white/5 px-3 py-1.5 font-mono text-xs text-violet-100'>search_stock_media</code></div></div><p class='mt-5 rounded-lg bg-violet-400/10 px-3 py-2 text-center text-[0.7rem] font-semibold text-violet-300'>5 contract tools · 29 typed tools</p><span class='absolute -right-4 top-1/2 hidden -translate-y-1/2 text-xl text-slate-500 xl:block' aria-hidden='true'>→</span></div><div class='relative flex flex-col rounded-2xl border border-emerald-400/40 bg-white/5 p-5'><span class='block h-1 w-12 rounded-full bg-emerald-400'></span><p class='mt-4 text-[0.65rem] font-semibold uppercase tracking-[0.25em] text-emerald-300'>Step 3 · PostgreSQL</p><p class='mt-2 text-lg font-bold leading-snug text-white'>Validated JSONB blocks</p><p class='mt-1 text-xs text-slate-400'>Supabase · one Zod schema per block.</p><div class='mt-4 flex-1 text-sm text-slate-200'><div class='space-y-2'><p class='flex items-start gap-2 text-sm text-slate-200'><span class='mt-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-emerald-400/20 text-[0.6rem] font-bold text-emerald-300'>&#10003;</span><span>Staged as a Live Draft.</span></p><p class='flex items-start gap-2 text-sm text-slate-200'><span class='mt-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-emerald-400/20 text-[0.6rem] font-bold text-emerald-300'>&#10003;</span><span>Editors refine in the visual CMS.</span></p><p class='flex items-start gap-2 text-sm text-slate-200'><span class='mt-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-emerald-400/20 text-[0.6rem] font-bold text-emerald-300'>&#10003;</span><span>Revisions and SEO checks built in.</span></p></div></div><p class='mt-5 rounded-lg bg-emerald-400/10 px-3 py-2 text-center text-[0.7rem] font-semibold text-emerald-300'>Nothing goes live until publish</p><span class='absolute -right-4 top-1/2 hidden -translate-y-1/2 text-xl text-slate-500 xl:block' aria-hidden='true'>→</span></div><div class='relative flex flex-col rounded-2xl border border-amber-400/40 bg-white/5 p-5'><span class='block h-1 w-12 rounded-full bg-amber-400'></span><p class='mt-4 text-[0.65rem] font-semibold uppercase tracking-[0.25em] text-amber-300'>Step 4 · Next.js 16</p><p class='mt-2 text-lg font-bold leading-snug text-white'>Published page</p><p class='mt-1 text-xs text-slate-400'>Server Components · edge cached.</p><div class='mt-4 flex-1 text-sm text-slate-200'><div class='space-y-2'><p class='flex items-start gap-2 text-sm text-slate-200'><span class='mt-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400/20 text-[0.6rem] font-bold text-amber-300'>&#10003;</span><span>No rebuild, no redeploy.</span></p><p class='flex items-start gap-2 text-sm text-slate-200'><span class='mt-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400/20 text-[0.6rem] font-bold text-amber-300'>&#10003;</span><span>100/100 Lighthouse defaults.</span></p><p class='flex items-start gap-2 text-sm text-slate-200'><span class='mt-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400/20 text-[0.6rem] font-bold text-amber-300'>&#10003;</span><span>Same site on day two.</span></p></div></div><p class='mt-5 rounded-lg bg-amber-400/10 px-3 py-2 text-center text-[0.7rem] font-semibold text-amber-300'>A website plus a CMS</p></div></div><div class='mt-6 h-1.5 rounded-full bg-gradient-to-r from-sky-400 via-violet-400 to-amber-400'></div><p class='mt-4 text-center text-sm text-slate-300'>Editors keep a visual CMS. Developers keep a clean Next.js 16 codebase. The agent never touches either.</p></div><p>The endpoint speaks Streamable HTTP. Your client posts JSON-RPC messages over a normal HTTPS request, and the server answers in the same response. There is no long-lived SSE stream to babysit and no SDK to install on the server side. The same 29 typed tools that power Cortex AI inside the editor are exposed over the wire, so the agent in your IDE and the agent in your CMS never drift apart.</p><div class='grid gap-4 md:grid-cols-2 my-8'><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Safety rule one</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Layouts stage as Live Drafts.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Layout tools write drafts, so nothing reaches visitors until an editor publishes it.</p></div><div class='rounded-3xl border border-blue-200/70 bg-blue-50/70 p-6 dark:border-blue-500/20 dark:bg-blue-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Safety rule two</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Every token carries a scope.</p><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>A read-only token never even sees a tool that could change data. Mutating tools are absent from its list.</p></div></div><h3>Available MCP Database Tools</h3><p>Five tool names form the public MCP contract. Each one forwards to a tested Cortex AI executor.</p><div class='grid gap-4 md:grid-cols-2 my-8'><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>get_database_schema</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-emerald-300 text-emerald-700 dark:border-emerald-500/40 dark:text-emerald-200'>read</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Returns every table the agent may read or change, with columns, primary keys, and read-only flags.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>generate_jsonb_layout</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-amber-300 text-amber-700 dark:border-amber-500/40 dark:text-amber-200'>write</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Turns a prompt into a full page layout, validates each block against the NextBlock schema, and stages it as a Live Draft.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>update_site_navigation</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-amber-300 text-amber-700 dark:border-amber-500/40 dark:text-amber-200'>write</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Adds, renames, or reorders header menu items per locale.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>query_site_analytics</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-emerald-300 text-emerald-700 dark:border-emerald-500/40 dark:text-emerald-200'>read</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Reads revenue, order counts, status breakdowns, and top products over a date range.</p></div><div class='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5'><div class='flex items-center justify-between gap-3'><code class='rounded-lg bg-violet-50 px-2.5 py-1 text-sm text-violet-700 dark:bg-violet-500/15 dark:text-violet-200'>search_stock_media</code><span class='rounded-full border px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] border-emerald-300 text-emerald-700 dark:border-emerald-500/40 dark:text-emerald-200'>read</span></div><p class='mt-3 mb-0 text-sm text-slate-600 dark:text-slate-300'>Finds free stock photos with alt text and photographer credits ready for an image block.</p></div><div class='rounded-3xl border border-dashed border-slate-300 p-5 dark:border-white/20'><p class='mt-0 mb-0 text-sm font-semibold text-slate-900 dark:text-white'>And 24 more.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>Create posts and products, translate pages, upload media, manage themes and scripts. Three resources expose the database, block, and custom block schemas.</p></div></div><h2>Step by Step: Connect Claude to NextBlock CMS in Cursor and Claude Code</h2><p>The server is off by default because it is a remote write surface onto live content. Turning it on takes three steps, and the trial means the first month costs nothing.</p><div class='grid gap-4 md:grid-cols-3 my-8'><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 font-mono text-3xl font-bold text-sky-500 dark:text-sky-300'>01</p><p class='mt-3 mb-0 text-lg font-semibold text-slate-900 dark:text-white'>Start the free trial.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>Open <strong>CMS Settings → Cortex AI</strong>, start the 30-day trial with no credit card, and switch on the MCP server access card.</p></div><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 font-mono text-3xl font-bold text-violet-500 dark:text-violet-300'>02</p><p class='mt-3 mb-0 text-lg font-semibold text-slate-900 dark:text-white'>Mint a token.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>Pick the scope you need. Read-only is enough for planning and audits.</p></div><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 font-mono text-3xl font-bold text-emerald-500 dark:text-emerald-300'>03</p><p class='mt-3 mb-0 text-lg font-semibold text-slate-900 dark:text-white'>Copy the config.</p><p class='mt-2 mb-0 text-sm text-slate-600 dark:text-slate-300'>The card renders one snippet each for Claude Code, Cursor, VS Code, and Claude Desktop.</p></div></div><p>Claude Code needs <code>\\\"type\\\": \\\"http\\\"</code> in the entry, or it skips the server without a warning. Cursor infers the transport from the URL and needs no type field. VS Code uses a top-level <code>servers</code> key and prompts for the token instead of storing it.</p><h3>Localhost Configuration Without a Token</h3><p>While you build, turn on <strong>Trust localhost without a token</strong> in the same settings card. A dev server on your machine then accepts loopback calls with no header at all. Standalone installs run on port 3000, and the monorepo dev server runs on port 4200.</p><p>Add this block to <code>.mcp.json</code> in your project root for Claude Code.</p><div class='my-8 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'><div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'><span class='h-3 w-3 rounded-full bg-red-400/70'></span><span class='h-3 w-3 rounded-full bg-yellow-400/70'></span><span class='h-3 w-3 rounded-full bg-green-400/70'></span><span class='ml-3 text-xs font-mono text-slate-400'>.mcp.json</span></div><pre class='m-0 overflow-x-auto rounded-none bg-transparent px-6 py-5 text-sm leading-6 text-slate-200'><code>{\\n \\\"mcpServers\\\": {\\n \\\"nextblock\\\": {\\n \\\"type\\\": \\\"http\\\",\\n \\\"url\\\": \\\"http://localhost:3000/api/mcp\\\"\\n }\\n }\\n}</code></pre></div><p>The CLI form does the same thing in one line.</p><div class='my-8 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'><div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'><span class='h-3 w-3 rounded-full bg-red-400/70'></span><span class='h-3 w-3 rounded-full bg-yellow-400/70'></span><span class='h-3 w-3 rounded-full bg-green-400/70'></span><span class='ml-3 text-xs font-mono text-slate-400'>terminal</span></div><pre class='m-0 overflow-x-auto rounded-none bg-transparent px-6 py-5 text-sm leading-6 text-slate-200'><code>claude mcp add --transport http nextblock http://localhost:3000/api/mcp</code></pre></div><div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Development only</p><p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Localhost trust is ignored in production builds, because a proxy can spoof the host header. Production always uses a token.</p></div><h3>Production Authentication via Bearer Tokens</h3><p>In production every client sends a bearer token. Tokens start with <code>nbmcp_</code>, are shown once at mint time, and are stored only as SHA-256 hashes. Revoking one is a single click, and the same value can never be minted again.</p><p>Add this to <code>.cursor/mcp.json</code> for Cursor, or drop the same entry into <code>.mcp.json</code> with a type field for Claude Code.</p><div class='my-8 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'><div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'><span class='h-3 w-3 rounded-full bg-red-400/70'></span><span class='h-3 w-3 rounded-full bg-yellow-400/70'></span><span class='h-3 w-3 rounded-full bg-green-400/70'></span><span class='ml-3 text-xs font-mono text-slate-400'>.cursor/mcp.json</span></div><pre class='m-0 overflow-x-auto rounded-none bg-transparent px-6 py-5 text-sm leading-6 text-slate-200'><code>{\\n \\\"mcpServers\\\": {\\n \\\"nextblock\\\": {\\n \\\"url\\\": \\\"https://your-site.com/api/mcp\\\",\\n \\\"headers\\\": { \\\"Authorization\\\": \\\"Bearer nbmcp_your_token\\\" }\\n }\\n }\\n}</code></pre></div><div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Try it</p><p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Once the client connects, ask for something concrete. Try &ldquo;inspect the database schema and draft a pricing page with three tiers.&rdquo; The agent calls the schema tool, then the layout tool, and your CMS shows a new Live Draft ready for review.</p></div><h2>Zero-Redeploy Production Rendering</h2><p>The reason this works is where the output lands. Cortex AI writes strict JSONB block records into PostgreSQL, not HTML strings and not source files. Each block has a Zod schema, so a bad field is rejected before it is stored.</p><div class='grid gap-4 md:grid-cols-3 my-8'><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 text-center dark:border-sky-500/20 dark:bg-sky-500/10'><p class='mt-0 mb-0 text-4xl font-extrabold text-slate-900 dark:text-white'>0</p><p class='mt-2 mb-0 text-xs font-semibold uppercase tracking-[0.2em] text-sky-700 dark:text-sky-200'>Rebuilds to publish</p></div><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 text-center dark:border-violet-500/20 dark:bg-violet-500/10'><p class='mt-0 mb-0 text-4xl font-extrabold text-slate-900 dark:text-white'>1</p><p class='mt-2 mb-0 text-xs font-semibold uppercase tracking-[0.2em] text-violet-700 dark:text-violet-200'>Zod schema per block</p></div><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 text-center dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='mt-0 mb-0 text-4xl font-extrabold text-slate-900 dark:text-white'>100</p><p class='mt-2 mb-0 text-xs font-semibold uppercase tracking-[0.2em] text-emerald-700 dark:text-emerald-200'>Lighthouse by default</p></div></div><p>At request time Next.js 16 renders those records with Server Components and caches the result at the edge. There is no build step between publish and live, and no HTML sanitizer tax on every render. The 100/100 Lighthouse defaults you get on an empty site are the same ones you get after the agent has drafted fifty pages.</p><div class='rounded-3xl border border-slate-200 bg-slate-50 p-6 my-10 dark:border-white/10 dark:bg-white/5'><p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300'>Next step</p><p class='mt-3 mb-0 text-xl font-semibold text-slate-900 dark:text-white'>Up in ten minutes, free for thirty days.</p><p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Deploy the free CMS to Vercel in one click, start the Cortex AI trial with no credit card, and connect Claude to NextBlock CMS today. Read how the block registry keeps all of it safe if you want the full picture.</p><div class='mt-5 flex flex-wrap gap-3'><a href='https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Start the 30-day free trial</a><a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Deploy to Vercel</a><a href='/article/how-nextblock-works' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>How NextBlock works</a></div></div>\"}$nbmcpguide3$::jsonb, updated_at = now()\n WHERE post_id = v_post AND block_type = 'text' AND \"order\" = 0\n AND content->>'html_content' LIKE '%A codebase to babysit.%';\n END IF;\n\n DELETE FROM public.media m\n WHERE m.object_key = 'images/mcp-prompt-to-production.webp'\n AND NOT EXISTS (SELECT 1 FROM public.blocks b WHERE b.content::text LIKE '%mcp-prompt-to-production.webp%')\n AND NOT EXISTS (SELECT 1 FROM public.posts p WHERE p.feature_image_id = m.id)\n AND NOT EXISTS (SELECT 1 FROM public.pages p WHERE p.feature_image_id = m.id)\n AND NOT EXISTS (SELECT 1 FROM public.product_media pm WHERE pm.media_id = m.id);\n\n ---------------------------------------------------------------------------\n -- 4. Articles pages: short hero, essay moves below the grid (EN + FR)\n ---------------------------------------------------------------------------\n SELECT id INTO v_articles_en FROM public.pages WHERE slug = 'articles' AND language_id = v_en ORDER BY id LIMIT 1;\n SELECT id INTO v_articles_fr FROM public.pages WHERE slug = 'articles' AND language_id = v_fr ORDER BY id LIMIT 1;\n\n IF v_articles_en IS NOT NULL THEN\n UPDATE public.blocks SET content = $nbjhero_en${\"is_hero\":true,\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#1e293b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"lg\",\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-sm uppercase tracking-[0.3em] text-blue-400 font-bold text-center md:text-left mb-4'>The NextBlock Journal</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h1 class='text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6'>The NextBlock Journal: Performance, DX, and Visual Editing</h1>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed'>Practical guides, architecture deep dives, and editor workflows from the NextBlock team. Pick a topic below or jump straight to the latest posts.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/articles#latest\",\"size\":\"lg\",\"text\":\"Explore Articles\",\"variant\":\"default\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://github.com/nextblock-cms/nextblock/discussions\",\"size\":\"lg\",\"text\":\"Subscribe for Updates\",\"variant\":\"outline\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full flex items-center justify-center rounded-3xl overflow-hidden border border-white/10 bg-white/5 shadow-2xl p-4 backdrop-blur-sm'><img src='/images/developer.webp' alt='Developer working with the Nextblock stack' class='w-full object-cover rounded-2xl shadow-lg' style='max-width: 400px;' /></div>\"}}]]}$nbjhero_en$::jsonb, updated_at = now()\n WHERE page_id = v_articles_en AND block_type = 'section' AND content::text LIKE '%Welcome to the NextBlock Journal.%';\n\n IF NOT EXISTS (SELECT 1 FROM public.blocks WHERE page_id = v_articles_en AND content::text LIKE '%What the NextBlock Journal Covers%') THEN\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_articles_en, v_en, 'section', $nbjsec_en${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"none\"},\"column_gap\":\"lg\",\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"text_content\":\"What the NextBlock Journal Covers\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-lg text-muted-foreground text-center max-w-3xl mx-auto mb-10'>Every post is tested on a live site before it is published, and each one includes the full technical reasoning and diagrams. Start with the topic that matches where you are.</p><div class='grid gap-4 md:grid-cols-2'><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200'>Getting started</p><h3 class='mt-3 text-xl font-semibold text-foreground'>Setup, every way</h3><p class='mt-3 text-sm text-muted-foreground'>Walkthroughs for one-click Vercel deploys, local Docker environments, and custom cloud stacks. Each guide gives you clear, tested steps, so a fresh site is live in minutes rather than days.</p><a href='/article/how-to-setup-nextblock' class='mt-4 inline-flex text-sm font-semibold text-sky-700 no-underline hover:underline dark:text-sky-200'>Read the setup guide →</a></div><div class='rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200'>Architecture and performance</p><h3 class='mt-3 text-xl font-semibold text-foreground'>How the engine works</h3><p class='mt-3 text-sm text-muted-foreground'>Deep dives into fast page rendering, clean CSS delivery, image optimization, and safe database migrations. Learn how NextBlock keeps a 100% Lighthouse score while giving writers a rich block editor.</p><a href='/article/how-nextblock-works' class='mt-4 inline-flex text-sm font-semibold text-indigo-700 no-underline hover:underline dark:text-indigo-200'>Look under the hood →</a></div><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>E-commerce workflows</p><h3 class='mt-3 text-xl font-semibold text-foreground'>From catalog to checkout</h3><p class='mt-3 text-sm text-muted-foreground'>Practical patterns for multi-currency pricing, automated tax sync, stock tracking, and provider-aware checkout with Stripe and Freemius, all managed from the same block editor.</p><a href='/article/nextblock-commerce-guide' class='mt-4 inline-flex text-sm font-semibold text-emerald-700 no-underline hover:underline dark:text-emerald-200'>Read the commerce guide →</a></div><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Cortex AI and MCP</p><h3 class='mt-3 text-xl font-semibold text-foreground'>AI that writes blocks, not code</h3><p class='mt-3 text-sm text-muted-foreground'>How to generate structured layouts with Cortex AI, connect Claude Code or Cursor to your site over MCP, and speed up translation work. The CMS is free, and Cortex AI starts with a 30-day trial and no credit card.</p><a href='/article/cortex-ai-mcp-connection-guide' class='mt-4 inline-flex text-sm font-semibold text-violet-700 no-underline hover:underline dark:text-violet-200'>Connect your AI →</a></div></div><p class='mt-8 text-center text-base text-foreground max-w-2xl mx-auto'>Every guide reads in under ten minutes. Code samples come from sites already in production, and every screenshot comes from the real CMS interface, so what you see here is what you get.</p><p class='mt-6 text-center text-sm text-muted-foreground max-w-2xl mx-auto'>Have a topic you want covered, or a case study to share? Join the community on <a href='https://github.com/nextblock-cms/nextblock/discussions' class='font-semibold text-foreground underline'>GitHub Discussions</a>. We welcome questions, ideas, and pull requests from all builders, and we update this collection with every release.</p>\"}}]]}$nbjsec_en$::jsonb,\n (SELECT COALESCE(MAX(\"order\"), 0) + 1 FROM public.blocks WHERE page_id = v_articles_en));\n END IF;\n END IF;\n\n IF v_articles_fr IS NOT NULL THEN\n UPDATE public.blocks SET content = $nbjhero_fr${\"is_hero\":true,\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#1e293b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"lg\",\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-sm uppercase tracking-[0.3em] text-blue-400 font-bold text-center md:text-left mb-4'>Le journal Nextblock</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h1 class='text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6'>Le Journal NextBlock : Performance, Expérience Dev et Édition</h1>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed'>Guides pratiques, dossiers d'architecture et conseils d'édition par l'équipe NextBlock. Choisissez un thème plus bas, ou allez droit aux derniers articles.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"/articles#latest\",\"size\":\"lg\",\"text\":\"Explorer les articles\",\"variant\":\"default\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://github.com/nextblock-cms/nextblock/discussions\",\"size\":\"lg\",\"text\":\"S'abonner aux mises à jour\",\"variant\":\"outline\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='h-full flex items-center justify-center rounded-3xl overflow-hidden border border-white/10 bg-white/5 shadow-2xl p-4 backdrop-blur-sm'><img src='/images/developer.webp' alt='Développeur travaillant avec la stack Nextblock' class='w-full object-cover rounded-2xl shadow-lg' style='max-width: 400px;' /></div>\"}}]]}$nbjhero_fr$::jsonb, updated_at = now()\n WHERE page_id = v_articles_fr AND block_type = 'section' AND content::text LIKE '%Bienvenue sur le Journal NextBlock.%';\n\n IF NOT EXISTS (SELECT 1 FROM public.blocks WHERE page_id = v_articles_fr AND content::text LIKE '%Ce que couvre le Journal NextBlock%') THEN\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_articles_fr, v_fr, 'section', $nbjsec_fr${\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"none\"},\"column_gap\":\"lg\",\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"text_content\":\"Ce que couvre le Journal NextBlock\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class='text-lg text-muted-foreground text-center max-w-3xl mx-auto mb-10'>Chaque article est testé sur un vrai site avant sa sortie. Il donne le raisonnement complet et des schémas clairs. Commencez par le thème qui vous parle.</p><div class='grid gap-4 md:grid-cols-2'><div class='rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200'>Premiers pas</p><h3 class='mt-3 text-xl font-semibold text-foreground'>Installer NextBlock, à votre façon</h3><p class='mt-3 text-sm text-muted-foreground'>Des guides pas à pas pour un déploiement Vercel en un clic, un Docker local ou votre propre cloud. Chaque étape est vérifiée. Votre site est en ligne en quelques minutes, pas en quelques jours.</p><a href='/article/comment-configurer-nextblock' class='mt-4 inline-flex text-sm font-semibold text-sky-700 no-underline hover:underline dark:text-sky-200'>Lire le guide d'installation →</a></div><div class='rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200'>Architecture et performance</p><h3 class='mt-3 text-xl font-semibold text-foreground'>Comment marche le moteur</h3><p class='mt-3 text-sm text-muted-foreground'>Des dossiers sur le rendu rapide des pages, la livraison du CSS, les images et les migrations sûres. Vous verrez comment NextBlock garde un score Lighthouse parfait, avec un vrai éditeur de blocs pour vos auteurs.</p><a href='/article/comment-nextblock-fonctionne' class='mt-4 inline-flex text-sm font-semibold text-indigo-700 no-underline hover:underline dark:text-indigo-200'>Regarder sous le capot →</a></div><div class='rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Commerce en ligne</p><h3 class='mt-3 text-xl font-semibold text-foreground'>Du catalogue au paiement</h3><p class='mt-3 text-sm text-muted-foreground'>Des méthodes concrètes pour les prix en plusieurs devises, les taxes automatiques, les stocks et le paiement avec Stripe et Freemius. Tout se gère depuis le même éditeur de blocs.</p><a href='/article/guide-commerce-nextblock' class='mt-4 inline-flex text-sm font-semibold text-emerald-700 no-underline hover:underline dark:text-emerald-200'>Lire le guide commerce →</a></div><div class='rounded-3xl border border-violet-200/70 bg-violet-50/70 p-6 dark:border-violet-500/20 dark:bg-violet-500/10'><p class='text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Cortex AI et MCP</p><h3 class='mt-3 text-xl font-semibold text-foreground'>Une IA qui écrit des blocs, pas du code</h3><p class='mt-3 text-sm text-muted-foreground'>Créez des mises en page avec Cortex AI. Reliez Claude Code ou Cursor à votre site via MCP. Traduisez plus vite. Le CMS est gratuit, et Cortex AI offre un essai de 30 jours sans carte bancaire.</p><a href='/article/cortex-ai-mcp-connection-guide' class='mt-4 inline-flex text-sm font-semibold text-violet-700 no-underline hover:underline dark:text-violet-200'>Connecter votre IA →</a></div></div><p class='mt-8 text-center text-base text-foreground max-w-2xl mx-auto'>Chaque guide se lit en moins de dix minutes. Les exemples de code viennent de sites déjà en production, et chaque capture vient de la vraie interface du CMS. Ce que vous lisez ici est ce que vous obtenez.</p><p class='mt-6 text-center text-sm text-muted-foreground max-w-2xl mx-auto'>Un sujet à proposer, ou une étude de cas à partager ? Rejoignez la communauté sur <a href='https://github.com/nextblock-cms/nextblock/discussions' class='font-semibold text-foreground underline'>GitHub Discussions</a>. Vos questions, idées et contributions sont les bienvenues. Cette collection grandit à chaque version.</p>\"}}]]}$nbjsec_fr$::jsonb,\n (SELECT COALESCE(MAX(\"order\"), 0) + 1 FROM public.blocks WHERE page_id = v_articles_fr));\n END IF;\n END IF;\nEND\n$body$;\n"
226
+ },
227
+ {
228
+ "version": "00000000000042",
229
+ "name": "00000000000042_how_nextblock_works_css_diagram.sql",
230
+ "sql": "-- 00000000000042_how_nextblock_works_css_diagram.sql\n--\n-- The \"How NextBlock™ Works\" article (EN 'how-nextblock-works', FR\n-- 'comment-nextblock-fonctionne') illustrated the platform with a rasterised\n-- figure (public/images/extensibility.webp). This replaces that <figure> with a\n-- CSS/HTML architecture diagram inside the same rich-text block: the NB platform\n-- core (site logo) with four spokes, the \"unified architecture\" band with three\n-- panels (content modeling, live editor and publishing, premium modules), the\n-- stack strip and the tagline. Same visual story, but crisp at every size,\n-- theme-aware and editable in the CMS.\n--\n-- The image file and its media row stay: the sandbox reset route registers it as\n-- a core media record and nothing else changes. Only the two article bodies do.\n--\n-- Idempotent: replace() is a no-op once the figure is gone, and the row is only\n-- matched while the exact seeded figure is still present, so an editor's rewrite\n-- of the article is left alone. Both posts still grade 100/100 in\n-- libs/utils/src/lib/seo (the logo <img> carries alt text).\n--\n-- After adding this file: npm run generate:migrations-bundle && npm run\n-- generate:sandbox && npm run sync:create-nextblock.\n\nDO $body$\nDECLARE\n v_en integer;\n v_fr integer;\n v_post integer;\nBEGIN\n SELECT id INTO v_en FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n SELECT id INTO v_post FROM public.posts WHERE slug = 'how-nextblock-works' AND language_id = v_en ORDER BY id LIMIT 1;\n IF v_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb(replace(content->>'html_content', $nbfig_en$<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\n <img src='/images/extensibility.webp' alt='NextBlock™ extensibility artwork showing the CMS connected to reusable modules and integrations' class='w-full h-auto object-cover' />\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>One unified design system spans content blocks, editing tools, and store features.</figcaption>\n</figure>$nbfig_en$, $nbdiag_en$<div class='not-prose my-12 overflow-hidden rounded-[2rem] border border-slate-800 shadow-2xl'><div class='bg-gradient-to-b from-[#020817] via-[#0b1a3a] to-[#0f172a] p-6 sm:p-8'><p class='text-center text-[0.65rem] font-semibold uppercase tracking-[0.3em] text-cyan-300'>NB platform core</p><div class='mx-auto mt-4 flex h-28 w-28 items-center justify-center rounded-full border border-cyan-400/40 bg-cyan-400/10 shadow-[0_0_70px_rgba(34,211,238,0.28)]'><img src='/images/nextblock-logo-small.webp' alt='NextBlock logo at the centre of the platform core' class='h-16 w-16 object-contain' /></div><p class='mt-3 text-center text-sm font-bold text-white'>One Next.js 16 app on Supabase</p><div class='mx-auto h-8 w-px bg-cyan-400/40'></div><div class='relative'><div class='absolute left-[12.5%] right-[12.5%] top-0 h-px bg-cyan-400/30'></div><div class='grid grid-cols-2 gap-3 md:grid-cols-4'><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M3 4h2l2.4 11.2a1 1 0 0 0 1 .8h9.2a1 1 0 0 0 1-.8L20 8H6.2M9 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>Commerce</span></div></div><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M13 2 4 14h7l-1 8 9-12h-7l1-8z'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>Performance</span></div></div><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M4 20V11M10 20V4M16 20v-7M2 20h20'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>Analytics</span></div></div><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>API & integrations</span></div></div></div></div><div class='mt-8 flex items-center gap-3'><span class='h-px flex-1 bg-cyan-400/30'></span><span class='rounded-full border border-cyan-400/40 bg-slate-950 px-4 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.3em] text-cyan-200'>Unified architecture</span><span class='h-px flex-1 bg-cyan-400/30'></span></div><div class='mt-4 grid gap-4 md:grid-cols-3'><div class='flex flex-col rounded-2xl border border-cyan-400/20 bg-slate-950/70 p-4'><p class='text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-cyan-300'>CMS & content modeling</p><div class='mt-3 flex-1'><div class='flex items-center justify-center gap-2'><div class='space-y-1.5'><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>page</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>post</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>product</span></div><span class='h-px w-4 bg-cyan-400/50'></span><div class='rounded-xl border border-cyan-400/40 bg-cyan-400/10 px-3 py-2 text-center'><p class='text-[0.55rem] uppercase tracking-[0.2em] text-cyan-300'>PostgreSQL</p><p class='text-xs font-semibold text-white'>JSONB blocks</p></div><span class='h-px w-4 bg-cyan-400/50'></span><div class='space-y-1.5'><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>block</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>revision</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>draft</span></div></div><p class='mt-3 text-xs text-slate-400'>Every page, post, and product is an ordered list of typed blocks. One Zod schema per block keeps the data valid.</p></div></div><div class='flex flex-col rounded-2xl border border-cyan-400/20 bg-slate-950/70 p-4'><p class='text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-cyan-300'>Live editor & publishing</p><div class='mt-3 flex-1'><div class='overflow-hidden rounded-xl border border-slate-700 bg-slate-900'><div class='flex items-center gap-1.5 border-b border-white/10 px-3 py-2'><span class='h-2 w-2 rounded-full bg-red-400/70'></span><span class='h-2 w-2 rounded-full bg-yellow-400/70'></span><span class='h-2 w-2 rounded-full bg-green-400/70'></span><span class='ml-2 truncate font-mono text-[0.6rem] text-slate-400'>cms/pages/home</span></div><div class='flex gap-2 p-3'><div class='w-10 space-y-1.5'><span class='block h-1.5 rounded-full bg-slate-700 w-full'></span><span class='block h-1.5 rounded-full bg-slate-700 w-3/4'></span><span class='block h-1.5 rounded-full bg-slate-700 w-full'></span><span class='block h-1.5 rounded-full bg-slate-700 w-1/2'></span></div><div class='flex-1 rounded-lg border border-dashed border-cyan-400/40 bg-cyan-400/5 p-2'><p class='text-[0.6rem] font-semibold text-cyan-200'>Live preview</p><div class='mt-1.5 space-y-1.5'><span class='block h-1.5 rounded-full bg-slate-600 w-11/12'></span><span class='block h-1.5 rounded-full bg-slate-600 w-2/3'></span><span class='block h-1.5 rounded-full bg-slate-600 w-4/5'></span></div></div></div><div class='flex items-center justify-between border-t border-white/10 px-3 py-2'><span class='rounded-full border border-amber-400/40 bg-amber-400/10 px-2 py-0.5 text-[0.6rem] font-semibold text-amber-200'>Live Draft</span><span class='rounded-md bg-cyan-400 px-2.5 py-1 text-[0.6rem] font-bold text-slate-950'>Publish</span></div></div><p class='mt-3 text-xs text-slate-400'>Editors work in a Notion-style editor with drafts and revisions. Publishing reads straight from PostgreSQL, so there is no redeploy.</p></div></div><div class='flex flex-col rounded-2xl border border-cyan-400/20 bg-slate-950/70 p-4'><p class='text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-cyan-300'>Premium modules</p><div class='mt-3 flex-1'><div class='flex items-center gap-2'><span class='shrink-0 rounded-md border border-cyan-400/40 bg-cyan-400/10 px-2 py-1 text-[0.6rem] font-semibold uppercase tracking-[0.15em] text-cyan-200'>Add-on</span><span class='h-px w-4 bg-cyan-400/50'></span><div class='flex-1 space-y-2'><div class='flex items-center gap-2 rounded-xl border border-emerald-400/40 bg-emerald-400/10 px-3 py-2'><span class='text-emerald-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M3 4h2l2.4 11.2a1 1 0 0 0 1 .8h9.2a1 1 0 0 0 1-.8L20 8H6.2M9 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/></svg></span><div><p class='text-xs font-semibold text-white'>Commerce Pro</p><p class='text-[0.6rem] text-slate-400'>Products, checkout, taxes, shipping.</p></div></div><div class='flex items-center gap-2 rounded-xl border border-violet-400/40 bg-violet-400/10 px-3 py-2'><span class='text-violet-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3zM19 16l.8 2.2L22 19l-2.2.8L19 22l-.8-2.2L16 19l2.2-.8L19 16z'/></svg></span><div><p class='text-xs font-semibold text-white'>Cortex AI</p><p class='text-[0.6rem] text-slate-400'>AI in the editor + MCP server. 30-day free trial.</p></div></div></div></div><p class='mt-3 text-xs text-slate-400'>The core CMS is free and open source. Modules switch on with a license and use the same blocks and editor.</p></div></div></div><div class='mt-6 flex flex-wrap items-center justify-center gap-2'><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Next.js 16</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Supabase</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Tiptap</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Tailwind</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Nx</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Git</span></div><p class='mt-6 rounded-2xl border border-cyan-400/30 bg-slate-950/80 px-4 py-4 text-center text-base font-bold uppercase tracking-[0.12em] text-white sm:text-lg'>One visual system. CMS, editor, and unlimited extensibility.</p></div></div>$nbdiag_en$))),\n updated_at = now()\n WHERE post_id = v_post\n AND block_type = 'text'\n AND content->>'html_content' LIKE '%extensibility.webp%'\n AND position($nbfig_eng$<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\n <img src='/images/extensibility.webp' alt='NextBlock™ extensibility artwork showing the CMS connected to reusable modules and integrations' class='w-full h-auto object-cover' />\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>One unified design system spans content blocks, editing tools, and store features.</figcaption>\n</figure>$nbfig_eng$ in content->>'html_content') > 0;\n END IF;\n\n SELECT id INTO v_post FROM public.posts WHERE slug = 'comment-nextblock-fonctionne' AND language_id = v_fr ORDER BY id LIMIT 1;\n IF v_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb(replace(content->>'html_content', $nbfig_fr$<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\n <img src='/images/extensibility.webp' alt='Illustration NextBlock montrant les connexions entre le CMS et les modules externes' class='w-full h-auto object-cover' />\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Un même système visuel réunit la gestion de contenu, l'édition et les modules du store.</figcaption>\n</figure>$nbfig_fr$, $nbdiag_fr$<div class='not-prose my-12 overflow-hidden rounded-[2rem] border border-slate-800 shadow-2xl'><div class='bg-gradient-to-b from-[#020817] via-[#0b1a3a] to-[#0f172a] p-6 sm:p-8'><p class='text-center text-[0.65rem] font-semibold uppercase tracking-[0.3em] text-cyan-300'>Cœur de la plateforme NB</p><div class='mx-auto mt-4 flex h-28 w-28 items-center justify-center rounded-full border border-cyan-400/40 bg-cyan-400/10 shadow-[0_0_70px_rgba(34,211,238,0.28)]'><img src='/images/nextblock-logo-small.webp' alt='Logo NextBlock au centre du cœur de la plateforme' class='h-16 w-16 object-contain' /></div><p class='mt-3 text-center text-sm font-bold text-white'>Une seule app Next.js 16 sur Supabase</p><div class='mx-auto h-8 w-px bg-cyan-400/40'></div><div class='relative'><div class='absolute left-[12.5%] right-[12.5%] top-0 h-px bg-cyan-400/30'></div><div class='grid grid-cols-2 gap-3 md:grid-cols-4'><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M3 4h2l2.4 11.2a1 1 0 0 0 1 .8h9.2a1 1 0 0 0 1-.8L20 8H6.2M9 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>Commerce</span></div></div><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M13 2 4 14h7l-1 8 9-12h-7l1-8z'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>Performance</span></div></div><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M4 20V11M10 20V4M16 20v-7M2 20h20'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>Analytique</span></div></div><div class='flex flex-col items-center'><div class='h-6 w-px bg-cyan-400/40'></div><div class='flex w-full flex-col items-center rounded-2xl border border-cyan-400/30 bg-white/5 px-3 py-4 text-center'><span class='flex h-11 w-11 items-center justify-center rounded-xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1'/></svg></span><span class='mt-3 text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-slate-100'>API et intégrations</span></div></div></div></div><div class='mt-8 flex items-center gap-3'><span class='h-px flex-1 bg-cyan-400/30'></span><span class='rounded-full border border-cyan-400/40 bg-slate-950 px-4 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.3em] text-cyan-200'>Architecture unifiée</span><span class='h-px flex-1 bg-cyan-400/30'></span></div><div class='mt-4 grid gap-4 md:grid-cols-3'><div class='flex flex-col rounded-2xl border border-cyan-400/20 bg-slate-950/70 p-4'><p class='text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-cyan-300'>CMS et modélisation du contenu</p><div class='mt-3 flex-1'><div class='flex items-center justify-center gap-2'><div class='space-y-1.5'><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>page</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>article</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>produit</span></div><span class='h-px w-4 bg-cyan-400/50'></span><div class='rounded-xl border border-cyan-400/40 bg-cyan-400/10 px-3 py-2 text-center'><p class='text-[0.55rem] uppercase tracking-[0.2em] text-cyan-300'>PostgreSQL</p><p class='text-xs font-semibold text-white'>Blocs JSONB</p></div><span class='h-px w-4 bg-cyan-400/50'></span><div class='space-y-1.5'><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>bloc</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>révision</span><span class='block rounded-md border border-slate-600/70 bg-slate-900 px-2.5 py-1 text-center font-mono text-[0.65rem] text-slate-200'>brouillon</span></div></div><p class='mt-3 text-xs text-slate-400'>Chaque page, article et produit est une liste de blocs typés. Un schéma Zod par bloc garde les données valides.</p></div></div><div class='flex flex-col rounded-2xl border border-cyan-400/20 bg-slate-950/70 p-4'><p class='text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-cyan-300'>Éditeur en direct et publication</p><div class='mt-3 flex-1'><div class='overflow-hidden rounded-xl border border-slate-700 bg-slate-900'><div class='flex items-center gap-1.5 border-b border-white/10 px-3 py-2'><span class='h-2 w-2 rounded-full bg-red-400/70'></span><span class='h-2 w-2 rounded-full bg-yellow-400/70'></span><span class='h-2 w-2 rounded-full bg-green-400/70'></span><span class='ml-2 truncate font-mono text-[0.6rem] text-slate-400'>cms/pages/accueil</span></div><div class='flex gap-2 p-3'><div class='w-10 space-y-1.5'><span class='block h-1.5 rounded-full bg-slate-700 w-full'></span><span class='block h-1.5 rounded-full bg-slate-700 w-3/4'></span><span class='block h-1.5 rounded-full bg-slate-700 w-full'></span><span class='block h-1.5 rounded-full bg-slate-700 w-1/2'></span></div><div class='flex-1 rounded-lg border border-dashed border-cyan-400/40 bg-cyan-400/5 p-2'><p class='text-[0.6rem] font-semibold text-cyan-200'>Aperçu en direct</p><div class='mt-1.5 space-y-1.5'><span class='block h-1.5 rounded-full bg-slate-600 w-11/12'></span><span class='block h-1.5 rounded-full bg-slate-600 w-2/3'></span><span class='block h-1.5 rounded-full bg-slate-600 w-4/5'></span></div></div></div><div class='flex items-center justify-between border-t border-white/10 px-3 py-2'><span class='rounded-full border border-amber-400/40 bg-amber-400/10 px-2 py-0.5 text-[0.6rem] font-semibold text-amber-200'>Brouillon</span><span class='rounded-md bg-cyan-400 px-2.5 py-1 text-[0.6rem] font-bold text-slate-950'>Publier</span></div></div><p class='mt-3 text-xs text-slate-400'>Les éditeurs écrivent dans un éditeur de type Notion, avec brouillons et révisions. La mise en ligne lit PostgreSQL en direct. Pas de build, pas d'attente.</p></div></div><div class='flex flex-col rounded-2xl border border-cyan-400/20 bg-slate-950/70 p-4'><p class='text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-cyan-300'>Modules premium</p><div class='mt-3 flex-1'><div class='flex items-center gap-2'><span class='shrink-0 rounded-md border border-cyan-400/40 bg-cyan-400/10 px-2 py-1 text-[0.6rem] font-semibold uppercase tracking-[0.15em] text-cyan-200'>Module</span><span class='h-px w-4 bg-cyan-400/50'></span><div class='flex-1 space-y-2'><div class='flex items-center gap-2 rounded-xl border border-emerald-400/40 bg-emerald-400/10 px-3 py-2'><span class='text-emerald-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M3 4h2l2.4 11.2a1 1 0 0 0 1 .8h9.2a1 1 0 0 0 1-.8L20 8H6.2M9 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/></svg></span><div><p class='text-xs font-semibold text-white'>Commerce Pro</p><p class='text-[0.6rem] text-slate-400'>Produits, paiement, taxes, livraison.</p></div></div><div class='flex items-center gap-2 rounded-xl border border-violet-400/40 bg-violet-400/10 px-3 py-2'><span class='text-violet-300'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' class='h-6 w-6' aria-hidden='true'><path d='M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3zM19 16l.8 2.2L22 19l-2.2.8L19 22l-.8-2.2L16 19l2.2-.8L19 16z'/></svg></span><div><p class='text-xs font-semibold text-white'>Cortex AI</p><p class='text-[0.6rem] text-slate-400'>IA dans l'éditeur + serveur MCP. Essai gratuit de 30 jours.</p></div></div></div></div><p class='mt-3 text-xs text-slate-400'>Le cœur du CMS est gratuit et open source. Les modules s'activent avec une licence. Ils utilisent les mêmes blocs et le même éditeur.</p></div></div></div><div class='mt-6 flex flex-wrap items-center justify-center gap-2'><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Next.js 16</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Supabase</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Tiptap</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Tailwind</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Nx</span><span class='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.15em] text-slate-300'>Git</span></div><p class='mt-6 rounded-2xl border border-cyan-400/30 bg-slate-950/80 px-4 py-4 text-center text-base font-bold uppercase tracking-[0.12em] text-white sm:text-lg'>Un seul système visuel. CMS, éditeur et extensibilité sans limite.</p></div></div>$nbdiag_fr$))),\n updated_at = now()\n WHERE post_id = v_post\n AND block_type = 'text'\n AND content->>'html_content' LIKE '%extensibility.webp%'\n AND position($nbfig_frg$<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\n <img src='/images/extensibility.webp' alt='Illustration NextBlock montrant les connexions entre le CMS et les modules externes' class='w-full h-auto object-cover' />\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Un même système visuel réunit la gestion de contenu, l'édition et les modules du store.</figcaption>\n</figure>$nbfig_frg$ in content->>'html_content') > 0;\n END IF;\nEND\n$body$;\n"
201
231
  }
202
232
  ];