@nextblock-cms/db 0.11.3 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/supabase/server.d.ts +9 -0
  2. package/lib/supabase/types.d.ts +9 -0
  3. package/package.json +1 -1
  4. package/supabase/migrations/00000000000000_baseline_schema.sql +2454 -0
  5. package/supabase/migrations/00000000000001_baseline_constraints_and_indexes.sql +1071 -0
  6. package/supabase/migrations/00000000000002_baseline_security_and_grants.sql +1335 -0
  7. package/supabase/migrations/00000000000003_baseline_seed.sql +468 -0
  8. package/supabase/migrations/00000000000004_default_logo_email_safe.sql +24 -0
  9. package/supabase/migrations/00000000000005_add_custom_canonical.sql +13 -0
  10. package/supabase/migrations/00000000000006_home_live_demo_promo.sql +67 -0
  11. package/supabase/migrations/00000000000007_home_live_demo_promo_copy_fix.sql +49 -0
  12. package/supabase/templates/confirmation.html +1 -1
  13. package/supabase/templates/email_change.html +1 -1
  14. package/supabase/templates/invite.html +1 -1
  15. package/supabase/templates/magic_link.html +1 -1
  16. package/supabase/templates/reauthentication.html +1 -1
  17. package/supabase/templates/recovery.html +1 -1
  18. package/supabase/migrations/00000000000000_setup_foundation_and_enums.sql +0 -40
  19. package/supabase/migrations/00000000000001_setup_cms_core.sql +0 -124
  20. package/supabase/migrations/00000000000002_setup_content_tables.sql +0 -138
  21. package/supabase/migrations/00000000000003_setup_catalog_and_licensing.sql +0 -160
  22. package/supabase/migrations/00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql +0 -797
  23. package/supabase/migrations/00000000000005_setup_functions_and_triggers.sql +0 -767
  24. package/supabase/migrations/00000000000006_setup_rls_and_grants.sql +0 -872
  25. package/supabase/migrations/00000000000007_setup_indexes.sql +0 -158
  26. package/supabase/migrations/00000000000008_seed_platform_defaults.sql +0 -155
  27. package/supabase/migrations/00000000000009_seed_translations.sql +0 -801
  28. package/supabase/migrations/00000000000010_seed_content_scaffold.sql +0 -1088
  29. package/supabase/migrations/00000000000011_setup_cortex_ai_settings.sql +0 -81
  30. package/supabase/migrations/00000000000012_setup_commerce_coupons.sql +0 -275
  31. package/supabase/migrations/00000000000013_setup_cortex_ai_db_mutation_audit.sql +0 -47
  32. package/supabase/migrations/00000000000014_setup_content_drafts.sql +0 -119
  33. package/supabase/migrations/00000000000015_setup_product_drafts.sql +0 -97
  34. package/supabase/migrations/00000000000016_add_feature_image_to_pages.sql +0 -8
  35. package/supabase/migrations/00000000000017_add_product_blocks.sql +0 -118
  36. package/supabase/migrations/00000000000018_setup_bot_protection_settings.sql +0 -77
  37. package/supabase/migrations/00000000000019_add_product_categories.sql +0 -64
  38. package/supabase/migrations/00000000000020_add_category_translations.sql +0 -10
  39. package/supabase/migrations/00000000000021_migrate_hero_blocks_to_sections.sql +0 -29
  40. package/supabase/migrations/00000000000022_seed_cortex_ai_guide_post.sql +0 -184
  41. package/supabase/migrations/00000000000023_setup_custom_block_definitions.sql +0 -183
  42. package/supabase/migrations/00000000000024_setup_ucp_cart_sessions.sql +0 -72
  43. package/supabase/migrations/00000000000025_add_sale_schedule_columns.sql +0 -406
  44. package/supabase/migrations/00000000000026_seed_on_sale_translation.sql +0 -9
  45. package/supabase/migrations/00000000000027_setup_privacy_and_mfa.sql +0 -479
  46. package/supabase/migrations/00000000000028_clear_advisor_warnings.sql +0 -93
  47. package/supabase/migrations/00000000000029_refresh_setup_article.sql +0 -252
  48. package/supabase/migrations/00000000000030_setup_system_configuration.sql +0 -80
  49. package/supabase/migrations/00000000000031_seed_footer_navigation.sql +0 -85
  50. package/supabase/migrations/00000000000032_neutralize_seeded_contact_emails.sql +0 -34
  51. package/supabase/migrations/00000000000033_setup_config_settings.sql +0 -99
  52. package/supabase/migrations/00000000000034_enable_staff_2fa_reminder_default.sql +0 -15
  53. package/supabase/migrations/00000000000035_reassert_advisor_fixes.sql +0 -33
  54. package/supabase/migrations/00000000000036_setup_system_alerts.sql +0 -72
  55. package/supabase/migrations/00000000000037_refresh_setup_article_install_paths.sql +0 -317
  56. package/supabase/migrations/00000000000038_seed_consent_banner_translations.sql +0 -126
  57. package/supabase/migrations/00000000000039_setup_interactions.sql +0 -153
  58. package/supabase/migrations/00000000000040_add_profiles_fk_to_interactions.sql +0 -8
  59. package/supabase/migrations/00000000000041_seed_interactions_translations.sql +0 -145
  60. package/supabase/migrations/00000000000042_seed_deploy_to_vercel_ctas.sql +0 -72
  61. package/supabase/migrations/00000000000043_swap_home_hero_media_to_youtube.sql +0 -42
  62. package/supabase/migrations/00000000000044_swap_article_nx_graph_for_youtube.sql +0 -66
@@ -1,118 +0,0 @@
1
- -- 00000000000017_add_product_blocks.sql
2
- -- Migration to support modular block editor for product descriptions.
3
-
4
- -- 1. Add product_id column to public.blocks
5
- ALTER TABLE public.blocks
6
- ADD COLUMN product_id uuid REFERENCES public.products(id) ON DELETE CASCADE;
7
-
8
- -- 2. Drop the old parent constraint on public.blocks
9
- ALTER TABLE public.blocks
10
- DROP CONSTRAINT IF EXISTS check_exactly_one_parent;
11
-
12
- -- 3. Add the updated parent constraint supporting product_id
13
- ALTER TABLE public.blocks
14
- ADD CONSTRAINT check_exactly_one_parent CHECK (
15
- (page_id IS NOT NULL AND post_id IS NULL AND product_id IS NULL)
16
- OR (post_id IS NOT NULL AND page_id IS NULL AND product_id IS NULL)
17
- OR (product_id IS NOT NULL AND page_id IS NULL AND post_id IS NULL)
18
- );
19
-
20
- -- 4. Add blocks column to product_drafts table
21
- ALTER TABLE public.product_drafts
22
- ADD COLUMN IF NOT EXISTS blocks jsonb NOT NULL DEFAULT '[]'::jsonb;
23
-
24
- -- 5. Add check constraint to ensure product_drafts.blocks is a JSON array
25
- ALTER TABLE public.product_drafts
26
- DROP CONSTRAINT IF EXISTS product_drafts_blocks_array;
27
-
28
- ALTER TABLE public.product_drafts
29
- ADD CONSTRAINT product_drafts_blocks_array CHECK (jsonb_typeof(blocks) = 'array');
30
-
31
- -- 6. Migrate existing product description_json to blocks
32
- INSERT INTO public.blocks (product_id, language_id, block_type, content, "order")
33
- SELECT
34
- id as product_id,
35
- language_id,
36
- 'text' as block_type,
37
- jsonb_build_object('html_content', COALESCE(description_json#>>'{}', '')) as content,
38
- 0 as "order"
39
- FROM public.products
40
- WHERE description_json IS NOT NULL AND (description_json#>>'{}') <> ''
41
- ON CONFLICT DO NOTHING;
42
-
43
- -- 7. Recreate blocks_anon_read_policy
44
- DROP POLICY IF EXISTS blocks_anon_read_policy ON public.blocks;
45
- CREATE POLICY blocks_anon_read_policy
46
- ON public.blocks
47
- FOR SELECT
48
- TO anon
49
- USING (
50
- (
51
- page_id IS NOT NULL
52
- AND EXISTS (
53
- SELECT 1
54
- FROM public.pages AS p
55
- WHERE p.id = blocks.page_id
56
- AND p.status = 'published'
57
- )
58
- )
59
- OR (
60
- post_id IS NOT NULL
61
- AND EXISTS (
62
- SELECT 1
63
- FROM public.posts AS pt
64
- WHERE pt.id = blocks.post_id
65
- AND pt.status = 'published'
66
- AND (pt.published_at IS NULL OR pt.published_at <= now())
67
- )
68
- )
69
- OR (
70
- product_id IS NOT NULL
71
- AND EXISTS (
72
- SELECT 1
73
- FROM public.products AS pr
74
- WHERE pr.id = blocks.product_id
75
- AND pr.status = 'active'
76
- )
77
- )
78
- );
79
-
80
- -- 8. Recreate blocks_read_policy
81
- DROP POLICY IF EXISTS blocks_read_policy ON public.blocks;
82
- CREATE POLICY blocks_read_policy
83
- ON public.blocks
84
- FOR SELECT
85
- TO authenticated
86
- USING (
87
- ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))
88
- OR (
89
- (
90
- page_id IS NOT NULL
91
- AND EXISTS (
92
- SELECT 1
93
- FROM public.pages AS p
94
- WHERE p.id = blocks.page_id
95
- AND p.status = 'published'
96
- )
97
- )
98
- OR (
99
- post_id IS NOT NULL
100
- AND EXISTS (
101
- SELECT 1
102
- FROM public.posts AS pt
103
- WHERE pt.id = blocks.post_id
104
- AND pt.status = 'published'
105
- AND (pt.published_at IS NULL OR pt.published_at <= now())
106
- )
107
- )
108
- OR (
109
- product_id IS NOT NULL
110
- AND EXISTS (
111
- SELECT 1
112
- FROM public.products AS pr
113
- WHERE pr.id = blocks.product_id
114
- AND pr.status = 'active'
115
- )
116
- )
117
- )
118
- );
@@ -1,77 +0,0 @@
1
- -- 00000000000018_setup_bot_protection_settings.sql
2
- -- NextBlock Bot Protection settings and sensitive keys policy hardening.
3
-
4
- COMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys such as Cortex AI BYOK and Bot Protection Secret Key are protected by row-level policies.';
5
-
6
- DROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;
7
- DROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;
8
- DROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;
9
- DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
10
-
11
- CREATE POLICY site_settings_read_policy
12
- ON public.site_settings
13
- FOR SELECT
14
- TO public
15
- USING (
16
- key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
17
- OR (
18
- key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
19
- AND (SELECT auth.role()) = 'authenticated'
20
- AND (SELECT public.get_current_user_role()) = 'ADMIN'
21
- )
22
- );
23
-
24
- CREATE POLICY site_settings_insert_policy
25
- ON public.site_settings
26
- FOR INSERT
27
- TO authenticated
28
- WITH CHECK (
29
- (
30
- key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
31
- AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')
32
- )
33
- OR (
34
- key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
35
- AND (SELECT public.get_current_user_role()) = 'ADMIN'
36
- )
37
- );
38
-
39
- CREATE POLICY site_settings_update_policy
40
- ON public.site_settings
41
- FOR UPDATE
42
- TO authenticated
43
- USING (
44
- (
45
- key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
46
- AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')
47
- )
48
- OR (
49
- key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
50
- AND (SELECT public.get_current_user_role()) = 'ADMIN'
51
- )
52
- )
53
- WITH CHECK (
54
- (
55
- key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
56
- AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')
57
- )
58
- OR (
59
- key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
60
- AND (SELECT public.get_current_user_role()) = 'ADMIN'
61
- )
62
- );
63
-
64
- CREATE POLICY site_settings_delete_policy
65
- ON public.site_settings
66
- FOR DELETE
67
- TO authenticated
68
- USING (
69
- (
70
- key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
71
- AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')
72
- )
73
- OR (
74
- key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')
75
- AND (SELECT public.get_current_user_role()) = 'ADMIN'
76
- )
77
- );
@@ -1,64 +0,0 @@
1
- -- 00000000000019_add_product_categories.sql
2
- -- Migration adding categories and many-to-many product_categories join table.
3
-
4
- CREATE TABLE IF NOT EXISTS public.categories (
5
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6
- name text NOT NULL,
7
- slug text NOT NULL UNIQUE,
8
- description text,
9
- created_at timestamp with time zone DEFAULT now() NOT NULL
10
- );
11
-
12
- CREATE TABLE IF NOT EXISTS public.product_categories (
13
- product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
14
- category_id uuid NOT NULL REFERENCES public.categories(id) ON DELETE CASCADE,
15
- PRIMARY KEY (product_id, category_id)
16
- );
17
-
18
- -- Indexing foreign keys for performance
19
- CREATE INDEX IF NOT EXISTS idx_product_categories_product_id ON public.product_categories(product_id);
20
- CREATE INDEX IF NOT EXISTS idx_product_categories_category_id ON public.product_categories(category_id);
21
-
22
- -- Documenting the tables
23
- COMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';
24
- COMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';
25
-
26
- -- Enable Row Level Security (RLS)
27
- ALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;
28
- ALTER TABLE public.product_categories ENABLE ROW LEVEL SECURITY;
29
-
30
- -- Define RLS Policies
31
- CREATE POLICY "Public can view categories"
32
- ON public.categories
33
- FOR SELECT
34
- TO public
35
- USING (true);
36
-
37
- CREATE POLICY "Public can view product_categories"
38
- ON public.product_categories
39
- FOR SELECT
40
- TO public
41
- USING (true);
42
-
43
- CREATE POLICY "Admin can manage categories"
44
- ON public.categories
45
- FOR ALL
46
- TO authenticated
47
- USING (((SELECT public.is_admin()) IS TRUE))
48
- WITH CHECK (((SELECT public.is_admin()) IS TRUE));
49
-
50
- CREATE POLICY "Admin can manage product_categories"
51
- ON public.product_categories
52
- FOR ALL
53
- TO authenticated
54
- USING (((SELECT public.is_admin()) IS TRUE))
55
- WITH CHECK (((SELECT public.is_admin()) IS TRUE));
56
-
57
- -- Define Grants for roles
58
- GRANT SELECT ON public.categories TO anon;
59
- GRANT ALL ON public.categories TO authenticated;
60
- GRANT ALL ON public.categories TO service_role;
61
-
62
- GRANT SELECT ON public.product_categories TO anon;
63
- GRANT ALL ON public.product_categories TO authenticated;
64
- GRANT ALL ON public.product_categories TO service_role;
@@ -1,10 +0,0 @@
1
- -- 00000000000020_add_category_translations.sql
2
- -- Add name_translations and description_translations columns to public.categories.
3
-
4
- ALTER TABLE public.categories
5
- ADD COLUMN IF NOT EXISTS name_translations jsonb NOT NULL DEFAULT '{}'::jsonb,
6
- ADD COLUMN IF NOT EXISTS description_translations jsonb NOT NULL DEFAULT '{}'::jsonb;
7
-
8
- -- Comment on columns
9
- COMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {"fr": "Numérique"}).';
10
- COMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';
@@ -1,29 +0,0 @@
1
- -- 00000000000021_migrate_hero_blocks_to_sections.sql
2
- -- Migration to convert 'hero' block types to 'section' block types with {"is_hero": true} content.
3
-
4
- -- 1. Migrate active blocks
5
- UPDATE public.blocks
6
- SET
7
- block_type = 'section',
8
- content = COALESCE(content, '{}'::jsonb) || '{"is_hero": true}'::jsonb
9
- WHERE block_type = 'hero';
10
-
11
- -- 2. Migrate content drafts containing hero blocks
12
- UPDATE public.content_drafts
13
- SET blocks = (
14
- SELECT COALESCE(
15
- jsonb_agg(
16
- CASE
17
- WHEN elem.value->>'block_type' = 'hero' THEN
18
- jsonb_set(elem.value, '{block_type}', '"section"'::jsonb) ||
19
- jsonb_build_object('content', COALESCE(elem.value->'content', '{}'::jsonb) || '{"is_hero": true}'::jsonb)
20
- ELSE elem.value
21
- END
22
- ),
23
- '[]'::jsonb
24
- )
25
- FROM jsonb_array_elements(public.content_drafts.blocks) AS elem(value)
26
- )
27
- WHERE public.content_drafts.blocks @> '[{"block_type": "hero"}]';
28
-
29
-
@@ -1,184 +0,0 @@
1
- -- 00000000000022_seed_cortex_ai_guide_post.sql
2
- -- Adds the Cortex AI guide post linked from the seeded home page CTA.
3
-
4
- BEGIN;
5
-
6
- DO $$
7
- DECLARE
8
- v_en_lang_id bigint;
9
- v_cortex_media_id uuid;
10
- v_cortex_post_id bigint;
11
- BEGIN
12
- SELECT id INTO v_en_lang_id
13
- FROM public.languages
14
- WHERE code = 'en'
15
- LIMIT 1;
16
-
17
- IF v_en_lang_id IS NULL THEN
18
- RAISE EXCEPTION 'English language not found.';
19
- END IF;
20
-
21
- INSERT INTO public.media AS seed_media (
22
- file_name,
23
- object_key,
24
- file_path,
25
- file_type,
26
- size_bytes,
27
- width,
28
- height,
29
- folder,
30
- description
31
- )
32
- VALUES (
33
- 'cortex-ai.webp',
34
- 'images/cortex-ai.webp',
35
- 'images/cortex-ai.webp',
36
- 'image/webp',
37
- 298588,
38
- 1024,
39
- 571,
40
- 'images',
41
- 'NextBlock Cortex AI editorial feature image'
42
- )
43
- ON CONFLICT (object_key) DO UPDATE
44
- SET
45
- file_name = COALESCE(seed_media.file_name, EXCLUDED.file_name),
46
- file_path = COALESCE(seed_media.file_path, EXCLUDED.file_path),
47
- file_type = COALESCE(seed_media.file_type, EXCLUDED.file_type),
48
- size_bytes = COALESCE(seed_media.size_bytes, EXCLUDED.size_bytes),
49
- width = COALESCE(seed_media.width, EXCLUDED.width),
50
- height = COALESCE(seed_media.height, EXCLUDED.height),
51
- folder = COALESCE(seed_media.folder, EXCLUDED.folder),
52
- description = COALESCE(seed_media.description, EXCLUDED.description),
53
- updated_at = now()
54
- RETURNING id INTO v_cortex_media_id;
55
-
56
- INSERT INTO public.posts (
57
- language_id,
58
- title,
59
- slug,
60
- label,
61
- status,
62
- excerpt,
63
- subtitle,
64
- published_at,
65
- meta_title,
66
- meta_description,
67
- translation_group_id,
68
- feature_image_id
69
- )
70
- VALUES (
71
- v_en_lang_id,
72
- 'NextBlock Cortex AI Guide',
73
- 'nextblock-cortex-ai-guide',
74
- 'AI Copilot',
75
- 'published',
76
- 'A practical guide to Cortex AI, the block-aware assistant for model routing, BYOK controls, and faster editorial production inside NextBlock.',
77
- 'See how Cortex AI brings structured generation, provider choice, and safer content workflows directly into the NextBlock editor.',
78
- now(),
79
- 'NextBlock Cortex AI Guide',
80
- 'Learn how NextBlock Cortex AI helps teams generate, refine, and translate structured block content with model routing and BYOK controls.',
81
- gen_random_uuid(),
82
- v_cortex_media_id
83
- )
84
- ON CONFLICT (language_id, slug) DO NOTHING
85
- RETURNING id INTO v_cortex_post_id;
86
-
87
- IF v_cortex_post_id IS NULL THEN
88
- SELECT id INTO v_cortex_post_id
89
- FROM public.posts
90
- WHERE language_id = v_en_lang_id
91
- AND slug = 'nextblock-cortex-ai-guide'
92
- LIMIT 1;
93
-
94
- UPDATE public.posts
95
- SET
96
- feature_image_id = v_cortex_media_id,
97
- updated_at = now()
98
- WHERE id = v_cortex_post_id
99
- AND feature_image_id IS NULL;
100
- END IF;
101
-
102
- IF v_cortex_post_id IS NULL THEN
103
- RAISE EXCEPTION 'Unable to create or find Cortex AI guide post.';
104
- END IF;
105
-
106
- INSERT INTO public.blocks (post_id, language_id, block_type, content, "order")
107
- SELECT
108
- v_cortex_post_id,
109
- v_en_lang_id,
110
- 'text',
111
- jsonb_build_object('html_content', $cortex_ai_html$
112
- <p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock Cortex AI is the AI layer built for the way NextBlock pages are actually composed. Instead of treating your site like a blank document, it understands blocks, sections, editor constraints, and the model choices your team wants to use.</p>
113
-
114
- <div class='grid gap-4 md:grid-cols-3 my-10'>
115
- <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'>
116
- <p class='text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Model routing</p>
117
- <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Pick the right model</h3>
118
- <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Route generation through OpenRouter or your configured provider strategy so each task can balance speed, quality, and cost.</p>
119
- </div>
120
- <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'>
121
- <p class='text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200'>BYOK control</p>
122
- <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Use your own keys</h3>
123
- <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Keep provider credentials under your control while still giving editors a clean, guided AI workflow in the CMS.</p>
124
- </div>
125
- <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'>
126
- <p class='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Typed output</p>
127
- <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Generate valid blocks</h3>
128
- <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Schema-aware generation helps AI output land as usable page structure instead of loose copy that needs a rebuild.</p>
129
- </div>
130
- </div>
131
-
132
- <h2>Why Cortex AI Belongs Inside the Editor</h2>
133
- <p>Generic chat tools can draft copy, but they do not know the difference between a hero, a card grid, a product description, and a localized article. Cortex AI lives closer to the editing surface, so generation can respect the same block contracts that render on the public site.</p>
134
- <p>That makes AI useful for practical production work: drafting a landing section, tightening an excerpt, expanding a product story, translating a post, or reshaping a rough idea into a block layout that already fits the system.</p>
135
-
136
- <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'>
137
- <p class='text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Editorial workflow</p>
138
- <div class='grid gap-5 md:grid-cols-2 mt-5'>
139
- <div class='rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50'>
140
- <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Faster first drafts</h3>
141
- <p class='text-sm text-slate-600 dark:text-slate-300'>Start with a structured prompt and get a section, article outline, or product narrative that already matches the site voice.</p>
142
- </div>
143
- <div class='rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50'>
144
- <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Cleaner revisions</h3>
145
- <p class='text-sm text-slate-600 dark:text-slate-300'>Ask for shorter, clearer, more technical, more polished, or locale-ready output without leaving the content screen.</p>
146
- </div>
147
- </div>
148
- </div>
149
-
150
- <h2>Model Routing and Cost Control</h2>
151
- <p>Cortex AI is designed around provider choice. Teams can route requests through the configured AI gateway, choose task-appropriate models, and keep bring-your-own-key setups separate from the editor experience. Editors get a simple control surface while developers keep the operational knobs.</p>
152
- <ul>
153
- <li>Use fast, inexpensive models for drafts, variations, and rewrites</li>
154
- <li>Reserve stronger models for long-form strategy, technical copy, or difficult transformations</li>
155
- <li>Keep provider keys and routing defaults in the server-side configuration layer</li>
156
- <li>Make cost and quality decisions without changing the public rendering system</li>
157
- </ul>
158
-
159
- <h2>Block-Aware Generation</h2>
160
- <p>The important shift is that Cortex AI is not just writing text. It is meant to produce content that can map back to NextBlock surfaces: section copy, article bodies, product descriptions, headings, calls to action, and localized variants. That reduces the cleanup step that usually happens after copying AI text from a separate tool.</p>
161
- <p>Because the output is shaped around existing block contracts, the generated content feels native in the editor and predictable on the frontend.</p>
162
-
163
- <h2>Safer Team Workflows</h2>
164
- <p>AI works best when it is helpful without becoming invisible infrastructure. Cortex AI keeps humans in the loop: editors review the output, developers control the available providers, and the CMS keeps the generated content inside the same revision and publishing flow as everything else.</p>
165
-
166
- <h2>A Practical Launch Flow</h2>
167
- <ol>
168
- <li>Draft the article, landing section, or product story from a focused prompt.</li>
169
- <li>Refine the result against the brand voice and target audience.</li>
170
- <li>Generate a localized version or shorter excerpt for cards and metadata.</li>
171
- <li>Review the content in the NextBlock editor, publish, and keep iterating through normal revisions.</li>
172
- </ol>
173
-
174
- <p>Cortex AI turns the CMS into a more capable production surface: not a replacement for editorial judgment, but a faster way to shape good ideas into structured pages that are ready to ship.</p>
175
- $cortex_ai_html$),
176
- 0
177
- WHERE NOT EXISTS (
178
- SELECT 1
179
- FROM public.blocks
180
- WHERE post_id = v_cortex_post_id
181
- );
182
- END $$;
183
-
184
- COMMIT;
@@ -1,183 +0,0 @@
1
- -- Custom block definition registry for data-rendered user blocks.
2
-
3
- CREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb)
4
- RETURNS boolean
5
- LANGUAGE sql
6
- IMMUTABLE
7
- SET search_path = ''
8
- AS $$
9
- SELECT CASE
10
- WHEN jsonb_typeof(candidate) <> 'array' THEN false
11
- ELSE
12
- NOT EXISTS (
13
- SELECT 1
14
- FROM jsonb_array_elements(candidate) AS field(value)
15
- WHERE jsonb_typeof(field.value) <> 'object'
16
- OR jsonb_typeof(field.value -> 'key') IS DISTINCT FROM 'string'
17
- OR jsonb_typeof(field.value -> 'label') IS DISTINCT FROM 'string'
18
- OR jsonb_typeof(field.value -> 'type') IS DISTINCT FROM 'string'
19
- OR field.value ->> 'key' !~ '^[a-z][a-z0-9_]*$'
20
- OR field.value ->> 'type' NOT IN ('text', 'rich-text', 'image_r2', 'db_relation')
21
- )
22
- AND (
23
- SELECT COUNT(*) = COUNT(DISTINCT field.value ->> 'key')
24
- FROM jsonb_array_elements(candidate) AS field(value)
25
- )
26
- END;
27
- $$;
28
-
29
- CREATE OR REPLACE FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb)
30
- RETURNS boolean
31
- LANGUAGE sql
32
- IMMUTABLE
33
- SET search_path = ''
34
- AS $$
35
- SELECT CASE
36
- WHEN jsonb_typeof(candidate) <> 'object' THEN false
37
- ELSE candidate ->> 'type' IN ('container', 'field_render')
38
- END;
39
- $$;
40
-
41
- CREATE TABLE IF NOT EXISTS public.custom_block_definitions (
42
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
43
- slug text NOT NULL UNIQUE CHECK (slug ~ '^[a-z][a-z0-9-]*$'),
44
- name text NOT NULL CHECK (length(trim(name)) > 0),
45
- description text NOT NULL DEFAULT '',
46
- fields jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (public.is_valid_custom_block_fields(fields)),
47
- layout_schema jsonb NOT NULL CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),
48
- is_original boolean NOT NULL DEFAULT true
49
- );
50
-
51
- COMMENT ON TABLE public.custom_block_definitions IS
52
- 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';
53
- COMMENT ON COLUMN public.custom_block_definitions.fields IS
54
- 'Strict JSONB field declarations for data-rendered custom blocks.';
55
- COMMENT ON COLUMN public.custom_block_definitions.layout_schema IS
56
- 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';
57
- COMMENT ON COLUMN public.custom_block_definitions.is_original IS
58
- 'False when a definition was created by duplicating an existing registry row.';
59
-
60
- CREATE INDEX IF NOT EXISTS idx_custom_block_definitions_is_original
61
- ON public.custom_block_definitions (is_original);
62
-
63
- CREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid)
64
- RETURNS public.custom_block_definitions
65
- LANGUAGE plpgsql
66
- SECURITY DEFINER
67
- SET search_path = public
68
- AS $$
69
- DECLARE
70
- source_definition public.custom_block_definitions%ROWTYPE;
71
- copied_definition public.custom_block_definitions%ROWTYPE;
72
- base_slug text;
73
- copy_slug text;
74
- copy_index integer := 1;
75
- BEGIN
76
- IF auth.role() <> 'service_role'
77
- AND COALESCE((SELECT public.get_current_user_role())::text, '') NOT IN ('ADMIN', 'WRITER') THEN
78
- RAISE EXCEPTION 'Not authorized to duplicate custom block definitions.'
79
- USING ERRCODE = '42501';
80
- END IF;
81
-
82
- SELECT *
83
- INTO source_definition
84
- FROM public.custom_block_definitions
85
- WHERE id = target_id;
86
-
87
- IF NOT FOUND THEN
88
- RAISE EXCEPTION 'Custom block definition % not found.', target_id
89
- USING ERRCODE = 'P0002';
90
- END IF;
91
-
92
- base_slug := regexp_replace(source_definition.slug, '-copy(-[0-9]+)?$', '');
93
- copy_slug := base_slug || '-copy';
94
-
95
- WHILE EXISTS (
96
- SELECT 1
97
- FROM public.custom_block_definitions
98
- WHERE slug = copy_slug
99
- ) LOOP
100
- copy_index := copy_index + 1;
101
- copy_slug := base_slug || '-copy-' || copy_index;
102
- END LOOP;
103
-
104
- INSERT INTO public.custom_block_definitions (
105
- id,
106
- slug,
107
- name,
108
- description,
109
- fields,
110
- layout_schema,
111
- is_original
112
- )
113
- VALUES (
114
- gen_random_uuid(),
115
- copy_slug,
116
- source_definition.name || ' Copy',
117
- source_definition.description,
118
- source_definition.fields,
119
- source_definition.layout_schema,
120
- false
121
- )
122
- RETURNING *
123
- INTO copied_definition;
124
-
125
- RETURN copied_definition;
126
- END;
127
- $$;
128
-
129
- ALTER TABLE public.custom_block_definitions ENABLE ROW LEVEL SECURITY;
130
-
131
- GRANT SELECT ON public.custom_block_definitions TO anon, authenticated, service_role;
132
- GRANT INSERT, UPDATE, DELETE ON public.custom_block_definitions TO authenticated;
133
- GRANT ALL ON public.custom_block_definitions TO service_role;
134
-
135
- DROP POLICY IF EXISTS custom_block_definitions_public_read_policy
136
- ON public.custom_block_definitions;
137
-
138
- CREATE POLICY custom_block_definitions_public_read_policy
139
- ON public.custom_block_definitions
140
- FOR SELECT
141
- TO public
142
- USING (true);
143
-
144
- DROP POLICY IF EXISTS custom_block_definitions_insert_policy
145
- ON public.custom_block_definitions;
146
-
147
- CREATE POLICY custom_block_definitions_insert_policy
148
- ON public.custom_block_definitions
149
- FOR INSERT
150
- TO authenticated
151
- WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));
152
-
153
- DROP POLICY IF EXISTS custom_block_definitions_update_policy
154
- ON public.custom_block_definitions;
155
-
156
- CREATE POLICY custom_block_definitions_update_policy
157
- ON public.custom_block_definitions
158
- FOR UPDATE
159
- TO authenticated
160
- USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))
161
- WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));
162
-
163
- DROP POLICY IF EXISTS custom_block_definitions_delete_policy
164
- ON public.custom_block_definitions;
165
-
166
- CREATE POLICY custom_block_definitions_delete_policy
167
- ON public.custom_block_definitions
168
- FOR DELETE
169
- TO authenticated
170
- USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));
171
-
172
- DROP POLICY IF EXISTS custom_block_definitions_service_role_policy
173
- ON public.custom_block_definitions;
174
-
175
- CREATE POLICY custom_block_definitions_service_role_policy
176
- ON public.custom_block_definitions
177
- FOR ALL
178
- TO service_role
179
- USING (true)
180
- WITH CHECK (true);
181
-
182
- REVOKE ALL ON FUNCTION public.duplicate_block_definition(uuid) FROM PUBLIC, anon;
183
- GRANT EXECUTE ON FUNCTION public.duplicate_block_definition(uuid) TO authenticated, service_role;