create-nextblock 0.9.95 → 0.9.99
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.
- package/package.json +1 -1
- package/templates/nextblock-template/app/[slug]/page.tsx +9 -2
- package/templates/nextblock-template/app/actions/package-actions.ts +9 -4
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3 -2
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +5 -1
- package/templates/nextblock-template/app/api/draft/route.ts +5 -1
- package/templates/nextblock-template/app/api/media/library/route.ts +6 -2
- package/templates/nextblock-template/app/api/process-image/route.ts +58 -43
- package/templates/nextblock-template/app/api/revalidate/route.ts +21 -18
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +20 -8
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +34 -28
- package/templates/nextblock-template/app/article/[slug]/page.tsx +4 -13
- package/templates/nextblock-template/app/checkout/success/actions.ts +3 -2
- package/templates/nextblock-template/app/cms/media/actions.ts +47 -31
- package/templates/nextblock-template/app/cms/orders/actions.ts +29 -29
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/users/actions.ts +119 -118
- package/templates/nextblock-template/app/cms/users/page.tsx +3 -3
- package/templates/nextblock-template/app/layout.tsx +10 -7
- package/templates/nextblock-template/app/lib/site-settings.ts +7 -4
- package/templates/nextblock-template/app/page.tsx +2 -1
- package/templates/nextblock-template/app/product/[slug]/page.tsx +9 -2
- package/templates/nextblock-template/app/robots.txt/route.ts +6 -3
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +55 -8
- package/templates/nextblock-template/app/setup/page.tsx +17 -1
- package/templates/nextblock-template/app/sitemap.ts +5 -3
- package/templates/nextblock-template/context/language-rest-client.ts +3 -2
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +125 -32
- package/templates/nextblock-template/lib/app-secrets.ts +39 -0
- package/templates/nextblock-template/lib/auth/crypto.ts +4 -1
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +23 -3
- package/templates/nextblock-template/lib/setup/actions.ts +16 -0
- package/templates/nextblock-template/lib/setup/env-status.ts +44 -5
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +172 -0
- package/templates/nextblock-template/lib/setup/schema-apply.ts +23 -7
- package/templates/nextblock-template/lib/site-url.ts +48 -0
- package/templates/nextblock-template/lib/storage/provider.ts +66 -0
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.ts +1 -1
- package/templates/nextblock-template/next.config.js +37 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +39 -4
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// AUTO-GENERATED by tools/scripts/generate-migrations-bundle.ts — do not edit by hand.
|
|
2
|
+
// Regenerate after adding a migration: npm run generate:migrations-bundle
|
|
3
|
+
//
|
|
4
|
+
// Embedded forward-only migrations so the /setup wizard can apply the schema on
|
|
5
|
+
// serverless hosts (Vercel) where libs/db isn't on the function filesystem.
|
|
6
|
+
|
|
7
|
+
export interface BundledMigration {
|
|
8
|
+
/** Numeric version prefix, e.g. "00000000000030". */
|
|
9
|
+
version: string;
|
|
10
|
+
/** Original migration filename. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Raw SQL contents. */
|
|
13
|
+
sql: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const MIGRATIONS_BUNDLE: BundledMigration[] = [
|
|
17
|
+
{
|
|
18
|
+
"version": "00000000000000",
|
|
19
|
+
"name": "00000000000000_setup_foundation_and_enums.sql",
|
|
20
|
+
"sql": "-- 00000000000000_setup_foundation_and_enums.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000000_setup_schema_privileges.sql\n-- Foundation privileges for the public schema.\n\nGRANT USAGE ON SCHEMA public TO postgres;\nGRANT USAGE ON SCHEMA public TO anon;\nGRANT USAGE ON SCHEMA public TO authenticated;\nGRANT USAGE ON SCHEMA public TO service_role;\r\n\r\n-- 00000000000001_setup_auth_and_content_enums.sql\n-- Core enums used by auth and content tables.\n\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN\n CREATE TYPE public.user_role AS ENUM ('ADMIN', 'WRITER', 'USER');\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'page_status') THEN\n CREATE TYPE public.page_status AS ENUM ('draft', 'published', 'archived');\n END IF;\nEND\n$$;\r\n\r\n-- 00000000000002_setup_navigation_and_revision_enums.sql\n-- Supporting enums for navigation and revision history.\n\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'menu_location') THEN\n CREATE TYPE public.menu_location AS ENUM ('HEADER', 'FOOTER', 'SIDEBAR');\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'revision_type') THEN\n CREATE TYPE public.revision_type AS ENUM ('snapshot', 'diff');\n END IF;\nEND\n$$;\r\n"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"version": "00000000000001",
|
|
24
|
+
"name": "00000000000001_setup_cms_core.sql",
|
|
25
|
+
"sql": "-- 00000000000001_setup_cms_core.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000003_setup_site_settings_and_profiles.sql\n-- Global settings, user profiles, and saved addresses.\n\nCREATE TABLE public.site_settings (\n key text PRIMARY KEY,\n value jsonb\n);\n\nCOMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings.';\n\nCREATE TABLE public.profiles (\n id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,\n updated_at timestamptz,\n full_name text,\n avatar_url text,\n website text,\n github_username text,\n phone text,\n role public.user_role NOT NULL DEFAULT 'USER'\n);\n\nCOMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';\nCOMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';\nCOMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';\n\nCREATE TABLE public.user_addresses (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,\n address_type text NOT NULL CHECK (address_type IN ('billing', 'shipping')),\n is_default boolean NOT NULL DEFAULT false,\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 timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON COLUMN public.user_addresses.company_name IS\n 'Optional company or organization name for the address.';\n\nCREATE UNIQUE INDEX idx_user_addresses_one_default_per_type\n ON public.user_addresses (user_id, address_type)\n WHERE is_default = true;\r\n\r\n-- 00000000000004_setup_languages_and_media.sql\n-- Core locale and media tables.\n\nCREATE TABLE public.languages (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n code text NOT NULL UNIQUE,\n name text NOT NULL,\n is_default boolean NOT NULL DEFAULT false,\n is_active boolean DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';\nCOMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';\n\nCREATE UNIQUE INDEX ensure_single_default_language_idx\n ON public.languages (is_default)\n WHERE is_default = true;\n\nCREATE TABLE public.media (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n uploader_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\n file_name text NOT NULL,\n object_key text NOT NULL UNIQUE,\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 timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';\nCOMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';\nCOMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';\nCOMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';\nCOMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';\nCOMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';\nCOMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';\nCOMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';\r\n\r\n-- 00000000000005_setup_translations_and_branding.sql\n-- Shared translation storage and logo metadata.\n\nCREATE TABLE public.translations (\n key text PRIMARY KEY,\n translations jsonb NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON COLUMN public.translations.key IS\n 'A unique, slugified identifier (e.g., \"sign_in_button_text\").';\nCOMMENT ON COLUMN public.translations.translations IS\n 'Stores translations as key-value pairs (e.g., {\"en\": \"Sign In\", \"fr\": \"S''inscrire\"}).';\n\nCREATE TABLE public.logos (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n name text NOT NULL,\n media_id uuid REFERENCES public.media(id) ON DELETE SET NULL,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.logos IS 'Stores company and brand logos.';\nCOMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';\nCOMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';\r\n"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"version": "00000000000002",
|
|
29
|
+
"name": "00000000000002_setup_content_tables.sql",
|
|
30
|
+
"sql": "-- 00000000000002_setup_content_tables.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000006_setup_pages_and_posts.sql\n-- Page and post records.\n\nCREATE TABLE public.posts (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,\n author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\n title text NOT NULL,\n slug text NOT NULL,\n label text,\n excerpt text,\n subtitle text,\n status public.page_status NOT NULL DEFAULT 'draft',\n published_at timestamptz,\n meta_title text,\n meta_description text,\n feature_image_id uuid REFERENCES public.media(id) ON DELETE SET NULL,\n version integer NOT NULL DEFAULT 1,\n translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT posts_language_id_slug_key UNIQUE (language_id, slug)\n);\n\nCOMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';\nCOMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';\nCOMMENT ON COLUMN public.posts.label IS\n 'Short editorial label rendered as a pill on post hero and article cards.';\nCOMMENT ON COLUMN public.posts.excerpt IS\n 'Short editorial summary used in post metadata rows and post cards.';\nCOMMENT ON COLUMN public.posts.subtitle IS\n 'Longer deck shown under the post title.';\nCOMMENT ON COLUMN public.posts.feature_image_id IS\n 'ID of the media item to be used as the feature image.';\nCOMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';\nCOMMENT ON COLUMN public.posts.translation_group_id IS\n 'Groups different language versions of the same conceptual post.';\n\nCREATE TABLE public.pages (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,\n author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\n title text NOT NULL,\n slug text NOT NULL,\n status public.page_status NOT NULL DEFAULT 'draft',\n meta_title text,\n meta_description text,\n version integer NOT NULL DEFAULT 1,\n translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT pages_language_id_slug_key UNIQUE (language_id, slug)\n);\n\nCOMMENT ON TABLE public.pages IS 'Stores static pages for the website.';\nCOMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';\nCOMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';\nCOMMENT ON COLUMN public.pages.translation_group_id IS\n 'Groups different language versions of the same conceptual page.';\r\n\r\n-- 00000000000007_setup_blocks_and_navigation.sql\n-- Content blocks and navigation trees.\n\nCREATE TABLE public.blocks (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n page_id bigint REFERENCES public.pages(id) ON DELETE CASCADE,\n post_id bigint REFERENCES public.posts(id) ON DELETE CASCADE,\n language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,\n block_type text NOT NULL,\n content jsonb,\n \"order\" integer NOT NULL DEFAULT 0,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT check_exactly_one_parent CHECK (\n (page_id IS NOT NULL AND post_id IS NULL)\n OR (post_id IS NOT NULL AND page_id IS NULL)\n )\n);\n\nCOMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';\nCOMMENT ON COLUMN public.blocks.block_type IS\n 'Type of the block, e.g., \"text\", \"image\".';\nCOMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';\nCOMMENT ON COLUMN public.blocks.order IS 'Sort order of the block.';\n\nCREATE TABLE public.navigation_items (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,\n menu_key public.menu_location NOT NULL,\n label text NOT NULL,\n url text NOT NULL,\n parent_id bigint REFERENCES public.navigation_items(id) ON DELETE CASCADE,\n \"order\" integer NOT NULL DEFAULT 0,\n page_id bigint REFERENCES public.pages(id) ON DELETE SET NULL,\n translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';\nCOMMENT ON COLUMN public.navigation_items.menu_key IS\n 'Identifies the menu this item belongs to.';\r\n\r\n-- 00000000000008_setup_revisions.sql\n-- Page and post revision history.\n\nCREATE TABLE public.page_revisions (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n page_id bigint NOT NULL REFERENCES public.pages(id) ON DELETE CASCADE,\n author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\n version integer NOT NULL,\n revision_type public.revision_type NOT NULL,\n content jsonb NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT page_revisions_page_version_key UNIQUE (page_id, version)\n);\n\nCOMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';\nCOMMENT ON COLUMN public.page_revisions.content IS\n 'If snapshot: full content; if diff: JSON Patch array.';\n\nCREATE TABLE public.post_revisions (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n post_id bigint NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE,\n author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\n version integer NOT NULL,\n revision_type public.revision_type NOT NULL,\n content jsonb NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT post_revisions_post_version_key UNIQUE (post_id, version)\n);\n\nCOMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';\nCOMMENT ON COLUMN public.post_revisions.content IS\n 'If snapshot: full content; if diff: JSON Patch array.';\r\n"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "00000000000003",
|
|
34
|
+
"name": "00000000000003_setup_catalog_and_licensing.sql",
|
|
35
|
+
"sql": "-- 00000000000003_setup_catalog_and_licensing.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000009_setup_products_and_media.sql\r\n-- Core product catalog tables.\r\n\r\nCREATE TABLE public.products (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,\r\n translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),\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 CHECK (product_type IN ('physical', 'digital')),\r\n payment_provider text NOT NULL CHECK (payment_provider IN ('stripe', 'freemius')),\r\n price integer NOT NULL,\r\n prices jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock integer DEFAULT 0,\r\n status text NOT NULL CHECK (status IN ('draft', 'active', 'archived')) DEFAULT 'draft',\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 NOT NULL DEFAULT 0 CHECK (trial_period_days >= 0),\r\n trial_requires_payment_method boolean NOT NULL DEFAULT false,\r\n upc text,\r\n is_taxable boolean NOT NULL DEFAULT true,\r\n created_at timestamptz DEFAULT now(),\r\n updated_at timestamptz DEFAULT now(),\r\n CONSTRAINT products_type_provider_consistency_check CHECK (\r\n (product_type = 'physical' AND payment_provider = 'stripe')\r\n OR (product_type = 'digital' AND payment_provider = 'freemius')\r\n ),\r\n CONSTRAINT products_language_id_slug_key UNIQUE (language_id, slug),\r\n CONSTRAINT products_language_id_sku_key UNIQUE (language_id, sku)\r\n);\r\n\r\nCOMMENT ON COLUMN public.products.is_taxable IS\r\n 'When true, this product participates in Stripe tax calculation.';\r\nCOMMENT ON COLUMN public.products.prices IS\r\n 'Regular prices by ISO 4217 code in the smallest currency unit.';\r\nCOMMENT ON COLUMN public.products.sale_prices IS\r\n 'Sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCREATE TABLE public.product_media (\r\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\r\n media_id uuid NOT NULL REFERENCES public.media(id) ON DELETE CASCADE,\r\n sort_order integer DEFAULT 0,\r\n PRIMARY KEY (product_id, media_id)\r\n);\r\n\r\n-- 00000000000010_setup_product_variants_and_attributes.sql\r\n-- Variant attributes, terms, and sellable variants.\r\n\r\nCREATE TABLE public.product_attributes (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n name text NOT NULL,\r\n slug text NOT NULL UNIQUE,\r\n name_translations jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n created_at timestamptz DEFAULT now(),\r\n updated_at timestamptz DEFAULT now()\r\n);\r\n\r\nCREATE TABLE public.product_attribute_terms (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n attribute_id uuid NOT NULL REFERENCES public.product_attributes(id) ON DELETE CASCADE,\r\n value text NOT NULL,\r\n slug text NOT NULL,\r\n sort_order integer NOT NULL DEFAULT 0,\r\n value_translations jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n created_at timestamptz DEFAULT now(),\r\n updated_at timestamptz DEFAULT now(),\r\n CONSTRAINT product_attribute_terms_attribute_id_slug_key UNIQUE (attribute_id, slug)\r\n);\r\n\r\nCREATE TABLE public.product_variants (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\r\n sku text NOT NULL,\r\n price_adjustment integer NOT NULL DEFAULT 0,\r\n price integer NOT NULL DEFAULT 0,\r\n prices jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock_quantity integer NOT NULL DEFAULT 0,\r\n upc text,\r\n main_media_id uuid REFERENCES public.media(id) ON DELETE SET NULL,\r\n created_at timestamptz DEFAULT now(),\r\n updated_at timestamptz DEFAULT now(),\r\n CONSTRAINT product_variants_product_id_sku_key UNIQUE (product_id, sku)\r\n);\r\n\r\nCOMMENT ON COLUMN public.product_variants.prices IS\r\n 'Variant regular prices by ISO 4217 code in the smallest currency unit.';\r\nCOMMENT ON COLUMN public.product_variants.sale_prices IS\r\n 'Variant sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCREATE TABLE public.inventory_items (\r\n sku text PRIMARY KEY,\r\n quantity integer NOT NULL DEFAULT 0 CHECK (quantity >= 0),\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now()\r\n);\r\n\r\nCOMMENT ON TABLE public.inventory_items IS\r\n 'Source-of-truth inventory records keyed by sellable SKU.';\r\nCOMMENT ON COLUMN public.inventory_items.sku IS\r\n 'Global sellable SKU. Matching products or variants share inventory.';\r\nCOMMENT ON COLUMN public.inventory_items.quantity IS\r\n 'Available quantity for this SKU.';\r\n\r\nCREATE TABLE public.variant_attribute_mapping (\r\n variant_id uuid NOT NULL REFERENCES public.product_variants(id) ON DELETE CASCADE,\r\n attribute_term_id uuid NOT NULL REFERENCES public.product_attribute_terms(id) ON DELETE CASCADE,\r\n PRIMARY KEY (variant_id, attribute_term_id)\r\n);\r\n\r\n-- 00000000000011_setup_licensing_and_freemius.sql\r\n-- Package activations and Freemius plan metadata.\r\n\r\nCREATE TABLE public.package_activations (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\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 NOT NULL DEFAULT 'active',\r\n meta jsonb DEFAULT '{}'::jsonb,\r\n last_validated_at timestamptz DEFAULT now(),\r\n created_at timestamptz DEFAULT now(),\r\n UNIQUE (license_key, package_id)\r\n);\r\n\r\nCREATE TABLE public.freemius_plans (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\r\n name text NOT NULL,\r\n title text,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now()\r\n);\r\n\r\nCREATE TABLE public.freemius_pricing (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n plan_id uuid NOT NULL REFERENCES public.freemius_plans(id) ON DELETE CASCADE,\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 NOT NULL DEFAULT true,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now()\r\n);\r\n"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"version": "00000000000004",
|
|
39
|
+
"name": "00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql",
|
|
40
|
+
"sql": "-- 00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- Orders, line items, and invoice numbering primitives.\r\n\r\nCREATE TABLE public.orders (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,\r\n status text NOT NULL CHECK (status IN ('pending', 'trial', 'paid', 'shipped', 'cancelled', 'refunded')) DEFAULT 'pending',\r\n total integer NOT NULL,\r\n stripe_session_id text UNIQUE,\r\n payment_intent_id text,\r\n customer_details jsonb,\r\n provider text CHECK (provider IN ('stripe', 'freemius')) DEFAULT 'stripe',\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 timestamptz,\r\n freemius_last_event_type text,\r\n freemius_last_synced_at timestamptz,\r\n currency text NOT NULL DEFAULT 'USD',\r\n subtotal integer,\r\n shipping_total integer,\r\n tax_total integer NOT NULL DEFAULT 0,\r\n tax_details jsonb,\r\n exchange_rate_at_purchase numeric(20,10) NOT NULL DEFAULT 1,\r\n inventory_deducted_at timestamptz,\r\n invoice_number text,\r\n paid_at timestamptz,\r\n created_at timestamptz DEFAULT now(),\r\n CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK (exchange_rate_at_purchase > 0)\r\n);\r\n\r\nCOMMENT ON COLUMN public.orders.currency IS\r\n 'ISO currency code used for the order totals.';\r\nCOMMENT ON COLUMN public.orders.subtotal IS\r\n 'Subtotal before shipping and tax, in the smallest currency unit.';\r\nCOMMENT ON COLUMN public.orders.shipping_total IS\r\n 'Shipping amount before tax, in the smallest currency unit.';\r\nCOMMENT ON COLUMN public.orders.tax_total IS\r\n 'Total tax amount collected for the order, in the smallest currency unit.';\r\nCOMMENT ON COLUMN public.orders.tax_details IS\r\n 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';\r\nCOMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS\r\n 'Exchange rate locked at purchase time relative to the store default currency.';\r\nCOMMENT ON COLUMN public.orders.invoice_number IS\r\n 'Stable printable invoice number assigned once when the order first becomes paid.';\r\nCOMMENT ON COLUMN public.orders.paid_at IS\r\n 'Timestamp when the order was first marked as paid.';\r\nCOMMENT ON COLUMN public.orders.freemius_license_id IS\r\n 'Freemius license ID used to reconcile checkout callbacks and webhooks.';\r\nCOMMENT ON COLUMN public.orders.freemius_subscription_id IS\r\n 'Freemius subscription ID when the order is associated with recurring billing.';\r\nCOMMENT ON COLUMN public.orders.freemius_trial_id IS\r\n 'Freemius trial ID when checkout starts in trial mode.';\r\nCOMMENT ON COLUMN public.orders.freemius_trial_ends_at IS\r\n 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';\r\nCOMMENT ON COLUMN public.orders.freemius_last_event_type IS\r\n 'Last Freemius checkout callback or webhook event applied to the order.';\r\nCOMMENT ON COLUMN public.orders.freemius_last_synced_at IS\r\n 'Timestamp when Freemius metadata was last reconciled locally.';\r\n\r\nCREATE UNIQUE INDEX idx_orders_invoice_number_unique\r\n ON public.orders (invoice_number)\r\n WHERE invoice_number IS NOT NULL;\r\n\r\nCREATE TABLE public.order_items (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE,\r\n product_id uuid REFERENCES public.products(id) ON DELETE SET NULL,\r\n variant_id uuid REFERENCES public.product_variants(id) ON DELETE SET NULL,\r\n quantity integer NOT NULL,\r\n price_at_purchase integer NOT NULL\r\n);\r\n\r\nCREATE SEQUENCE public.order_invoice_number_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n MINVALUE 1\r\n NO MAXVALUE\r\n CACHE 1;\r\n\r\n-- 00000000000013_setup_shipping_and_taxes.sql\r\n-- Shipping zones, shipping methods, and manual tax rates.\r\n\r\nCREATE TABLE public.shipping_zones (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n name text NOT NULL,\r\n priority_order integer NOT NULL DEFAULT 0,\r\n created_at timestamptz DEFAULT now(),\r\n updated_at timestamptz DEFAULT now()\r\n);\r\n\r\nCREATE TABLE public.shipping_zone_locations (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n zone_id uuid NOT NULL REFERENCES public.shipping_zones(id) ON DELETE CASCADE,\r\n country_code text NOT NULL,\r\n state_code text,\r\n postal_code text,\r\n created_at timestamptz DEFAULT now()\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.country_code IS\r\n 'ISO 3166-1 alpha-2 country code.';\r\nCOMMENT ON COLUMN public.shipping_zone_locations.state_code IS\r\n 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';\r\nCOMMENT ON COLUMN public.shipping_zone_locations.postal_code IS\r\n 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';\r\n\r\nCREATE TABLE public.shipping_zone_methods (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n zone_id uuid NOT NULL REFERENCES public.shipping_zones(id) ON DELETE CASCADE,\r\n method_type text NOT NULL CHECK (method_type IN ('flat_rate', 'free_shipping')),\r\n cost_amount integer NOT NULL DEFAULT 0,\r\n cost_currency text NOT NULL DEFAULT 'USD',\r\n min_order_amount integer NOT NULL DEFAULT 0,\r\n name text NOT NULL,\r\n name_translations jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n currency_pricing_mode text NOT NULL DEFAULT 'auto',\r\n cost_amounts jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n min_order_amounts jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n created_at timestamptz DEFAULT now(),\r\n updated_at timestamptz DEFAULT now(),\r\n CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid\r\n CHECK (currency_pricing_mode IN ('auto', 'manual')),\r\n CONSTRAINT shipping_zone_methods_cost_currency_format\r\n CHECK (cost_currency ~ '^[A-Z]{3}$')\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.name_translations IS\r\n 'Localized shipping method labels keyed by language code. Example: {\"fr\": \"Livraison standard\"}.';\r\nCOMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS\r\n 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';\r\nCOMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS\r\n 'Shipping costs by ISO 4217 code in the smallest currency unit.';\r\nCOMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS\r\n 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';\r\n\r\nCREATE TABLE public.tax_rates (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n country_code text NOT NULL,\r\n state_code text,\r\n tax_name text NOT NULL CHECK (char_length(btrim(tax_name)) > 0),\r\n tax_rate numeric(7,4) NOT NULL CHECK (tax_rate >= 0 AND tax_rate <= 100),\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now()\r\n);\r\n\r\nCOMMENT ON TABLE public.tax_rates IS\r\n 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';\r\nCOMMENT ON COLUMN public.tax_rates.country_code IS\r\n 'ISO 3166-1 alpha-2 country code.';\r\nCOMMENT ON COLUMN public.tax_rates.state_code IS\r\n 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';\r\nCOMMENT ON COLUMN public.tax_rates.tax_name IS\r\n 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';\r\nCOMMENT ON COLUMN public.tax_rates.tax_rate IS\r\n 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';\r\n\r\nCREATE UNIQUE INDEX tax_rates_country_state_name_key\r\n ON public.tax_rates (country_code, COALESCE(state_code, ''), lower(tax_name));\r\n\r\n-- 00000000000014_setup_currencies.sql\r\n-- Multi-currency configuration.\r\n\r\nCREATE TABLE public.currencies (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n code text NOT NULL UNIQUE CHECK (code ~ '^[A-Z]{3}$'),\r\n symbol text NOT NULL,\r\n exchange_rate numeric(20,10) NOT NULL CHECK (exchange_rate > 0),\r\n is_default boolean NOT NULL DEFAULT false,\r\n is_active boolean NOT NULL DEFAULT true,\r\n rounding_mode text NOT NULL DEFAULT 'none',\r\n rounding_increment integer NOT NULL DEFAULT 1,\r\n rounding_charm_amount integer,\r\n auto_update_exchange_rate boolean NOT NULL DEFAULT true,\r\n exchange_rate_updated_at timestamptz,\r\n exchange_rate_source text,\r\n auto_sync_product_prices boolean NOT NULL DEFAULT false,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now(),\r\n CONSTRAINT currencies_default_must_be_active CHECK (NOT is_default OR is_active),\r\n CONSTRAINT currencies_rounding_mode_valid\r\n CHECK (rounding_mode IN ('none', 'nearest', 'up', 'down', 'charm')),\r\n CONSTRAINT currencies_rounding_increment_positive\r\n CHECK (rounding_increment > 0),\r\n CONSTRAINT currencies_rounding_charm_nonnegative\r\n CHECK (rounding_charm_amount IS NULL OR rounding_charm_amount >= 0),\r\n CONSTRAINT currencies_charm_requires_amount\r\n CHECK (rounding_mode <> 'charm' OR rounding_charm_amount IS NOT NULL),\r\n CONSTRAINT currencies_default_exchange_rate_is_one\r\n CHECK (NOT is_default OR exchange_rate = 1),\r\n CONSTRAINT currencies_default_auto_update_disabled\r\n CHECK (NOT is_default OR auto_update_exchange_rate = false),\r\n CONSTRAINT currencies_default_product_price_sync_disabled\r\n CHECK (NOT is_default OR auto_sync_product_prices = false)\r\n);\r\n\r\nCOMMENT ON TABLE public.currencies IS\r\n 'Store currencies available for storefront display and conversion.';\r\nCOMMENT ON COLUMN public.currencies.exchange_rate IS\r\n 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';\r\nCOMMENT ON COLUMN public.currencies.rounding_mode IS\r\n 'Rounding strategy applied when prices are auto-converted into this currency.';\r\nCOMMENT ON COLUMN public.currencies.rounding_increment IS\r\n 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';\r\nCOMMENT ON COLUMN public.currencies.rounding_charm_amount IS\r\n 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';\r\nCOMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS\r\n 'Whether scheduled FX sync jobs should refresh this currency.';\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS\r\n 'When this currency exchange rate was last refreshed or manually set.';\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_source IS\r\n 'Human-readable source for the current exchange rate, such as a provider host or manual override.';\r\nCOMMENT ON COLUMN public.currencies.auto_sync_product_prices IS\r\n '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 UNIQUE INDEX idx_currencies_single_default\r\n ON public.currencies (is_default)\r\n WHERE is_default = true;\r\n\r\n-- 00000000000017_setup_currency_shipping_and_tax_functions.sql\r\n-- Currency, shipping, and tax helper functions plus dependent constraints.\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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.get_default_currency_code()\r\nRETURNS text\r\nLANGUAGE sql\r\nSTABLE\r\nSET search_path = ''\r\nAS $$\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.normalize_currency_amount_map(amounts jsonb)\r\nRETURNS jsonb\r\nLANGUAGE sql\r\nIMMUTABLE\r\nSET search_path = ''\r\nAS $$\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.is_valid_currency_amount_map(amounts jsonb)\r\nRETURNS boolean\r\nLANGUAGE sql\r\nIMMUTABLE\r\nSET search_path = ''\r\nAS $$\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)\r\nRETURNS boolean\r\nLANGUAGE sql\r\nIMMUTABLE\r\nSET search_path = ''\r\nAS $$\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.sync_currency_price_maps()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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_legacy_price_columns_for_currency(target_currency text)\r\nRETURNS void\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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.set_currency_defaults()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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.handle_default_currency_change()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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.clear_currency_price_overrides(target_currency text)\r\nRETURNS void\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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.sync_shipping_method_currency_maps()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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\nALTER TABLE public.products\r\n ADD CONSTRAINT products_prices_is_valid\r\n CHECK (public.is_valid_currency_amount_map(prices)),\r\n ADD CONSTRAINT products_sale_prices_are_valid\r\n CHECK (public.is_valid_sale_price_map(prices, sale_prices));\r\n\r\nALTER TABLE public.product_variants\r\n ADD CONSTRAINT product_variants_prices_is_valid\r\n CHECK (public.is_valid_currency_amount_map(prices)),\r\n ADD CONSTRAINT product_variants_sale_prices_are_valid\r\n CHECK (public.is_valid_sale_price_map(prices, sale_prices));\r\n\r\nALTER TABLE public.shipping_zone_methods\r\n ADD CONSTRAINT shipping_zone_methods_cost_amounts_valid\r\n CHECK (public.is_valid_currency_amount_map(cost_amounts)),\r\n ADD CONSTRAINT shipping_zone_methods_min_order_amounts_valid\r\n CHECK (public.is_valid_currency_amount_map(min_order_amounts)),\r\n ADD CONSTRAINT shipping_zone_methods_cost_amounts_include_source\r\n CHECK (cost_amounts ? upper(cost_currency)),\r\n ADD CONSTRAINT shipping_zone_methods_min_order_amounts_include_source\r\n CHECK (min_order_amounts ? upper(cost_currency));\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\r\n BEFORE INSERT OR UPDATE ON public.shipping_zone_locations\r\n FOR EACH ROW\r\n 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\r\n BEFORE INSERT OR UPDATE ON public.tax_rates\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_tax_rates_write();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_products_currency_prices ON public.products;\r\nCREATE TRIGGER trg_sync_products_currency_prices\r\n BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices\r\n ON public.products\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.sync_currency_price_maps();\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\r\n BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices\r\n ON public.product_variants\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.sync_currency_price_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_set_currency_defaults ON public.currencies;\r\nCREATE TRIGGER trg_set_currency_defaults\r\n BEFORE INSERT OR UPDATE ON public.currencies\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.set_currency_defaults();\r\n\r\nDROP TRIGGER IF EXISTS trg_handle_default_currency_change ON public.currencies;\r\nCREATE TRIGGER trg_handle_default_currency_change\r\n AFTER INSERT OR UPDATE ON public.currencies\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_default_currency_change();\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\r\n BEFORE INSERT OR UPDATE OF cost_amount, cost_currency, min_order_amount, currency_pricing_mode, cost_amounts, min_order_amounts\r\n ON public.shipping_zone_methods\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.sync_shipping_method_currency_maps();\r\n\r\nGRANT EXECUTE ON FUNCTION public.clear_currency_price_overrides(text) TO authenticated;\r\nGRANT EXECUTE ON FUNCTION public.clear_currency_price_overrides(text) TO service_role;\r\n\r\n-- 00000000000032_seed_shipping_defaults.sql\r\n-- Default North America shipping zone and methods.\r\n\r\nDO $$\r\nDECLARE\r\n v_zone_id uuid;\r\nBEGIN\r\n INSERT INTO public.shipping_zones (name, priority_order)\r\n VALUES ('North America', 10)\r\n RETURNING id INTO v_zone_id;\r\n\r\n INSERT INTO public.shipping_zone_locations (zone_id, country_code)\r\n VALUES\r\n (v_zone_id, 'US'),\r\n (v_zone_id, 'CA'),\r\n (v_zone_id, 'MX');\r\n\r\n INSERT INTO public.shipping_zone_methods (\r\n zone_id,\r\n method_type,\r\n cost_amount,\r\n name,\r\n min_order_amount,\r\n name_translations\r\n )\r\n VALUES\r\n (\r\n v_zone_id,\r\n 'flat_rate',\r\n 1500,\r\n 'Standard Shipping',\r\n 0,\r\n '{\"fr\": \"Livraison standard\"}'::jsonb\r\n ),\r\n (\r\n v_zone_id,\r\n 'free_shipping',\r\n 0,\r\n 'Free Shipping (Orders over $100)',\r\n 10000,\r\n '{\"fr\": \"Livraison gratuite (commandes de plus de 100 $)\"}'::jsonb\r\n );\r\nEND\r\n$$;\r\n"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"version": "00000000000005",
|
|
44
|
+
"name": "00000000000005_setup_functions_and_triggers.sql",
|
|
45
|
+
"sql": "-- 00000000000005_setup_functions_and_triggers.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000015_setup_core_functions_and_triggers.sql\r\n-- Shared auth helpers and CMS timestamp triggers.\r\n\r\nCREATE OR REPLACE FUNCTION public.get_my_claim(claim text)\r\nRETURNS jsonb\r\nLANGUAGE sql\r\nSTABLE\r\nSET search_path = ''\r\nAS $$\r\n SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_current_user_role()\r\nRETURNS public.user_role\r\nLANGUAGE sql\r\nSTABLE\r\nSET search_path = public\r\nAS $$\r\n SELECT role FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_admin()\r\nRETURNS boolean\r\nLANGUAGE sql\r\nSTABLE\r\nSET search_path = public\r\nAS $$\r\n SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_new_user()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSECURITY DEFINER\r\nSET search_path = public\r\nAS $$\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\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\n\r\nREVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;\r\n\r\nDO $$\r\nDECLARE\r\n missing_user record;\r\n v_github_username text;\r\n v_full_name text;\r\n v_role public.user_role;\r\n v_admin_exists boolean;\r\nBEGIN\r\n SELECT EXISTS (SELECT 1 FROM public.profiles WHERE role = 'ADMIN')\r\n INTO v_admin_exists;\r\n\r\n FOR missing_user IN\r\n SELECT *\r\n FROM auth.users\r\n WHERE id NOT IN (SELECT id FROM public.profiles)\r\n ORDER BY created_at ASC\r\n LOOP\r\n IF missing_user.raw_app_meta_data->>'provider' = 'github'\r\n OR (missing_user.raw_user_meta_data->>'iss') LIKE '%github%' THEN\r\n v_github_username := COALESCE(\r\n missing_user.raw_user_meta_data->>'user_name',\r\n missing_user.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 v_full_name := missing_user.raw_user_meta_data->>'full_name';\r\n\r\n IF v_admin_exists = false THEN\r\n v_role := 'ADMIN';\r\n v_admin_exists := true;\r\n\r\n INSERT INTO public.site_settings (key, value)\r\n VALUES ('is_admin_created', 'true'::jsonb)\r\n ON CONFLICT (key) DO UPDATE\r\n SET value = 'true'::jsonb;\r\n ELSE\r\n v_role := 'USER';\r\n END IF;\r\n\r\n INSERT INTO public.profiles (id, role, full_name, avatar_url, github_username)\r\n VALUES (\r\n missing_user.id,\r\n v_role,\r\n v_full_name,\r\n missing_user.raw_user_meta_data->>'avatar_url',\r\n v_github_username\r\n )\r\n ON CONFLICT (id) DO NOTHING;\r\n END LOOP;\r\nEND\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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.handle_languages_update()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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_pages_update()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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_blocks_update()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nDROP TRIGGER IF EXISTS set_updated_at ON public.translations;\r\nCREATE TRIGGER set_updated_at\r\n BEFORE UPDATE ON public.translations\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS on_languages_update ON public.languages;\r\nCREATE TRIGGER on_languages_update\r\n BEFORE UPDATE ON public.languages\r\n FOR EACH ROW\r\n 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\r\n BEFORE UPDATE ON public.media\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_media_update();\r\n\r\nDROP TRIGGER IF EXISTS on_posts_update ON public.posts;\r\nCREATE TRIGGER on_posts_update\r\n BEFORE UPDATE ON public.posts\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_posts_update();\r\n\r\nDROP TRIGGER IF EXISTS on_pages_update ON public.pages;\r\nCREATE TRIGGER on_pages_update\r\n BEFORE UPDATE ON public.pages\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_pages_update();\r\n\r\nDROP TRIGGER IF EXISTS on_blocks_update ON public.blocks;\r\nCREATE TRIGGER on_blocks_update\r\n BEFORE UPDATE ON public.blocks\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_blocks_update();\r\n\r\nDROP TRIGGER IF EXISTS on_navigation_items_update ON public.navigation_items;\r\nCREATE TRIGGER on_navigation_items_update\r\n BEFORE UPDATE ON public.navigation_items\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_navigation_items_update();\r\n\r\n-- 00000000000016_setup_ecommerce_functions_and_triggers.sql\r\n-- Product RPCs, inventory sync, and invoice helpers.\r\n\r\nCREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities()\r\nRETURNS boolean\r\nLANGUAGE plpgsql\r\nSTABLE\r\nSET search_path = public\r\nAS $$\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.format_order_invoice_number(p_value bigint)\r\nRETURNS text\r\nLANGUAGE sql\r\nIMMUTABLE\r\nSET search_path = ''\r\nAS $$\r\n SELECT 'INV-' || lpad(p_value::text, 6, '0');\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.generate_order_invoice_number()\r\nRETURNS text\r\nLANGUAGE sql\r\nVOLATILE\r\nSET search_path = ''\r\nAS $$\r\n SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(\r\n p_order_id uuid,\r\n p_paid_at timestamptz DEFAULT now()\r\n)\r\nRETURNS TABLE(invoice_number text, paid_at timestamptz)\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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.upsert_product_with_variants(product_payload jsonb)\r\nRETURNS uuid\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $function$\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 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 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 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 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 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$function$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_inventory_items_update()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text)\r\nRETURNS void\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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.handle_inventory_item_change()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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.apply_order_inventory_deduction(p_order_id uuid)\r\nRETURNS void\r\nLANGUAGE plpgsql\r\nSET search_path = public\r\nAS $$\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\nDROP TRIGGER IF EXISTS on_inventory_items_update ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_items_update\r\n BEFORE UPDATE ON public.inventory_items\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_inventory_items_update();\r\n\r\nDROP TRIGGER IF EXISTS on_inventory_item_change ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_item_change\r\n AFTER INSERT OR UPDATE OF quantity OR DELETE ON public.inventory_items\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_inventory_item_change();\r\n\r\nGRANT EXECUTE ON FUNCTION public.get_ecommerce_track_quantities() TO authenticated;\r\nGRANT EXECUTE ON FUNCTION public.get_ecommerce_track_quantities() TO service_role;\r\nGRANT EXECUTE ON FUNCTION public.generate_order_invoice_number() TO authenticated;\r\nGRANT EXECUTE ON FUNCTION public.generate_order_invoice_number() TO service_role;\r\nGRANT EXECUTE ON FUNCTION public.assign_order_invoice_metadata(uuid, timestamptz) TO authenticated;\r\nGRANT EXECUTE ON FUNCTION public.assign_order_invoice_metadata(uuid, timestamptz) TO service_role;\r\nGRANT EXECUTE ON FUNCTION public.upsert_product_with_variants(jsonb) TO authenticated;\r\nGRANT EXECUTE ON FUNCTION public.upsert_product_with_variants(jsonb) TO service_role;\r\nGRANT EXECUTE ON FUNCTION public.sync_inventory_cache_for_sku(text) TO authenticated;\r\nGRANT EXECUTE ON FUNCTION public.sync_inventory_cache_for_sku(text) TO service_role;\r\nGRANT EXECUTE ON FUNCTION public.apply_order_inventory_deduction(uuid) TO authenticated;\r\nGRANT EXECUTE ON FUNCTION public.apply_order_inventory_deduction(uuid) TO service_role;\r\n"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"version": "00000000000006",
|
|
49
|
+
"name": "00000000000006_setup_rls_and_grants.sql",
|
|
50
|
+
"sql": "-- 00000000000006_setup_rls_and_grants.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000018_setup_core_cms_rls.sql\n-- RLS, grants, and core admin/public access policies.\n\nGRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO anon;\nGRANT ALL ON ALL TABLES IN SCHEMA public TO authenticated;\nGRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated;\nGRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;\nGRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO service_role;\n\nALTER TABLE public.site_settings ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.user_addresses ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.languages ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.media ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.translations ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.logos ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY profiles_read_policy\n ON public.profiles\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY profiles_service_role_policy\n ON public.profiles\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY profiles_update_policy\n ON public.profiles\n FOR UPDATE\n TO authenticated\n USING (\n (id = (SELECT auth.uid()))\n OR ((SELECT public.get_current_user_role()) = 'ADMIN')\n )\n WITH CHECK (\n (id = (SELECT auth.uid()))\n OR ((SELECT public.get_current_user_role()) = 'ADMIN')\n );\n\nCREATE POLICY profiles_insert_policy\n ON public.profiles\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY \"Users can manage own addresses\"\n ON public.user_addresses\n FOR ALL\n TO authenticated\n USING (user_id = (SELECT auth.uid()))\n WITH CHECK (user_id = (SELECT auth.uid()));\n\nCREATE POLICY \"Service role manages all addresses\"\n ON public.user_addresses\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY languages_read_policy\n ON public.languages\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY languages_insert_policy\n ON public.languages\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY languages_update_policy\n ON public.languages\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN')\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY languages_delete_policy\n ON public.languages\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY media_read_policy\n ON public.media\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY media_insert_policy\n ON public.media\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY media_update_policy\n ON public.media\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY media_delete_policy\n ON public.media\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY media_service_role_policy\n ON public.media\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY site_settings_read_policy\n ON public.site_settings\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY site_settings_insert_policy\n ON public.site_settings\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY site_settings_update_policy\n ON public.site_settings\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY site_settings_delete_policy\n ON public.site_settings\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY translations_read_policy\n ON public.translations\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY translations_insert_policy\n ON public.translations\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY translations_update_policy\n ON public.translations\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY translations_delete_policy\n ON public.translations\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY logos_read_policy\n ON public.logos\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY logos_insert_policy\n ON public.logos\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY logos_update_policy\n ON public.logos\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN')\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY logos_delete_policy\n ON public.logos\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\r\n\r\n-- 00000000000019_setup_content_rls.sql\n-- RLS policies for authoring and published content access.\n\nALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.blocks ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.navigation_items ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.page_revisions ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.post_revisions ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY pages_anon_read_policy\n ON public.pages\n FOR SELECT\n TO anon\n USING (status = 'published');\n\nCREATE POLICY pages_read_policy\n ON public.pages\n FOR SELECT\n TO authenticated\n USING (\n (status = 'published')\n OR (author_id = (SELECT auth.uid()) AND status <> 'published')\n OR ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n );\n\nCREATE POLICY pages_insert_policy\n ON public.pages\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY pages_update_policy\n ON public.pages\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY pages_delete_policy\n ON public.pages\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY posts_anon_read_policy\n ON public.posts\n FOR SELECT\n TO anon\n USING (status = 'published' AND (published_at IS NULL OR published_at <= now()));\n\nCREATE POLICY posts_read_policy\n ON public.posts\n FOR SELECT\n TO authenticated\n USING (\n (\n status = 'published'\n AND (published_at IS NULL OR published_at <= now())\n )\n OR (author_id = (SELECT auth.uid()) AND status <> 'published')\n OR ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n );\n\nCREATE POLICY posts_insert_policy\n ON public.posts\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY posts_update_policy\n ON public.posts\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY posts_delete_policy\n ON public.posts\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY blocks_anon_read_policy\n ON public.blocks\n FOR SELECT\n TO anon\n USING (\n (\n page_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.pages AS p\n WHERE p.id = blocks.page_id\n AND p.status = 'published'\n )\n )\n OR (\n post_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.posts AS pt\n WHERE pt.id = blocks.post_id\n AND pt.status = 'published'\n AND (pt.published_at IS NULL OR pt.published_at <= now())\n )\n )\n );\n\nCREATE POLICY blocks_read_policy\n ON public.blocks\n FOR SELECT\n TO authenticated\n USING (\n ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n OR (\n (\n page_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.pages AS p\n WHERE p.id = blocks.page_id\n AND p.status = 'published'\n )\n )\n OR (\n post_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.posts AS pt\n WHERE pt.id = blocks.post_id\n AND pt.status = 'published'\n AND (pt.published_at IS NULL OR pt.published_at <= now())\n )\n )\n )\n );\n\nCREATE POLICY blocks_insert_policy\n ON public.blocks\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY blocks_update_policy\n ON public.blocks\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY blocks_delete_policy\n ON public.blocks\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY navigation_read_policy\n ON public.navigation_items\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY navigation_items_insert_policy\n ON public.navigation_items\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY navigation_items_update_policy\n ON public.navigation_items\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN')\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY navigation_items_delete_policy\n ON public.navigation_items\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nCREATE POLICY page_revisions_read_policy\n ON public.page_revisions\n FOR SELECT\n TO authenticated\n USING (true);\n\nCREATE POLICY page_revisions_insert_policy\n ON public.page_revisions\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY page_revisions_update_policy\n ON public.page_revisions\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY page_revisions_delete_policy\n ON public.page_revisions\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY post_revisions_read_policy\n ON public.post_revisions\n FOR SELECT\n TO authenticated\n USING (true);\n\nCREATE POLICY post_revisions_insert_policy\n ON public.post_revisions\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY post_revisions_update_policy\n ON public.post_revisions\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nCREATE POLICY post_revisions_delete_policy\n ON public.post_revisions\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\r\n\r\n-- 00000000000020_setup_commerce_and_financial_rls.sql\n-- Consolidated RLS for commerce, licensing, shipping, tax, and currency tables.\n\nALTER TABLE public.products ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.product_media ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.product_attributes ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.product_attribute_terms ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.product_variants ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.variant_attribute_mapping ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.order_items ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.package_activations ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.freemius_plans ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.freemius_pricing ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.inventory_items ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.shipping_zones ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.shipping_zone_locations ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.shipping_zone_methods ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.tax_rates ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.currencies ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY \"Public can view products\"\n ON public.products\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY products_insert_policy\n ON public.products\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY products_update_policy\n ON public.products\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY products_delete_policy\n ON public.products\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public can view product media\"\n ON public.product_media\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY product_media_insert_policy\n ON public.product_media\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_media_update_policy\n ON public.product_media\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_media_delete_policy\n ON public.product_media\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public read product_attributes\"\n ON public.product_attributes\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY product_attributes_insert_policy\n ON public.product_attributes\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_attributes_update_policy\n ON public.product_attributes\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_attributes_delete_policy\n ON public.product_attributes\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public read product_attribute_terms\"\n ON public.product_attribute_terms\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY product_attribute_terms_insert_policy\n ON public.product_attribute_terms\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_attribute_terms_update_policy\n ON public.product_attribute_terms\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_attribute_terms_delete_policy\n ON public.product_attribute_terms\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public read product_variants\"\n ON public.product_variants\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY product_variants_insert_policy\n ON public.product_variants\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_variants_update_policy\n ON public.product_variants\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_variants_delete_policy\n ON public.product_variants\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public read variant_attribute_mapping\"\n ON public.variant_attribute_mapping\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY variant_attribute_mapping_insert_policy\n ON public.variant_attribute_mapping\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY variant_attribute_mapping_update_policy\n ON public.variant_attribute_mapping\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY variant_attribute_mapping_delete_policy\n ON public.variant_attribute_mapping\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Users can view own orders\"\n ON public.orders\n FOR SELECT\n TO authenticated\n USING (\n ((SELECT public.is_admin()) IS TRUE)\n OR (user_id = (SELECT auth.uid()))\n );\n\nCREATE POLICY orders_insert_policy\n ON public.orders\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY orders_update_policy\n ON public.orders\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY orders_delete_policy\n ON public.orders\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Service Role manages orders\"\n ON public.orders\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY \"Users can view own order items\"\n ON public.order_items\n FOR SELECT\n TO authenticated\n USING (\n ((SELECT public.is_admin()) IS TRUE)\n OR EXISTS (\n SELECT 1\n FROM public.orders\n WHERE orders.id = order_items.order_id\n AND orders.user_id = (SELECT auth.uid())\n )\n );\n\nCREATE POLICY order_items_insert_policy\n ON public.order_items\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY order_items_update_policy\n ON public.order_items\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY order_items_delete_policy\n ON public.order_items\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Service Role manages order items\"\n ON public.order_items\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY \"Allow service role full access\"\n ON public.package_activations\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY \"Allow authenticated read access\"\n ON public.package_activations\n FOR SELECT\n TO authenticated\n USING (true);\n\nCREATE POLICY \"Public read access for freemius_plans\"\n ON public.freemius_plans\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY \"Public read access for freemius_pricing\"\n ON public.freemius_pricing\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY \"Public can view inventory items\"\n ON public.inventory_items\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY inventory_items_insert_policy\n ON public.inventory_items\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY inventory_items_update_policy\n ON public.inventory_items\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY inventory_items_delete_policy\n ON public.inventory_items\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Service Role manages inventory items\"\n ON public.inventory_items\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY \"Public read shipping_zones\"\n ON public.shipping_zones\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY shipping_zones_insert_policy\n ON public.shipping_zones\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY shipping_zones_update_policy\n ON public.shipping_zones\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY shipping_zones_delete_policy\n ON public.shipping_zones\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public read shipping_zone_locations\"\n ON public.shipping_zone_locations\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY shipping_zone_locations_insert_policy\n ON public.shipping_zone_locations\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY shipping_zone_locations_update_policy\n ON public.shipping_zone_locations\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY shipping_zone_locations_delete_policy\n ON public.shipping_zone_locations\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public read shipping_zone_methods\"\n ON public.shipping_zone_methods\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY shipping_zone_methods_insert_policy\n ON public.shipping_zone_methods\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY shipping_zone_methods_update_policy\n ON public.shipping_zone_methods\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY shipping_zone_methods_delete_policy\n ON public.shipping_zone_methods\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Public read tax_rates\"\n ON public.tax_rates\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY tax_rates_insert_policy\n ON public.tax_rates\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY tax_rates_update_policy\n ON public.tax_rates\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY tax_rates_delete_policy\n ON public.tax_rates\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Service Role manages tax_rates\"\n ON public.tax_rates\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nCREATE POLICY \"Public read active currencies\"\n ON public.currencies\n FOR SELECT\n TO anon, authenticated\n USING (is_active = true);\n\nCREATE POLICY currencies_insert_policy\n ON public.currencies\n FOR INSERT\n TO authenticated\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY currencies_update_policy\n ON public.currencies\n FOR UPDATE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY currencies_delete_policy\n ON public.currencies\n FOR DELETE\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Service role manages currencies\"\n ON public.currencies\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\r\n"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"version": "00000000000007",
|
|
54
|
+
"name": "00000000000007_setup_indexes.sql",
|
|
55
|
+
"sql": "-- 00000000000007_setup_indexes.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000021_setup_cms_indexes.sql\r\n-- Supporting indexes for accounts, content, and revisions.\r\n\r\nCREATE INDEX idx_user_addresses_user_id\r\n ON public.user_addresses (user_id);\r\n\r\nCREATE INDEX idx_user_addresses_type\r\n ON public.user_addresses (address_type);\r\n\r\nCREATE INDEX idx_media_uploader_id\r\n ON public.media (uploader_id);\r\n\r\nCREATE INDEX media_folder_idx\r\n ON public.media (folder);\r\n\r\nCREATE INDEX idx_posts_feature_image_id\r\n ON public.posts (feature_image_id);\r\n\r\nCREATE INDEX idx_posts_author_id\r\n ON public.posts (author_id);\r\n\r\nCREATE INDEX idx_posts_translation_group_id\r\n ON public.posts (translation_group_id);\r\n\r\nCREATE INDEX idx_pages_author_id\r\n ON public.pages (author_id);\r\n\r\nCREATE INDEX idx_pages_translation_group_id\r\n ON public.pages (translation_group_id);\r\n\r\nCREATE INDEX idx_blocks_language_id\r\n ON public.blocks (language_id);\r\n\r\nCREATE INDEX idx_blocks_page_id\r\n ON public.blocks (page_id);\r\n\r\nCREATE INDEX idx_blocks_post_id\r\n ON public.blocks (post_id);\r\n\r\nCREATE INDEX idx_navigation_items_menu_lang_order\r\n ON public.navigation_items (menu_key, language_id, \"order\");\r\n\r\nCREATE INDEX idx_navigation_items_language_id\r\n ON public.navigation_items (language_id);\r\n\r\nCREATE INDEX idx_navigation_items_page_id\r\n ON public.navigation_items (page_id);\r\n\r\nCREATE INDEX idx_navigation_items_parent_id\r\n ON public.navigation_items (parent_id);\r\n\r\nCREATE INDEX idx_logos_media_id\r\n ON public.logos (media_id);\r\n\r\nCREATE INDEX idx_page_revisions_page_id_version\r\n ON public.page_revisions (page_id, version);\r\n\r\nCREATE INDEX idx_page_revisions_author_id\r\n ON public.page_revisions (author_id);\r\n\r\nCREATE INDEX idx_post_revisions_post_id_version\r\n ON public.post_revisions (post_id, version);\r\n\r\nCREATE INDEX idx_post_revisions_author_id\r\n ON public.post_revisions (author_id);\r\n\r\n-- 00000000000022_setup_commerce_indexes.sql\r\n-- Consolidated commerce, shipping, and financial indexes.\r\n\r\nCREATE INDEX idx_products_slug\r\n ON public.products (slug);\r\n\r\nCREATE INDEX idx_products_translation_group_id\r\n ON public.products (translation_group_id);\r\n\r\nCREATE INDEX idx_products_prices_gin\r\n ON public.products\r\n USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX idx_product_media_product_id\r\n ON public.product_media (product_id);\r\n\r\nCREATE INDEX idx_product_media_media_id\r\n ON public.product_media (media_id);\r\n\r\nCREATE INDEX idx_order_items_order_id\r\n ON public.order_items (order_id);\r\n\r\nCREATE INDEX idx_order_items_variant_id\r\n ON public.order_items (variant_id);\r\n\r\nCREATE INDEX idx_order_items_product_id\r\n ON public.order_items (product_id);\r\n\r\nCREATE INDEX idx_orders_user_id\r\n ON public.orders (user_id);\r\n\r\nCREATE INDEX idx_orders_freemius_license_id\r\n ON public.orders (freemius_license_id)\r\n WHERE freemius_license_id IS NOT NULL;\r\n\r\nCREATE INDEX idx_orders_freemius_subscription_id\r\n ON public.orders (freemius_subscription_id)\r\n WHERE freemius_subscription_id IS NOT NULL;\r\n\r\nCREATE INDEX idx_orders_freemius_trial_id\r\n ON public.orders (freemius_trial_id)\r\n WHERE freemius_trial_id IS NOT NULL;\r\n\r\nCREATE INDEX idx_package_activations_package_id\r\n ON public.package_activations (package_id);\r\n\r\nCREATE INDEX idx_package_activations_license_key\r\n ON public.package_activations (license_key);\r\n\r\nCREATE INDEX idx_freemius_plans_product_id\r\n ON public.freemius_plans (product_id);\r\n\r\nCREATE INDEX idx_freemius_pricing_plan_id\r\n ON public.freemius_pricing (plan_id);\r\n\r\nCREATE INDEX idx_product_attribute_terms_attribute_id\r\n ON public.product_attribute_terms (attribute_id);\r\n\r\nCREATE INDEX idx_product_variants_product_id\r\n ON public.product_variants (product_id);\r\n\r\nCREATE INDEX idx_product_variants_main_media_id\r\n ON public.product_variants (main_media_id);\r\n\r\nCREATE INDEX idx_product_variants_prices_gin\r\n ON public.product_variants\r\n USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX idx_variant_attribute_mapping_attribute_term_id\r\n ON public.variant_attribute_mapping (attribute_term_id);\r\n\r\nCREATE INDEX idx_inventory_items_updated_at\r\n ON public.inventory_items (updated_at DESC);\r\n\r\nCREATE INDEX idx_shipping_zone_locations_zone_id\r\n ON public.shipping_zone_locations (zone_id);\r\n\r\nCREATE INDEX idx_shipping_zone_locations_country_state_postal\r\n ON public.shipping_zone_locations (country_code, state_code, postal_code);\r\n\r\nCREATE INDEX idx_shipping_zone_methods_name_translations\r\n ON public.shipping_zone_methods\r\n USING gin (name_translations);\r\n\r\nCREATE INDEX idx_shipping_zone_methods_zone_id\r\n ON public.shipping_zone_methods (zone_id);\r\n\r\nCREATE INDEX idx_tax_rates_country_state\r\n ON public.tax_rates (country_code, state_code);\r\n"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"version": "00000000000008",
|
|
59
|
+
"name": "00000000000008_seed_platform_defaults.sql",
|
|
60
|
+
"sql": "-- 00000000000008_seed_platform_defaults.sql\n-- Consolidated migration preserving original statement order within grouped sections.\n\n-- 00000000000023_seed_platform_defaults.sql\n-- Default settings, base languages, and the store currency seed.\n\nINSERT INTO public.site_settings (key, value)\nVALUES ('footer_copyright', '{\"en\": \"© {year} Nextblock CMS. All rights reserved.\", \"fr\": \"© {year} Nextblock CMS. Tous droits réservés.\"}')\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.site_settings (key, value)\nVALUES ('is_admin_created', 'false'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.site_settings (key, value)\nVALUES (\n 'enabled_payment_providers',\n '{\"stripe\": false, \"freemius\": false}'::jsonb\n)\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.site_settings (key, value)\nVALUES ('site_title', '\"NextBlock™ CMS\"'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.site_settings (key, value)\nVALUES ('site_description', '\"NextBlock™ is an open-source CMS on Next.js + Supabase — a visual block editor, blazing-fast multilingual pages, and built-in e-commerce.\"'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.site_settings (key, value)\nVALUES ('site_keywords', '\"NextBlock, CMS, Next.js, Supabase, headless CMS, block editor, visual page builder, multilingual, e-commerce, open source\"'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.site_settings (key, value)\nVALUES (\n 'ecommerce_inventory_settings',\n '{\"track_quantities\": true, \"enable_taxes\": false}'::jsonb\n)\nON CONFLICT (key) DO UPDATE\nSET value = CASE\n WHEN jsonb_typeof(site_settings.value) = 'object' THEN\n jsonb_set(\n jsonb_set(\n site_settings.value,\n '{track_quantities}',\n COALESCE(\n site_settings.value->'track_quantities',\n site_settings.value->'trackQuantities',\n 'true'::jsonb\n ),\n true\n ),\n '{enable_taxes}',\n COALESCE(\n site_settings.value->'enable_taxes',\n site_settings.value->'enableTaxes',\n 'false'::jsonb\n ),\n true\n )\n ELSE\n jsonb_build_object(\n 'track_quantities',\n CASE\n WHEN lower(trim(BOTH '\"' FROM site_settings.value::text)) IN ('false', 'f', '0', 'no', 'off') THEN false\n ELSE true\n END,\n 'enable_taxes',\n false\n )\nEND;\n\nINSERT INTO public.site_settings (key, value)\nVALUES (\n 'invoice_settings',\n '{\n \"business_name\": \"\",\n \"email\": \"\",\n \"phone\": \"\",\n \"address\": {\n \"line1\": \"\",\n \"line2\": \"\",\n \"city\": \"\",\n \"state\": \"\",\n \"postal_code\": \"\",\n \"country_code\": \"CA\"\n },\n \"tax_registrations\": []\n }'::jsonb\n)\nON CONFLICT (key) DO UPDATE\nSET value = CASE\n WHEN jsonb_typeof(site_settings.value) = 'object' THEN\n jsonb_build_object(\n 'business_name', COALESCE(site_settings.value->>'business_name', ''),\n 'email', COALESCE(site_settings.value->>'email', ''),\n 'phone', COALESCE(site_settings.value->>'phone', ''),\n 'address', CASE\n WHEN jsonb_typeof(site_settings.value->'address') = 'object' THEN\n jsonb_build_object(\n 'line1', COALESCE(site_settings.value->'address'->>'line1', ''),\n 'line2', COALESCE(site_settings.value->'address'->>'line2', ''),\n 'city', COALESCE(site_settings.value->'address'->>'city', ''),\n 'state', COALESCE(site_settings.value->'address'->>'state', ''),\n 'postal_code', COALESCE(site_settings.value->'address'->>'postal_code', ''),\n 'country_code', COALESCE(NULLIF(site_settings.value->'address'->>'country_code', ''), 'CA')\n )\n ELSE\n jsonb_build_object(\n 'line1', '',\n 'line2', '',\n 'city', '',\n 'state', '',\n 'postal_code', '',\n 'country_code', 'CA'\n )\n END,\n 'tax_registrations', CASE\n WHEN jsonb_typeof(site_settings.value->'tax_registrations') = 'array' THEN\n site_settings.value->'tax_registrations'\n ELSE\n '[]'::jsonb\n END\n )\n ELSE\n '{\n \"business_name\": \"\",\n \"email\": \"\",\n \"phone\": \"\",\n \"address\": {\n \"line1\": \"\",\n \"line2\": \"\",\n \"city\": \"\",\n \"state\": \"\",\n \"postal_code\": \"\",\n \"country_code\": \"CA\"\n },\n \"tax_registrations\": []\n }'::jsonb\nEND;\n\nINSERT INTO public.languages (code, name, is_default, is_active)\nVALUES\n ('en', 'English', true, true),\n ('fr', 'Français', false, true);\n\nINSERT INTO public.currencies (code, symbol, exchange_rate, is_default, is_active)\nVALUES ('USD', '$', 1, true, true)\nON CONFLICT (code) DO UPDATE\nSET\n symbol = EXCLUDED.symbol,\n exchange_rate = EXCLUDED.exchange_rate,\n is_default = EXCLUDED.is_default,\n is_active = EXCLUDED.is_active,\n updated_at = now();\n"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"version": "00000000000009",
|
|
64
|
+
"name": "00000000000009_seed_translations.sql",
|
|
65
|
+
"sql": "-- 00000000000009_seed_translations.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000024_seed_core_translations.sql\r\n-- Base profile, account, and storefront translations.\r\n\r\nINSERT INTO public.translations (key, translations)\r\nVALUES \r\n ('continue_with_github', '{\"en\": \"Continue with GitHub\", \"es\": \"Continuar con GitHub\", \"fr\": \"Continuer avec GitHub\"}'::jsonb),\r\n ('or_continue_with', '{\"en\": \"Or continue with\", \"es\": \"O continuar con\", \"fr\": \"Ou continuer avec\"}'::jsonb),\r\n ('customer_profile', '{\"en\": \"Customer Profile\", \"es\": \"Perfil de Cliente\", \"fr\": \"Profil Client\"}'::jsonb),\r\n ('personal_information', '{\"en\": \"Personal Information\", \"es\": \"Información Personal\", \"fr\": \"Informations Personnelles\"}'::jsonb),\r\n ('full_name', '{\"en\": \"Full Name\", \"es\": \"Nombre Completo\", \"fr\": \"Nom Complet\"}'::jsonb),\r\n ('github_username', '{\"en\": \"GitHub Username\", \"es\": \"Nombre de usuario de GitHub\", \"fr\": \"Nom d''utilisateur GitHub\"}'::jsonb),\r\n ('github_username_help', '{\"en\": \"Required only for purchasing developer licenses.\", \"es\": \"Requerido solo para comprar licencias de desarrollador.\", \"fr\": \"Requis uniquement pour l''achat de licences développeur.\"}'::jsonb),\r\n ('phone_number', '{\"en\": \"Phone Number\", \"es\": \"Número de Teléfono\", \"fr\": \"Numéro de Téléphone\"}'::jsonb),\r\n ('billing_address', '{\"en\": \"Billing Address\", \"es\": \"Dirección de Facturación\", \"fr\": \"Adresse de Facturation\"}'::jsonb),\r\n ('address_line_1', '{\"en\": \"Address Line 1\", \"es\": \"Dirección Línea 1\", \"fr\": \"Adresse Ligne 1\"}'::jsonb),\r\n ('address_line_2', '{\"en\": \"Address Line 2 (Optional)\", \"es\": \"Dirección Línea 2 (Opcional)\", \"fr\": \"Adresse Ligne 2 (Optionnel)\"}'::jsonb),\r\n ('city', '{\"en\": \"City\", \"es\": \"Ciudad\", \"fr\": \"Ville\"}'::jsonb),\r\n ('state_province', '{\"en\": \"State / Province\", \"es\": \"Estado / Provincia\", \"fr\": \"État / Province\"}'::jsonb),\r\n ('postal_zip_code', '{\"en\": \"Postal / Zip Code\", \"es\": \"Código Postal\", \"fr\": \"Code Postal\"}'::jsonb),\r\n ('country', '{\"en\": \"Country\", \"es\": \"País\", \"fr\": \"Pays\"}'::jsonb),\r\n ('save_profile', '{\"en\": \"Save Profile\", \"es\": \"Guardar Perfil\", \"fr\": \"Enregistrer le Profil\"}'::jsonb),\r\n ('saving', '{\"en\": \"Saving...\", \"es\": \"Guardando...\", \"fr\": \"Enregistrement...\"}'::jsonb),\r\n ('profile_updated_success', '{\"en\": \"Profile updated successfully\", \"es\": \"Perfil actualizado con éxito\", \"fr\": \"Profil mis à jour avec succès\"}'::jsonb),\r\n ('profile_update_failed', '{\"en\": \"Failed to update profile\", \"es\": \"Error al actualizar el perfil\", \"fr\": \"Échec de la mise à jour du profil\"}'::jsonb),\r\n ('address_required', '{\"en\": \"Address is required\", \"es\": \"La dirección es obligatoria\", \"fr\": \"L''adresse est requise\"}'::jsonb),\r\n ('city_required', '{\"en\": \"City is required\", \"es\": \"La ciudad es obligatoria\", \"fr\": \"La ville est requise\"}'::jsonb),\r\n ('zip_code_required', '{\"en\": \"Zip Code is required\", \"es\": \"El código postal es obligatorio\", \"fr\": \"Le code postal est requis\"}'::jsonb),\r\n ('country_required', '{\"en\": \"Country is required\", \"es\": \"El país es obligatorio\", \"fr\": \"Le pays est requis\"}'::jsonb),\r\n ('enter_valid_json', '{\"en\": \"Enter valid JSON for billing address.\", \"es\": \"Ingrese JSON válido para la dirección de facturación.\", \"fr\": \"Entrez un JSON valide pour l''adresse de facturation.\"}'::jsonb),\r\n ('public_profile', '{\"en\": \"Public Profile\", \"es\": \"Perfil Público\", \"fr\": \"Profil Public\"}'::jsonb),\r\n ('details', '{\"en\": \"Account Details\", \"es\": \"Detalles de la Cuenta\", \"fr\": \"Détails du Compte\"}'::jsonb),\r\n ('identity', '{\"en\": \"Identity\", \"es\": \"Identidad\", \"fr\": \"Identité\"}'::jsonb),\r\n ('website', '{\"en\": \"Website\", \"es\": \"Sitio Web\", \"fr\": \"Site Web\"}'::jsonb),\r\n ('avatar_url', '{\"en\": \"Avatar URL\", \"es\": \"URL del Avatar\", \"fr\": \"URL de l''Avatar\"}'::jsonb),\r\n ('connect_github', '{\"en\": \"Connect GitHub\", \"es\": \"Conectar GitHub\", \"fr\": \"Connecter GitHub\"}'::jsonb),\r\n ('github_link_failed', '{\"en\": \"Failed to link GitHub account\", \"es\": \"Error al vincular cuenta de GitHub\", \"fr\": \"Échec de la liaison du compte GitHub\"}'::jsonb),\r\n ('save_changes', '{\"en\": \"Save Changes\", \"es\": \"Guardar Cambios\", \"fr\": \"Enregistrer les Modifications\"}'::jsonb),\r\n ('github_connected', '{\"en\": \"GitHub Connected\", \"es\": \"GitHub Conectado\", \"fr\": \"GitHub Connecté\"}'::jsonb),\r\n ('linked_to', '{\"en\": \"Linked to\", \"es\": \"Vinculado a\", \"fr\": \"Lié à\"}'::jsonb),\r\n ('optional', '{\"en\": \"Optional\", \"fr\": \"Optionnel\"}'::jsonb),\r\n ('shipping_address', '{\"en\": \"Shipping Address\", \"fr\": \"Adresse de livraison\"}'::jsonb),\r\n ('profile_settings_title', '{\"en\": \"Profile Settings\", \"fr\": \"Paramètres du profil\"}'::jsonb),\r\n ('profile_settings_description', '{\"en\": \"Keep your contact details and default addresses up to date for faster checkout.\", \"fr\": \"Gardez vos coordonnées et adresses par défaut à jour pour un paiement plus rapide.\"}'::jsonb),\r\n ('profile_not_found', '{\"en\": \"Profile not found.\", \"fr\": \"Profil introuvable.\"}'::jsonb),\r\n ('profile_basic_info_help', '{\"en\": \"This information appears on your account and helps us prepare your orders.\", \"fr\": \"Ces informations apparaissent sur votre compte et nous aident à préparer vos commandes.\"}'::jsonb),\r\n ('profile_address_defaults_help', '{\"en\": \"These default addresses are prefilled during checkout and can still be edited for each order.\", \"fr\": \"Ces adresses par défaut sont préremplies au paiement et restent modifiables pour chaque commande.\"}'::jsonb),\r\n ('use_billing_for_shipping', '{\"en\": \"Use billing address for shipping\", \"fr\": \"Utiliser l''adresse de facturation pour la livraison\"}'::jsonb),\r\n ('profile_use_billing_for_shipping_help', '{\"en\": \"Keep one default address unless you regularly ship somewhere else.\", \"fr\": \"Gardez une seule adresse par défaut sauf si vous faites souvent livrer ailleurs.\"}'::jsonb),\r\n ('checkout_complete_billing_address', '{\"en\": \"Please complete your billing address before continuing.\", \"fr\": \"Veuillez compléter votre adresse de facturation avant de continuer.\"}'::jsonb),\r\n ('checkout_complete_shipping_address', '{\"en\": \"Please complete your shipping address before continuing.\", \"fr\": \"Veuillez compléter votre adresse de livraison avant de continuer.\"}'::jsonb),\r\n ('checkout_prefill_notice', '{\"en\": \"Using your saved account details for {email}. You can still adjust them for this order.\", \"fr\": \"Nous utilisons les renseignements enregistrés pour {email}. Vous pouvez toujours les ajuster pour cette commande.\"}'::jsonb),\r\n ('checkout_billing_address_help', '{\"en\": \"We use this address for payment verification and invoicing.\", \"fr\": \"Nous utilisons cette adresse pour la vérification du paiement et la facturation.\"}'::jsonb),\r\n ('checkout_use_billing_for_shipping_help', '{\"en\": \"Uncheck this if you want your order delivered to a different address.\", \"fr\": \"Décochez ceci si vous souhaitez faire livrer votre commande à une autre adresse.\"}'::jsonb),\r\n ('checkout_shipping_address_help', '{\"en\": \"Choose where physical items should be delivered.\", \"fr\": \"Choisissez où les articles physiques doivent être livrés.\"}'::jsonb),\r\n ('checkout_payment_only_notice', '{\"en\": \"Stripe checkout is kept focused on payment because your address details are already collected here.\", \"fr\": \"Le paiement Stripe reste centré sur le paiement puisque vos coordonnées sont déjà recueillies ici.\"}'::jsonb),\r\n ('auth.signup_existing_account_hint', '{\"en\": \"That email may already be registered. Try signing in or resetting your password.\", \"fr\": \"Cette adresse e-mail est peut-être déjà utilisée. Essayez de vous connecter ou de réinitialiser votre mot de passe.\"}'::jsonb),\r\n ('auth.signup_check_email_profile', '{\"en\": \"Check your email to confirm your account. We''ll bring you to your profile next so you can finish setting up your details.\", \"fr\": \"Vérifiez votre e-mail pour confirmer votre compte. Nous vous amènerons ensuite à votre profil pour terminer la configuration de vos renseignements.\"}'::jsonb),\r\n ('ecommerce.add_to_cart', '{\"en\": \"Add to Cart\", \"fr\": \"Ajouter au panier\"}'::jsonb),\r\n ('ecommerce.added_to_cart', '{\"en\": \"Added to cart\", \"fr\": \"Ajouté au panier\"}'::jsonb),\r\n ('ecommerce.added_to_cart_success', '{\"en\": \"{item} added to cart\", \"fr\": \"{item} ajouté au panier\"}'::jsonb),\r\n ('ecommerce.added_to_cart_error', '{\"en\": \"Failed to add item to cart\", \"fr\": \"Échec de l''ajout au panier\"}'::jsonb),\r\n ('ecommerce.no_image', '{\"en\": \"No Image\", \"fr\": \"Pas d''image\"}'::jsonb),\r\n ('ecommerce.view_details', '{\"en\": \"View Details\", \"fr\": \"Voir les détails\"}'::jsonb),\r\n ('ecommerce.item_added_desc', '{\"en\": \"The item has been added to your cart.\", \"fr\": \"L''article a été ajouté à votre panier.\"}'::jsonb),\r\n ('ecommerce.checkout', '{\"en\": \"Checkout\", \"fr\": \"Paiement\"}'::jsonb),\r\n ('ecommerce.cart', '{\"en\": \"Cart\", \"fr\": \"Panier\"}'::jsonb),\r\n ('ecommerce.subtotal', '{\"en\": \"Subtotal\", \"fr\": \"Sous-total\"}'::jsonb),\r\n ('ecommerce.shipping', '{\"en\": \"Shipping\", \"fr\": \"Livraison\"}'::jsonb),\r\n ('ecommerce.tax', '{\"en\": \"Tax\", \"fr\": \"Taxes\"}'::jsonb),\r\n ('ecommerce.total', '{\"en\": \"Total\", \"fr\": \"Total\"}'::jsonb),\r\n ('ecommerce.order_summary', '{\"en\": \"Order Summary\", \"fr\": \"Résumé de la commande\"}'::jsonb),\r\n ('ecommerce.order_summary_desc', '{\"en\": \"Review your items before proceeding to payment.\", \"fr\": \"Vérifiez vos articles avant de procéder au paiement.\"}'::jsonb),\r\n ('ecommerce.payment_details', '{\"en\": \"Payment Details\", \"fr\": \"Détails du paiement\"}'::jsonb),\r\n ('ecommerce.shipping_info', '{\"en\": \"Shipping Information\", \"fr\": \"Informations de livraison\"}'::jsonb),\r\n ('ecommerce.delivery_notice', '{\"en\": \"Digital product - No shipping required\", \"fr\": \"Produit numérique - Aucune livraison requise\"}'::jsonb),\r\n ('ecommerce.back_to_shop', '{\"en\": \"Back to Shop\", \"fr\": \"Retour à la boutique\"}'::jsonb),\r\n ('ecommerce.empty_cart', '{\"en\": \"Your cart is empty\", \"fr\": \"Votre panier est vide\"}'::jsonb),\r\n ('ecommerce.no_products_found', '{\"en\": \"No products found\", \"fr\": \"Aucun produit trouvé\"}'::jsonb),\r\n ('ecommerce.featured_products', '{\"en\": \"Featured Products\", \"fr\": \"Produits vedettes\"}'::jsonb),\r\n ('ecommerce.latest_products', '{\"en\": \"Latest Products\", \"fr\": \"Derniers produits\"}'::jsonb),\r\n ('ecommerce.search_products', '{\"en\": \"Search products...\", \"fr\": \"Rechercher des produits...\"}'::jsonb),\r\n ('ecommerce.price_low_to_high', '{\"en\": \"Price: Low to High\", \"fr\": \"Prix : Croissant\"}'::jsonb),\r\n ('ecommerce.price_high_to_low', '{\"en\": \"Price: High to Low\", \"fr\": \"Prix : Décroissant\"}'::jsonb),\r\n ('ecommerce.newest', '{\"en\": \"Newest\", \"fr\": \"Nouveautés\"}'::jsonb),\r\n ('ecommerce.filters', '{\"en\": \"Filters\", \"fr\": \"Filtres\"}'::jsonb),\r\n ('ecommerce.apply_filters', '{\"en\": \"Apply Filters\", \"fr\": \"Appliquer les filtres\"}'::jsonb),\r\n ('ecommerce.clear_all', '{\"en\": \"Clear All\", \"fr\": \"Tout effacer\"}'::jsonb),\r\n ('ecommerce.digital_notice', '{\"en\": \"This is a digital product. You will receive access instructions via email after purchase.\", \"fr\": \"Ceci est un produit numérique. Vous recevrez les instructions d''accès par e-mail après l''achat.\"}'::jsonb),\r\n ('ecommerce.shopping_cart', '{\"en\": \"Shopping Cart\", \"fr\": \"Panier d''achat\"}'::jsonb),\r\n ('ecommerce.cart_empty', '{\"en\": \"Your cart is empty\", \"fr\": \"Votre panier est vide\"}'::jsonb),\r\n ('ecommerce.cart_empty_description', '{\"en\": \"Looks like you haven''t added anything to your cart yet.\", \"fr\": \"On dirait que vous n''avez encore rien ajouté à votre panier.\"}'::jsonb),\r\n ('ecommerce.continue_shopping', '{\"en\": \"Continue Shopping\", \"fr\": \"Continuer vos achats\"}'::jsonb),\r\n ('ecommerce.go_to_shop', '{\"en\": \"Go to Shop\", \"fr\": \"Aller à la boutique\"}'::jsonb),\r\n ('ecommerce.checkout_successful', '{\"en\": \"Checkout Successful\", \"fr\": \"Paiement Réussi\"}'::jsonb),\r\n ('ecommerce.sandbox_notice', '{\"en\": \"This is a Sandbox environment. The Freemius checkout is skipped here for demo purposes.\", \"fr\": \"Ceci est un environnement de bac à sable. Le paiement Freemius est sauté ici à des fins de démonstration.\"}'::jsonb),\r\n ('ecommerce.license_notice', '{\"en\": \"To purchase a real license for your self-hosted NextBlock™ instance, visit:\", \"fr\": \"Pour acheter une vraie licence pour votre instance NextBlock™ auto-hébergée, visitez :\"}'::jsonb),\r\n ('ecommerce.purchase_at', '{\"en\": \"Purchase at nextblock.ca\", \"fr\": \"Acheter sur nextblock.ca\"}'::jsonb),\r\n ('ecommerce.qty', '{\"en\": \"Qty\", \"fr\": \"Qté\"}'::jsonb),\r\n ('ecommerce.quantity', '{\"en\": \"Quantity\", \"fr\": \"Quantité\"}'::jsonb),\r\n ('ecommerce.product', '{\"en\": \"Product\", \"fr\": \"Produit\"}'::jsonb),\r\n ('ecommerce.price', '{\"en\": \"Price\", \"fr\": \"Prix\"}'::jsonb),\r\n ('ecommerce.secure_payment', '{\"en\": \"Secure payment processing\", \"fr\": \"Traitement sécurisé du paiement\"}'::jsonb),\r\n ('ecommerce.shipping_taxes_notice', '{\"en\": \"* Taxes and shipping will be calculated on the next step.\", \"fr\": \"* Les taxes et les frais de livraison seront calculés à l''étape suivante.\"}'::jsonb),\r\n ('ecommerce.shipping_taxes_calculated', '{\"en\": \"Shipping & taxes calculated at checkout.\", \"fr\": \"Livraison et taxes calculées lors du paiement.\"}'::jsonb),\r\n ('ecommerce.email_address', '{\"en\": \"Email Address\", \"fr\": \"Adresse e-mail\"}'::jsonb),\r\n ('ecommerce.pay_now', '{\"en\": \"Pay Now\", \"fr\": \"Payer maintenant\"}'::jsonb),\r\n ('ecommerce.proceed_to_checkout', '{\"en\": \"Proceed to Checkout\", \"fr\": \"Passer à la caisse\"}'::jsonb),\r\n ('ecommerce.processing', '{\"en\": \"Processing...\", \"fr\": \"Traitement...\"}'::jsonb),\r\n ('ecommerce.invalid_email', '{\"en\": \"Please enter a valid email address.\", \"fr\": \"Veuillez entrer une adresse e-mail valide.\"}'::jsonb),\r\n ('ecommerce.checkout_failed', '{\"en\": \"Checkout failed: \", \"fr\": \"Le paiement a échoué : \"}'::jsonb),\r\n ('ecommerce.generic_error', '{\"en\": \"An error occurred. Please try again.\", \"fr\": \"Une erreur est survenue. Veuillez réessayer.\"}'::jsonb),\r\n ('ecommerce.checkout_popup_blocked', '{\"en\": \"Checkout popup blocked or failed to load. Falling back to direct link.\", \"fr\": \"Le popup de paiement a été bloqué ou n''a pas pu être chargé. Retour au lien direct.\"}'::jsonb),\r\n ('ecommerce.view_full_cart', '{\"en\": \"View Full Cart\", \"fr\": \"Voir le panier complet\"}'::jsonb),\r\n ('ecommerce.ready_to_checkout', '{\"en\": \"Ready to Checkout?\", \"fr\": \"Prêt à passer au paiement ?\"}'::jsonb),\r\n ('ecommerce.sale_badge', '{\"en\": \"Sale {percent}% Off\", \"fr\": \"Solde {percent}% de rabais\"}'::jsonb),\r\n ('ecommerce.low_stock', '{\"en\": \"Only {count} left\", \"fr\": \"Plus que {count} en stock\"}'::jsonb),\r\n ('ecommerce.instant_digital_delivery', '{\"en\": \"Instant Digital Delivery\", \"fr\": \"Livraison numérique instantanée\"}'::jsonb),\r\n ('ecommerce.free_shipping', '{\"en\": \"Free Shipping\", \"fr\": \"Livraison gratuite\"}'::jsonb),\r\n ('ecommerce.secure_checkout', '{\"en\": \"Secure Checkout\", \"fr\": \"Paiement sécurisé\"}'::jsonb),\r\n ('ecommerce.no_description', '{\"en\": \"No description available.\", \"fr\": \"Aucune description disponible.\"}'::jsonb),\r\n ('ecommerce.checkout_overlay_title', '{\"en\": \"Order Checkout\", \"fr\": \"Paiement de la commande\"}'::jsonb),\r\n ('ecommerce.email_placeholder', '{\"en\": \"you@example.com\", \"fr\": \"vous@exemple.com\"}'::jsonb),\r\n ('ecommerce.contact_information', '{\"en\": \"Contact Information\", \"fr\": \"Informations de contact\"}'::jsonb),\r\n ('ecommerce.shipping_address', '{\"en\": \"Shipping Address\", \"fr\": \"Adresse de livraison\"}'::jsonb),\r\n ('ecommerce.shipping_method', '{\"en\": \"Shipping Method\", \"fr\": \"Mode de livraison\"}'::jsonb),\r\n ('ecommerce.available_rates', '{\"en\": \"Available Rates\", \"fr\": \"Tarifs disponibles\"}'::jsonb),\r\n ('ecommerce.calculating', '{\"en\": \"Calculating...\", \"fr\": \"Calcul en cours...\"}'::jsonb),\r\n ('ecommerce.select_rate', '{\"en\": \"Select a shipping rate\", \"fr\": \"Sélectionnez un tarif de livraison\"}'::jsonb),\r\n ('ecommerce.enter_postal_code', '{\"en\": \"Enter postal code\", \"fr\": \"Entrez le code postal\"}'::jsonb),\r\n ('ecommerce.free', '{\"en\": \"Free\", \"fr\": \"Gratuit\"}'::jsonb),\r\n ('ecommerce.first_last_name', '{\"en\": \"First & Last Name\", \"fr\": \"Nom et prénom\"}'::jsonb),\r\n ('ecommerce.address', '{\"en\": \"Address\", \"fr\": \"Adresse\"}'::jsonb),\r\n ('ecommerce.city', '{\"en\": \"City\", \"fr\": \"Ville\"}'::jsonb),\r\n ('ecommerce.state_province', '{\"en\": \"State / Province\", \"fr\": \"État / Province\"}'::jsonb),\r\n ('ecommerce.zip_postal', '{\"en\": \"ZIP / Postal Code\", \"fr\": \"Code postal\"}'::jsonb),\r\n ('ecommerce.postal_code', '{\"en\": \"Postal Code\", \"fr\": \"Code postal\"}'::jsonb),\r\n ('ecommerce.zip_postal_code', '{\"en\": \"ZIP / Postal Code\", \"fr\": \"Code postal\"}'::jsonb),\r\n ('ecommerce.estimate_shipping', '{\"en\": \"Estimate Shipping\", \"fr\": \"Estimer la livraison\"}'::jsonb),\r\n ('ecommerce.calculate', '{\"en\": \"Calculate\", \"fr\": \"Calculer\"}'::jsonb),\r\n ('ecommerce.no_rates_found', '{\"en\": \"No shipping rates found for this region.\", \"fr\": \"Aucun tarif de livraison trouvé pour cette région.\"}'::jsonb),\r\n ('ecommerce.no_rates_for_region', '{\"en\": \"No shipping rates found for this region.\", \"fr\": \"Aucun tarif de livraison trouvé pour cette région.\"}'::jsonb),\r\n ('ecommerce.enter_address_for_rates', '{\"en\": \"Enter your address to see shipping rates.\", \"fr\": \"Entrez votre adresse pour voir les tarifs de livraison.\"}'::jsonb),\r\n ('ecommerce.secure_checkout_guarantee', '{\"en\": \"Secure checkout guaranteed\", \"fr\": \"Paiement sécurisé garanti\"}'::jsonb),\r\n ('ecommerce.pricing_unavailable', '{\"en\": \"Pricing unavailable\", \"fr\": \"Prix non disponible\"}'::jsonb),\r\n ('ecommerce.monthly', '{\"en\": \"Monthly\", \"fr\": \"Mensuel\"}'::jsonb),\r\n ('ecommerce.annual', '{\"en\": \"Annual\", \"fr\": \"Annuel\"}'::jsonb),\r\n ('ecommerce.lifetime', '{\"en\": \"Lifetime\", \"fr\": \"À vie\"}'::jsonb),\r\n ('ecommerce.year', '{\"en\": \"year\", \"fr\": \"an\"}'::jsonb),\r\n ('ecommerce.month', '{\"en\": \"month\", \"fr\": \"mois\"}'::jsonb),\r\n ('ecommerce.get_license', '{\"en\": \"Get License\", \"fr\": \"Obtenir la licence\"}'::jsonb),\r\n ('ecommerce.full_name', '{\"en\": \"Full Name\", \"fr\": \"Nom complet\"}'::jsonb),\r\n ('ecommerce.country', '{\"en\": \"Country\", \"fr\": \"Pays\"}'::jsonb)\r\nON CONFLICT (key) DO UPDATE\r\nSET translations = EXCLUDED.translations;\r\n\r\n-- 00000000000025_seed_freemius_translations.sql\r\n-- Freemius storefront copy introduced with the licensing expansion.\r\n\r\nINSERT INTO public.translations (key, translations) VALUES\r\n ('ecommerce.pricing_unavailable', '{\"en\": \"Pricing Unavailable\", \"es\": \"Precios no disponibles\"}'),\r\n ('ecommerce.monthly', '{\"en\": \"Monthly\", \"es\": \"Mensual\"}'),\r\n ('ecommerce.annual', '{\"en\": \"Annual\", \"es\": \"Anual\"}'),\r\n ('ecommerce.lifetime', '{\"en\": \"Lifetime\", \"es\": \"De por vida\"}'),\r\n ('ecommerce.year', '{\"en\": \"year\", \"es\": \"año\"}'),\r\n ('ecommerce.month', '{\"en\": \"month\", \"es\": \"mes\"}'),\r\n ('ecommerce.get_license', '{\"en\": \"Get License\", \"es\": \"Obtener Licencia\"}'),\r\n ('ecommerce.added_to_cart_success', '{\"en\": \"{item} added to your cart.\", \"es\": \"{item} añadido al carrito.\"}'),\r\n ('ecommerce.added_to_cart_error', '{\"en\": \"Could not add item to cart.\", \"es\": \"No se pudo añadir el artículo al carrito.\"}'),\r\n ('ecommerce.freemius_trial_preference_title', '{\"en\": \"How would you like to start your trial?\", \"es\": \"¿Cómo te gustaría comenzar tu prueba?\", \"fr\": \"Comment souhaitez-vous commencer votre essai ?\"}'),\r\n ('ecommerce.freemius_trial_no_card', '{\"en\": \"Start Free Trial (No card required)\", \"es\": \"Comenzar prueba gratuita (Sin tarjeta)\", \"fr\": \"Commencer l''essai gratuit (Sans carte requise)\"}'),\r\n ('ecommerce.freemius_trial_with_card', '{\"en\": \"Enter Payment Details Now (Still get full trial length free)\", \"es\": \"Ingresar detalles de pago ahora (Aún obtienes toda la duración de la prueba gratis)\", \"fr\": \"Entrer les détails de paiement maintenant (Vous bénéficiez toujours de toute la durée de l''essai gratuitement)\"}'),\r\n ('ecommerce.freemius_trial_with_card_help', '{\"en\": \"You will not be billed until the trial ends. Cancel anytime.\", \"es\": \"No se te cobrará hasta que termine la prueba. Cancela en cualquier momento.\", \"fr\": \"Vous ne serez pas facturé avant la fin de l''essai. Annulez à tout moment.\"}')\r\nON CONFLICT (key) DO UPDATE\r\nSET translations = EXCLUDED.translations;\r\n\r\n-- 00000000000026_seed_product_variation_translation_keys.sql\r\n-- Adds missing storefront translation keys for variable-product UX copy.\r\n\r\nINSERT INTO public.translations (key, translations)\r\nVALUES\r\n (\r\n 'ecommerce.choose_your_options',\r\n '{\"en\": \"Choose Your Options\", \"fr\": \"Choisissez vos options\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.variant_availability_help',\r\n '{\"en\": \"Select a combination to resolve the exact variant price and availability.\", \"fr\": \"Selectionnez une combinaison pour afficher le prix exact et la disponibilite de la variante.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.in_stock',\r\n '{\"en\": \"{count} in stock\", \"fr\": \"{count} en stock\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.out_of_stock',\r\n '{\"en\": \"Out of stock\", \"fr\": \"Rupture de stock\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.select_options',\r\n '{\"en\": \"Select Options\", \"fr\": \"Choisir des options\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.variant_selection_required',\r\n '{\"en\": \"Select one term from every dropdown to resolve a variation.\", \"fr\": \"Selectionnez une valeur dans chaque liste pour afficher la variante correspondante.\"}'::jsonb\r\n )\r\nON CONFLICT (key) DO UPDATE\r\nSET\r\n translations = EXCLUDED.translations,\r\n updated_at = now();\r\n\r\n-- 00000000000027_seed_shipping_rate_translations.sql\r\n-- Shipping and tax copy for checkout.\r\n\r\nINSERT INTO public.translations (key, translations)\r\nVALUES\r\n (\r\n 'ecommerce.tax_calculated_on_stripe',\r\n '{\"en\": \"Calculated on Stripe\", \"es\": \"Calculado en Stripe\", \"fr\": \"Calculé sur Stripe\"}'::jsonb\r\n ),\r\n (\r\n 'checkout_stripe_tax_finalized_notice',\r\n '{\"en\": \"Tax will be finalized by Stripe Tax on the payment step.\", \"es\": \"El impuesto se finalizará con Stripe Tax en el paso de pago.\", \"fr\": \"La taxe sera finalisée par Stripe Tax à l''étape du paiement.\"}'::jsonb\r\n )\r\nON CONFLICT (key) DO UPDATE\r\nSET translations = EXCLUDED.translations;\r\n\r\n-- 00000000000028_seed_invoice_branding_translations.sql\r\n-- Invoice, branding, and receipt translations.\r\n\r\nINSERT INTO public.translations (key, translations)\r\nVALUES\r\n (\r\n 'branding',\r\n '{\"en\": \"Branding\", \"fr\": \"Image de marque\"}'::jsonb\r\n ),\r\n (\r\n 'company_name',\r\n '{\"en\": \"Company name\", \"fr\": \"Nom de l''entreprise\"}'::jsonb\r\n ),\r\n (\r\n 'invoice',\r\n '{\"en\": \"Invoice\", \"fr\": \"Facture\"}'::jsonb\r\n ),\r\n (\r\n 'invoice_number',\r\n '{\"en\": \"Invoice #\", \"fr\": \"Facture no\"}'::jsonb\r\n ),\r\n (\r\n 'paid_on',\r\n '{\"en\": \"Paid on\", \"fr\": \"Paye le\"}'::jsonb\r\n ),\r\n (\r\n 'bill_to',\r\n '{\"en\": \"Bill to\", \"fr\": \"Facturer a\"}'::jsonb\r\n ),\r\n (\r\n 'ship_to',\r\n '{\"en\": \"Ship to\", \"fr\": \"Livrer a\"}'::jsonb\r\n ),\r\n (\r\n 'print_invoice',\r\n '{\"en\": \"Print / Save as PDF\", \"fr\": \"Imprimer / Enregistrer en PDF\"}'::jsonb\r\n ),\r\n (\r\n 'tax_registrations',\r\n '{\"en\": \"Tax registrations\", \"fr\": \"Inscriptions fiscales\"}'::jsonb\r\n ),\r\n (\r\n 'invoice_settings',\r\n '{\"en\": \"Invoice settings\", \"fr\": \"Parametres de facture\"}'::jsonb\r\n ),\r\n (\r\n 'business_name',\r\n '{\"en\": \"Business name\", \"fr\": \"Nom de l''entreprise\"}'::jsonb\r\n ),\r\n (\r\n 'order_number',\r\n '{\"en\": \"Order #\", \"fr\": \"Commande no\"}'::jsonb\r\n ),\r\n (\r\n 'print_invoice_help',\r\n '{\"en\": \"Use your browser print dialog to save this invoice as a PDF.\", \"fr\": \"Utilisez la boite de dialogue d''impression de votre navigateur pour enregistrer cette facture en PDF.\"}'::jsonb\r\n ),\r\n (\r\n 'return_home',\r\n '{\"en\": \"Return to Home\", \"fr\": \"Retour a l''accueil\"}'::jsonb\r\n ),\r\n (\r\n 'receipt_finalizing',\r\n '{\"en\": \"Finalizing your invoice and payment details...\", \"fr\": \"Finalisation de votre facture et des details du paiement...\"}'::jsonb\r\n ),\r\n (\r\n 'receipt_not_ready',\r\n '{\"en\": \"Your invoice will appear here once the payment sync is complete.\", \"fr\": \"Votre facture apparaitra ici une fois la synchronisation du paiement terminee.\"}'::jsonb\r\n ),\r\n (\r\n 'tax_breakdown',\r\n '{\"en\": \"Tax breakdown\", \"fr\": \"Detail des taxes\"}'::jsonb\r\n ),\r\n (\r\n 'amount',\r\n '{\"en\": \"Amount\", \"fr\": \"Montant\"}'::jsonb\r\n ),\r\n (\r\n 'price',\r\n '{\"en\": \"Price\", \"fr\": \"Prix\"}'::jsonb\r\n ),\r\n (\r\n 'from',\r\n '{\"en\": \"From\", \"fr\": \"De\"}'::jsonb\r\n )\r\nON CONFLICT (key) DO UPDATE\r\nSET\r\n translations = EXCLUDED.translations,\r\n updated_at = now();\r\n\r\n-- 00000000000029_seed_account_order_translations.sql\r\n-- Adds storefront account navigation, customer order, and password translations.\r\n\r\nBEGIN;\r\n\r\nINSERT INTO public.translations (key, translations)\r\nVALUES\r\n (\r\n 'account_navigation',\r\n '{\"en\": \"Account\", \"fr\": \"Compte\"}'::jsonb\r\n ),\r\n (\r\n 'account_orders',\r\n '{\"en\": \"Orders\", \"fr\": \"Commandes\"}'::jsonb\r\n ),\r\n (\r\n 'change_my_password',\r\n '{\"en\": \"Change my password\", \"fr\": \"Changer mon mot de passe\"}'::jsonb\r\n ),\r\n (\r\n 'profile_orders_title',\r\n '{\"en\": \"My orders\", \"fr\": \"Mes commandes\"}'::jsonb\r\n ),\r\n (\r\n 'profile_orders_description',\r\n '{\"en\": \"Review your recent purchases and open printable invoices.\", \"fr\": \"Consultez vos achats récents et ouvrez vos factures imprimables.\"}'::jsonb\r\n ),\r\n (\r\n 'profile_orders_empty',\r\n '{\"en\": \"You do not have any orders yet.\", \"fr\": \"Vous n''avez pas encore de commandes.\"}'::jsonb\r\n ),\r\n (\r\n 'profile_order_detail_title',\r\n '{\"en\": \"Order invoice\", \"fr\": \"Facture de commande\"}'::jsonb\r\n ),\r\n (\r\n 'profile_order_detail_description',\r\n '{\"en\": \"Review and print your finalized invoice.\", \"fr\": \"Consultez et imprimez votre facture finalisée.\"}'::jsonb\r\n ),\r\n (\r\n 'profile_order_invoice_pending',\r\n '{\"en\": \"The printable invoice will appear here once this order has been finalized.\", \"fr\": \"La facture imprimable apparaîtra ici une fois que cette commande aura été finalisée.\"}'::jsonb\r\n ),\r\n (\r\n 'profile_password_title',\r\n '{\"en\": \"Change your password\", \"fr\": \"Changer votre mot de passe\"}'::jsonb\r\n ),\r\n (\r\n 'profile_password_description',\r\n '{\"en\": \"Update your account password without leaving your profile.\", \"fr\": \"Mettez à jour le mot de passe de votre compte sans quitter votre profil.\"}'::jsonb\r\n ),\r\n (\r\n 'new_password',\r\n '{\"en\": \"New password\", \"fr\": \"Nouveau mot de passe\"}'::jsonb\r\n ),\r\n (\r\n 'confirm_new_password',\r\n '{\"en\": \"Confirm new password\", \"fr\": \"Confirmer le nouveau mot de passe\"}'::jsonb\r\n ),\r\n (\r\n 'password_updated_success',\r\n '{\"en\": \"Password updated successfully.\", \"fr\": \"Mot de passe mis à jour avec succès.\"}'::jsonb\r\n ),\r\n (\r\n 'password_update_failed',\r\n '{\"en\": \"Password update failed.\", \"fr\": \"La mise à jour du mot de passe a échoué.\"}'::jsonb\r\n ),\r\n (\r\n 'passwords_do_not_match',\r\n '{\"en\": \"Passwords do not match.\", \"fr\": \"Les mots de passe ne correspondent pas.\"}'::jsonb\r\n ),\r\n (\r\n 'order_date',\r\n '{\"en\": \"Date\", \"fr\": \"Date\"}'::jsonb\r\n ),\r\n (\r\n 'order_status_paid',\r\n '{\"en\": \"Paid\", \"fr\": \"Payée\"}'::jsonb\r\n ),\r\n (\r\n 'order_status_pending',\r\n '{\"en\": \"Pending\", \"fr\": \"En attente\"}'::jsonb\r\n ),\r\n (\r\n 'order_status_trial',\r\n '{\"en\": \"Trial\", \"fr\": \"Essai\"}'::jsonb\r\n ),\r\n (\r\n 'order_status_shipped',\r\n '{\"en\": \"Shipped\", \"fr\": \"Expédiée\"}'::jsonb\r\n ),\r\n (\r\n 'order_status_cancelled',\r\n '{\"en\": \"Cancelled\", \"fr\": \"Annulée\"}'::jsonb\r\n ),\r\n (\r\n 'order_status_refunded',\r\n '{\"en\": \"Refunded\", \"fr\": \"Remboursée\"}'::jsonb\r\n ),\r\n (\r\n 'back_to_orders',\r\n '{\"en\": \"Back to orders\", \"fr\": \"Retour aux commandes\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_trial_started',\r\n '{\"en\": \"Trial started\", \"fr\": \"Essai demarre\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_order_pending',\r\n '{\"en\": \"Order pending\", \"fr\": \"Commande en attente\"}'::jsonb\r\n )\r\nON CONFLICT (key) DO UPDATE\r\nSET translations = EXCLUDED.translations;\r\n\r\nCOMMIT;\r\n\r\n-- 00000000000030_seed_checkout_state_translations.sql\r\n-- Adds checkout state and CTA translations for shipping-gated checkout UX.\r\n\r\nBEGIN;\r\n\r\nINSERT INTO public.translations (key, translations)\r\nVALUES\r\n (\r\n 'select_an_option',\r\n '{\"en\": \"Select an option\", \"es\": \"Selecciona una opcion\", \"fr\": \"Selectionnez une option\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.shipping_method_required',\r\n '{\"en\": \"Please select a shipping method before continuing.\", \"es\": \"Selecciona un metodo de envio antes de continuar.\", \"fr\": \"Veuillez selectionner un mode de livraison avant de continuer.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.waiting_on_address_info',\r\n '{\"en\": \"Complete your shipping address to view available shipping options.\", \"es\": \"Completa tu direccion de envio para ver las opciones de envio disponibles.\", \"fr\": \"Completez votre adresse de livraison pour voir les options de livraison disponibles.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.calculating_shipping',\r\n '{\"en\": \"Calculating shipping...\", \"es\": \"Calculando el envio...\", \"fr\": \"Calcul de la livraison...\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.sandbox_checkout_stripe_description',\r\n '{\"en\": \"This simulated step represents the Stripe checkout for physical products.\", \"es\": \"Este paso simulado representa el pago de Stripe para productos fisicos.\", \"fr\": \"Cette etape simulee represente le paiement Stripe pour les produits physiques.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.sandbox_checkout_freemius_description',\r\n '{\"en\": \"This simulated step represents the Freemius checkout for digital products.\", \"es\": \"Este paso simulado representa el pago de Freemius para productos digitales.\", \"fr\": \"Cette etape simulee represente le paiement Freemius pour les produits numeriques.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.digital_label',\r\n '{\"en\": \"Digital\", \"es\": \"Digital\", \"fr\": \"Numerique\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.physical_label',\r\n '{\"en\": \"Physical\", \"es\": \"Fisico\", \"fr\": \"Physique\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.physical_products',\r\n '{\"en\": \"Physical products\", \"es\": \"Productos fisicos\", \"fr\": \"Produits physiques\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.digital_products',\r\n '{\"en\": \"Digital products\", \"es\": \"Productos digitales\", \"fr\": \"Produits numeriques\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.estimated_total',\r\n '{\"en\": \"Estimated total\", \"es\": \"Total estimado\", \"fr\": \"Total estime\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.stripe_checkout_title',\r\n '{\"en\": \"Stripe Checkout\", \"es\": \"Pago con Stripe\", \"fr\": \"Paiement Stripe\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.stripe_checkout_description',\r\n '{\"en\": \"Pay for physical products in one Stripe checkout session.\", \"es\": \"Paga los productos fisicos en una sola sesion de Stripe.\", \"fr\": \"Payez les produits physiques dans une seule session Stripe.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.item_count_one',\r\n '{\"en\": \"{count} item\", \"es\": \"{count} articulo\", \"fr\": \"{count} article\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.item_count_other',\r\n '{\"en\": \"{count} items\", \"es\": \"{count} articulos\", \"fr\": \"{count} articles\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.physical_subtotal',\r\n '{\"en\": \"Physical subtotal\", \"es\": \"Subtotal fisico\", \"fr\": \"Sous-total physique\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.total_on_stripe',\r\n '{\"en\": \"Total on Stripe\", \"es\": \"Total en Stripe\", \"fr\": \"Total sur Stripe\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_physical_products',\r\n '{\"en\": \"Checkout Physical Products\", \"es\": \"Pagar productos fisicos\", \"fr\": \"Payer les produits physiques\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.shipping_taxes_collected_on_stripe',\r\n '{\"en\": \"Shipping and taxes are only collected during the Stripe step for physical products.\", \"es\": \"El envio y los impuestos solo se cobran durante el paso de Stripe para productos fisicos.\", \"fr\": \"La livraison et les taxes sont percues uniquement a l''etape Stripe pour les produits physiques.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.freemius_checkout_title',\r\n '{\"en\": \"Freemius Checkout\", \"es\": \"Pago con Freemius\", \"fr\": \"Paiement Freemius\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.freemius_checkout_description',\r\n '{\"en\": \"Digital products use the Freemius checkout flow.\", \"es\": \"Los productos digitales usan el flujo de pago de Freemius.\", \"fr\": \"Les produits numeriques utilisent le flux de paiement Freemius.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.license_count_one',\r\n '{\"en\": \"{count} license\", \"es\": \"{count} licencia\", \"fr\": \"{count} licence\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.license_count_other',\r\n '{\"en\": \"{count} licenses\", \"es\": \"{count} licencias\", \"fr\": \"{count} licences\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_billing_cycle_monthly',\r\n '{\"en\": \"Monthly subscription\", \"es\": \"Suscripcion mensual\", \"fr\": \"Abonnement mensuel\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_billing_cycle_annual',\r\n '{\"en\": \"Annual subscription\", \"es\": \"Suscripcion anual\", \"fr\": \"Abonnement annuel\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_billing_cycle_lifetime',\r\n '{\"en\": \"Lifetime subscription\", \"es\": \"Suscripcion de por vida\", \"fr\": \"Abonnement a vie\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_product',\r\n '{\"en\": \"Checkout {title}\", \"es\": \"Pagar {title}\", \"fr\": \"Paiement de {title}\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_digital_product',\r\n '{\"en\": \"Checkout Digital Product\", \"es\": \"Pagar producto digital\", \"fr\": \"Payer le produit numerique\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.digital_subtotal',\r\n '{\"en\": \"Digital subtotal\", \"es\": \"Subtotal digital\", \"fr\": \"Sous-total numerique\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.freemius_multi_checkout_notice',\r\n '{\"en\": \"Freemius licenses are completed one at a time, so each digital product gets its own checkout action.\", \"es\": \"Las licencias de Freemius se completan una por una, por lo que cada producto digital tiene su propia accion de pago.\", \"fr\": \"Les licences Freemius se finalisent une a la fois, donc chaque produit numerique a sa propre action de paiement.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.freemius_tax_notice',\r\n '{\"en\": \"Taxes and compliance for digital products are handled inside the Freemius checkout.\", \"es\": \"Los impuestos y la conformidad para los productos digitales se gestionan dentro del pago de Freemius.\", \"fr\": \"Les taxes et la conformite pour les produits numeriques sont gerees dans le paiement Freemius.\"}'::jsonb\r\n ),\r\n (\r\n 'continue_checkout',\r\n '{\"en\": \"Continue Checkout\", \"fr\": \"Continuer le paiement\"}'::jsonb\r\n ),\r\n (\r\n 'checkout_success_sync_failed',\r\n '{\"en\": \"We could not finalize your invoice yet. Please refresh shortly.\", \"fr\": \"Nous n''avons pas encore pu finaliser votre facture. Veuillez rafraichir la page sous peu.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.shipping_country_required',\r\n '{\"en\": \"Country is required to calculate shipping.\", \"fr\": \"Le pays est requis pour calculer la livraison.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.shipping_calculation_failed',\r\n '{\"en\": \"We couldn''t calculate shipping right now. Please try again.\", \"fr\": \"Nous n''avons pas pu calculer la livraison pour le moment. Veuillez reessayer.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_license_inactive',\r\n '{\"en\": \"The ecommerce module license is inactive.\", \"fr\": \"La licence du module ecommerce est inactive.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_invalid_items',\r\n '{\"en\": \"Your checkout items could not be processed.\", \"fr\": \"Les articles de votre commande n''ont pas pu etre traites.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_provider_items_required',\r\n '{\"en\": \"Each checkout step must include items assigned to a payment provider.\", \"fr\": \"Chaque etape de paiement doit inclure des articles associes a un fournisseur de paiement.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_mixed_provider_steps',\r\n '{\"en\": \"Products with different payment providers must be purchased in separate checkout steps.\", \"fr\": \"Les produits utilisant differents fournisseurs de paiement doivent etre achetes en etapes separees.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_freemius_single_item',\r\n '{\"en\": \"Freemius products must be purchased one at a time.\", \"fr\": \"Les produits Freemius doivent etre achetes un a la fois.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_billing_address_required',\r\n '{\"en\": \"A billing address is required to continue checkout.\", \"fr\": \"Une adresse de facturation est requise pour continuer le paiement.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_internal_server_error',\r\n '{\"en\": \"Something went wrong while preparing your checkout. Please try again.\", \"fr\": \"Une erreur s''est produite lors de la preparation de votre paiement. Veuillez reessayer.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_missing_session_id',\r\n '{\"en\": \"We couldn''t find a checkout session to finalize.\", \"fr\": \"Nous n''avons pas trouve de session de paiement a finaliser.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_payment_pending',\r\n '{\"en\": \"Your payment is still pending.\", \"fr\": \"Votre paiement est toujours en attente.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_success_order_not_found',\r\n '{\"en\": \"We couldn''t find the order linked to this checkout.\", \"fr\": \"Nous n''avons pas trouve la commande liee a ce paiement.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_success_invalid_reference',\r\n '{\"en\": \"This checkout reference can''t be finalized from this page.\", \"fr\": \"Cette reference de paiement ne peut pas etre finalisee depuis cette page.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_success_inventory_update_failed',\r\n '{\"en\": \"We couldn''t update inventory for this order.\", \"fr\": \"Nous n''avons pas pu mettre a jour l''inventaire pour cette commande.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.checkout_success_status_update_failed',\r\n '{\"en\": \"We couldn''t update the order status.\", \"fr\": \"Nous n''avons pas pu mettre a jour le statut de la commande.\"}'::jsonb\r\n ),\r\n (\r\n 'ecommerce.unknown_error',\r\n '{\"en\": \"Unknown error\", \"es\": \"Error desconocido\", \"fr\": \"Erreur inconnue\"}'::jsonb\r\n )\r\nON CONFLICT (key) DO UPDATE\r\nSET\r\n translations = EXCLUDED.translations,\r\n updated_at = now();\r\n\r\nCOMMIT;\r\n\r\n-- 00000000000031_seed_french_freemius_translation_fixes.sql\r\n-- Restores French translations that were overwritten by the Freemius ecommerce expansion seed.\r\nINSERT INTO public.translations (key, translations)\r\nVALUES\r\n (\r\n 'ecommerce.pricing_unavailable',\r\n jsonb_build_object(\r\n 'en', 'Pricing Unavailable',\r\n 'es', 'Precios no disponibles',\r\n 'fr', 'Tarification indisponible'\r\n )\r\n ),\r\n (\r\n 'ecommerce.monthly',\r\n jsonb_build_object(\r\n 'en', 'Monthly',\r\n 'es', 'Mensual',\r\n 'fr', 'Mensuel'\r\n )\r\n ),\r\n (\r\n 'ecommerce.annual',\r\n jsonb_build_object(\r\n 'en', 'Annual',\r\n 'es', 'Anual',\r\n 'fr', 'Annuel'\r\n )\r\n ),\r\n (\r\n 'ecommerce.lifetime',\r\n jsonb_build_object(\r\n 'en', 'Lifetime',\r\n 'es', 'De por vida',\r\n 'fr', 'À vie'\r\n )\r\n ),\r\n (\r\n 'ecommerce.year',\r\n jsonb_build_object(\r\n 'en', 'year',\r\n 'es', 'año',\r\n 'fr', 'an'\r\n )\r\n ),\r\n (\r\n 'ecommerce.month',\r\n jsonb_build_object(\r\n 'en', 'month',\r\n 'es', 'mes',\r\n 'fr', 'mois'\r\n )\r\n ),\r\n (\r\n 'ecommerce.get_license',\r\n jsonb_build_object(\r\n 'en', 'Get License',\r\n 'es', 'Obtener Licencia',\r\n 'fr', 'Obtenir la licence'\r\n )\r\n ),\r\n (\r\n 'ecommerce.added_to_cart_success',\r\n jsonb_build_object(\r\n 'en', '{item} added to your cart.',\r\n 'es', '{item} añadido al carrito.',\r\n 'fr', '{item} ajouté à votre panier.'\r\n )\r\n ),\r\n (\r\n 'ecommerce.added_to_cart_error',\r\n jsonb_build_object(\r\n 'en', 'Could not add item to cart.',\r\n 'es', 'No se pudo añadir el artículo al carrito.',\r\n 'fr', 'Impossible d''ajouter l''article au panier.'\r\n )\r\n )\r\nON CONFLICT (key) DO UPDATE\r\nSET translations =\r\n COALESCE(public.translations.translations, '{}'::jsonb)\r\n || jsonb_build_object('fr', EXCLUDED.translations->>'fr');\r\n\r\n-- 00000000000032_seed_global_search_translations.sql\r\n-- Global storefront search copy.\r\nINSERT INTO public.translations (key, translations)\r\nVALUES\r\n (\r\n 'edit_product',\r\n '{\"en\": \"Edit Product\", \"fr\": \"Modifier le produit\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.trigger',\r\n '{\"en\": \"Search\", \"fr\": \"Rechercher\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.title',\r\n '{\"en\": \"Search\", \"fr\": \"Rechercher\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.description',\r\n '{\"en\": \"Search published pages, posts, and products.\", \"fr\": \"Rechercher dans les pages, articles et produits publies.\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.placeholder',\r\n '{\"en\": \"Search...\", \"fr\": \"Rechercher...\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.filter_all',\r\n '{\"en\": \"All\", \"fr\": \"Tout\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.filter_pages',\r\n '{\"en\": \"Pages\", \"fr\": \"Pages\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.filter_posts',\r\n '{\"en\": \"Posts\", \"fr\": \"Articles\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.filter_products',\r\n '{\"en\": \"Products\", \"fr\": \"Produits\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.result_page',\r\n '{\"en\": \"Page\", \"fr\": \"Page\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.result_post',\r\n '{\"en\": \"Post\", \"fr\": \"Article\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.result_product',\r\n '{\"en\": \"Product\", \"fr\": \"Produit\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.recent',\r\n '{\"en\": \"Recent\", \"fr\": \"Recents\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.error_title',\r\n '{\"en\": \"Search is unavailable.\", \"fr\": \"La recherche est indisponible.\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.error_description',\r\n '{\"en\": \"Please try again in a moment.\", \"fr\": \"Veuillez reessayer dans un instant.\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.empty_title',\r\n '{\"en\": \"No results found.\", \"fr\": \"Aucun resultat trouve.\"}'::jsonb\r\n ),\r\n (\r\n 'global_search.empty_description',\r\n '{\"en\": \"Try another search term.\", \"fr\": \"Essayez un autre terme de recherche.\"}'::jsonb\r\n )\r\nON CONFLICT (key) DO UPDATE\r\nSET\r\n translations = EXCLUDED.translations,\r\n updated_at = now();\r\n"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"version": "00000000000010",
|
|
69
|
+
"name": "00000000000010_seed_content_scaffold.sql",
|
|
70
|
+
"sql": "-- 00000000000010_seed_content_scaffold.sql\r\n-- Consolidated migration preserving original statement order within grouped sections.\r\n\r\n-- 00000000000033_seed_logo_and_content_scaffold.sql\r\n-- Foundational translations, logo assets, and starter content scaffolding.\r\n\r\nBEGIN;\r\n\r\n-- 1. Translations\r\n-- Merged from multiple translation seed files\r\nINSERT INTO public.translations (key, translations) VALUES\r\n('sign_in', '{\"en\": \"Sign in\", \"fr\": \"Connexion\"}'),\r\n('sign_up', '{\"en\": \"Sign up\", \"fr\": \"Inscription\"}'),\r\n('sign_out', '{\"en\": \"Sign out\", \"fr\": \"Déconnexion\"}'),\r\n('dont_have_account', '{\"en\": \"Don''t have an account?\", \"fr\": \"Pas encore de compte ?\"}'),\r\n('email', '{\"en\": \"Email\", \"fr\": \"Email\"}'),\r\n('you_at_example_com', '{\"en\": \"you@example.com\", \"fr\": \"vous@example.com\"}'),\r\n('password', '{\"en\": \"Password\", \"fr\": \"Mot de passe\"}'),\r\n('forgot_password', '{\"en\": \"Forgot Password?\", \"fr\": \"Mot de passe oublié ?\"}'),\r\n('your_password', '{\"en\": \"Your password\", \"fr\": \"Votre mot de passe\"}'),\r\n('signing_in_pending', '{\"en\": \"Signing In...\", \"fr\": \"Connexion en cours...\"}'),\r\n('already_have_account', '{\"en\": \"Already have an account?\", \"fr\": \"Déjà un compte ?\"}'),\r\n('signing_up_pending', '{\"en\": \"Signing up...\", \"fr\": \"Inscription en cours...\"}'),\r\n('reset_password', '{\"en\": \"Reset Password\", \"fr\": \"Réinitialiser le mot de passe\"}'),\r\n('auth.signup_form_description', '{\"en\": \"Create your account in one quick step. We''ll email you a confirmation link before you finish your profile.\", \"fr\": \"Créez votre compte en une étape rapide. Nous vous enverrons un lien de confirmation avant de terminer votre profil.\"}'),\r\n('auth.signup_success_badge', '{\"en\": \"Signup received\", \"fr\": \"Inscription reçue\"}'),\r\n('auth.signup_success_title', '{\"en\": \"Check your inbox\", \"fr\": \"Vérifiez votre boîte de réception\"}'),\r\n('auth.signup_success_step_confirm', '{\"en\": \"Open the email we sent and confirm your address.\", \"fr\": \"Ouvrez l''e-mail envoyé et confirmez votre adresse.\"}'),\r\n('auth.signup_success_step_profile', '{\"en\": \"After confirmation, we''ll bring you to your profile to finish setup.\", \"fr\": \"Après confirmation, nous vous amènerons à votre profil pour terminer la configuration.\"}'),\r\n('auth.signup_success_step_spam', '{\"en\": \"If it doesn''t arrive soon, check spam, junk, or promotions.\", \"fr\": \"S''il n''arrive pas bientôt, vérifiez les dossiers spam, indésirables ou promotions.\"}'),\r\n('auth.signup_use_different_email', '{\"en\": \"Use a different email\", \"fr\": \"Utiliser une autre adresse e-mail\"}'),\r\n('auth.back_to_sign_in', '{\"en\": \"Back to sign in\", \"fr\": \"Retour à la connexion\"}'),\r\n('auth.signup_rate_limit', '{\"en\": \"You''re trying too quickly. Please wait a moment before requesting another confirmation email.\", \"fr\": \"Vous allez trop vite. Veuillez attendre un instant avant de demander un nouvel e-mail de confirmation.\"}'),\r\n('blog_prefix', '{\"en\": \"article\", \"fr\": \"article\"}'),\r\n('edit_page', '{\"en\": \"Edit Page\", \"fr\": \"Éditer la page\"}'),\r\n('edit_post', '{\"en\": \"Edit Post\", \"fr\": \"Éditer l''article\"}'),\r\n('open_main_menu', '{\"en\": \"Open main menu\", \"fr\": \"Ouvrir le menu principal\"}'),\r\n('mobile_navigation_menu', '{\"en\": \"Mobile navigation menu\", \"fr\": \"Menu de navigation mobile\"}'),\r\n('cms_dashboard', '{\"en\": \"CMS Dashboard\", \"fr\": \"Tableau de bord CMS\"}'),\r\n('update_env_file_warning', '{\"en\": \"Please update .env.local file with anon key and url\", \"fr\": \"Veuillez mettre à jour .env.local avec l''anon key et l''URL\"}'),\r\n('greeting', '{\"en\": \"Hey, {username}!\", \"fr\": \"Salut, {username} !\"}'),\r\n('theme_switcher', '{\"en\": \"Theme Switcher\", \"fr\": \"Sélecteur de thème\"}'),\r\n('theme_light', '{\"en\": \"Light\", \"fr\": \"Clair\"}'),\r\n('theme_dark', '{\"en\": \"Dark\", \"fr\": \"Sombre\"}'),\r\n('theme_system', '{\"en\": \"System\", \"fr\": \"Système\"}'),\r\n('theme_vibrant', '{\"en\": \"Vibrant\", \"fr\": \"Vibrant\"}'),\r\n('sandbox_mode_banner', '{\"en\": \"Sandbox Mode: Data is public and resets every 15 minutes.\", \"fr\": \"Mode Sandbox : Les données sont publiques et réinitialisées toutes les 15 minutes.\"}'),\r\n('demo_access_title', '{\"en\": \"Demo Access\", \"fr\": \"Accès Démo\"}'),\r\n('demo_access_desc', '{\"en\": \"This is a demo site. You may use the following credentials to access the admin section:\", \"fr\": \"Ceci est un site de démonstration. Vous pouvez utiliser les identifiants suivants pour accéder à l''administration :\"}'),\r\n('demo_user_label', '{\"en\": \"User:\", \"fr\": \"Utilisateur :\"}'),\r\n('demo_password_label', '{\"en\": \"Password:\", \"fr\": \"Mot de passe :\"}')\r\nON CONFLICT (key) DO UPDATE\r\nSET translations = EXCLUDED.translations;\r\n\r\n\r\n-- 2. Site Logo\r\nDO $$\r\nDECLARE\r\n v_logo_media_id UUID := gen_random_uuid();\r\n v_admin_id UUID;\r\nBEGIN\r\n -- Get an admin user ID to set as uploader (optional, fallback to NULL)\r\n SELECT id INTO v_admin_id FROM public.profiles WHERE role = 'ADMIN' LIMIT 1;\r\n\r\n -- Insert the logo into the media table\r\n INSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description)\r\n VALUES (\r\n v_logo_media_id,\r\n v_admin_id,\r\n 'nextblock-logo-small.webp',\r\n 'images/nextblock-logo-small.webp',\r\n 'image/webp',\r\n 10000,\r\n 'NextBlock™ Site Logo'\r\n )\r\n ON CONFLICT (object_key) DO UPDATE\r\n SET\r\n file_name = excluded.file_name,\r\n file_type = excluded.file_type,\r\n description = excluded.description\r\n RETURNING id INTO v_logo_media_id;\r\n\r\n -- Insert the logo into the logos table\r\n INSERT INTO public.logos (name, media_id)\r\n VALUES ('NextBlock™ Logo', v_logo_media_id)\r\n ON CONFLICT DO NOTHING; -- Assuming name is not unique but we don't want to double insert if running multiple times? No unique constraint on name.\r\n -- Actually, logos table has no unique constraint on name. \r\n -- But since this is a seed, we might want to avoid duplicates if run multiple times.\r\n -- Let's check if it exists.\r\n IF NOT EXISTS (SELECT 1 FROM public.logos WHERE name = 'NextBlock™ Logo') THEN\r\n INSERT INTO public.logos (name, media_id) VALUES ('NextBlock™ Logo', v_logo_media_id);\r\n END IF;\r\n\r\nEND $$;\r\n\r\n\r\n\r\n-- 3. Foundational Content (Pages & Posts Structure)\r\nDO $$\r\nDECLARE\r\n v_home_page_group_id uuid := gen_random_uuid();\r\n v_blog_page_group_id uuid := gen_random_uuid();\r\n v_how_it_works_post_group_id uuid := gen_random_uuid();\r\n v_setup_post_group_id uuid := gen_random_uuid();\r\n v_commerce_post_group_id uuid := gen_random_uuid();\r\n v_contact_page_group_id uuid := gen_random_uuid();\r\n v_en_lang_id bigint;\r\n v_fr_lang_id bigint;\r\n v_architecture_media_id uuid;\r\n v_extensibility_media_id uuid;\r\n v_included_media_id uuid;\r\n v_setup_media_id uuid;\r\n v_commerce_plan_media_id uuid;\r\n v_commerce_media_id uuid;\r\nBEGIN\r\n SELECT id INTO v_en_lang_id FROM public.languages WHERE code = 'en' LIMIT 1;\r\n SELECT id INTO v_fr_lang_id FROM public.languages WHERE code = 'fr' LIMIT 1;\r\n\r\n IF v_en_lang_id IS NULL OR v_fr_lang_id IS NULL THEN\r\n RAISE EXCEPTION 'Required languages (en, fr) not found.';\r\n END IF;\r\n\r\n INSERT INTO public.pages (language_id, title, slug, status, translation_group_id)\r\n VALUES (v_en_lang_id, 'Home', 'home', 'published', v_home_page_group_id)\r\n ON CONFLICT (language_id, slug) DO UPDATE SET title = EXCLUDED.title, status = EXCLUDED.status;\r\n\r\n INSERT INTO public.pages (language_id, title, slug, status, translation_group_id)\r\n VALUES (v_fr_lang_id, 'Accueil', 'accueil', 'published', v_home_page_group_id)\r\n ON CONFLICT (language_id, slug) DO UPDATE SET title = EXCLUDED.title, status = EXCLUDED.status;\r\n\r\n INSERT INTO public.pages (language_id, title, slug, status, translation_group_id)\r\n VALUES (v_en_lang_id, 'Articles', 'articles', 'published', v_blog_page_group_id)\r\n ON CONFLICT (language_id, slug) DO UPDATE SET title = EXCLUDED.title, status = EXCLUDED.status;\r\n\r\n INSERT INTO public.pages (language_id, title, slug, status, translation_group_id)\r\n VALUES (v_fr_lang_id, 'Articles', 'articles', 'published', v_blog_page_group_id)\r\n ON CONFLICT (language_id, slug) DO UPDATE SET title = EXCLUDED.title, status = EXCLUDED.status;\r\n\r\n INSERT INTO public.pages (language_id, title, slug, status, translation_group_id)\r\n VALUES (v_en_lang_id, 'Contact Us', 'contact', 'published', v_contact_page_group_id)\r\n ON CONFLICT (language_id, slug) DO UPDATE SET title = EXCLUDED.title, status = EXCLUDED.status;\r\n\r\n INSERT INTO public.pages (language_id, title, slug, status, translation_group_id)\r\n VALUES (v_fr_lang_id, 'Contactez-nous', 'contact', 'published', v_contact_page_group_id)\r\n ON CONFLICT (language_id, slug) DO UPDATE SET title = EXCLUDED.title, status = EXCLUDED.status;\r\n\r\n v_architecture_media_id := gen_random_uuid();\r\n INSERT INTO public.media (id, file_name, object_key, file_type, size_bytes, width, height, description)\r\n VALUES (\r\n v_architecture_media_id,\r\n 'NBcover.webp',\r\n 'images/NBcover.webp',\r\n 'image/webp',\r\n 180000,\r\n 1024,\r\n 572,\r\n 'NextBlock™ architecture overview cover image'\r\n )\r\n ON CONFLICT (object_key) DO UPDATE\r\n SET\r\n file_name = EXCLUDED.file_name,\r\n width = EXCLUDED.width,\r\n height = EXCLUDED.height,\r\n description = EXCLUDED.description\r\n RETURNING id INTO v_architecture_media_id;\r\n\r\n v_extensibility_media_id := gen_random_uuid();\r\n INSERT INTO public.media (id, file_name, object_key, file_type, size_bytes, width, height, description)\r\n VALUES (\r\n v_extensibility_media_id,\r\n 'extensibility.webp',\r\n 'images/extensibility.webp',\r\n 'image/webp',\r\n 246808,\r\n 1024,\r\n 559,\r\n 'NextBlock™ extensibility editorial artwork'\r\n )\r\n ON CONFLICT (object_key) DO UPDATE\r\n SET\r\n file_name = EXCLUDED.file_name,\r\n width = EXCLUDED.width,\r\n height = EXCLUDED.height,\r\n description = EXCLUDED.description\r\n RETURNING id INTO v_extensibility_media_id;\r\n\r\n v_included_media_id := gen_random_uuid();\r\n INSERT INTO public.media (id, file_name, object_key, file_type, size_bytes, width, height, description)\r\n VALUES (\r\n v_included_media_id,\r\n 'included.webp',\r\n 'images/included.webp',\r\n 'image/webp',\r\n 237478,\r\n 1024,\r\n 559,\r\n 'NextBlock™ getting-started platform artwork'\r\n )\r\n ON CONFLICT (object_key) DO UPDATE\r\n SET\r\n file_name = EXCLUDED.file_name,\r\n width = EXCLUDED.width,\r\n height = EXCLUDED.height,\r\n description = EXCLUDED.description\r\n RETURNING id INTO v_included_media_id;\r\n\r\n v_setup_media_id := gen_random_uuid();\r\n INSERT INTO public.media (id, file_name, object_key, file_type, size_bytes, width, height, description)\r\n VALUES (\r\n v_setup_media_id,\r\n 'programmer-upscaled.webp',\r\n 'images/programmer-upscaled.webp',\r\n 'image/webp',\r\n 780000,\r\n 8192,\r\n 2632,\r\n 'NextBlock™ setup guide cover image'\r\n )\r\n ON CONFLICT (object_key) DO UPDATE\r\n SET\r\n file_name = EXCLUDED.file_name,\r\n width = EXCLUDED.width,\r\n height = EXCLUDED.height,\r\n description = EXCLUDED.description\r\n RETURNING id INTO v_setup_media_id;\r\n\r\n v_commerce_plan_media_id := gen_random_uuid();\r\n INSERT INTO public.media (id, file_name, object_key, file_type, size_bytes, width, height, description)\r\n VALUES (\r\n v_commerce_plan_media_id,\r\n 'commerce-plan.webp',\r\n 'images/commerce-plan.webp',\r\n 'image/webp',\r\n 269854,\r\n 1024,\r\n 559,\r\n 'NextBlock™ commerce roadmap artwork'\r\n )\r\n ON CONFLICT (object_key) DO UPDATE\r\n SET\r\n file_name = EXCLUDED.file_name,\r\n width = EXCLUDED.width,\r\n height = EXCLUDED.height,\r\n description = EXCLUDED.description\r\n RETURNING id INTO v_commerce_plan_media_id;\r\n\r\n v_commerce_media_id := gen_random_uuid();\r\n INSERT INTO public.media (id, file_name, object_key, file_type, size_bytes, width, height, description)\r\n VALUES (\r\n v_commerce_media_id,\r\n 'commerce-wide.webp',\r\n 'images/commerce-wide.webp',\r\n 'image/webp',\r\n 250584,\r\n 1024,\r\n 434,\r\n 'NextBlock™ Commerce editorial feature image'\r\n )\r\n ON CONFLICT (object_key) DO UPDATE\r\n SET\r\n file_name = EXCLUDED.file_name,\r\n width = EXCLUDED.width,\r\n height = EXCLUDED.height,\r\n description = EXCLUDED.description\r\n RETURNING id INTO v_commerce_media_id;\r\n\r\n INSERT INTO public.posts (language_id, title, slug, label, status, excerpt, subtitle, translation_group_id, feature_image_id)\r\n VALUES (\r\n v_en_lang_id,\r\n 'How NextBlock™ Works: A Look Under the Hood',\r\n 'how-nextblock-works',\r\n 'Architecture',\r\n 'published',\r\n 'Under the hood of the monorepo, block registry, and editor stack that power NextBlock.',\r\n 'A guided tour of the monorepo, block registry, editor stack, and open-core architecture behind NextBlock.',\r\n v_how_it_works_post_group_id,\r\n v_architecture_media_id\r\n )\r\n ON CONFLICT (language_id, slug) DO UPDATE\r\n SET\r\n title = EXCLUDED.title,\r\n label = EXCLUDED.label,\r\n excerpt = EXCLUDED.excerpt,\r\n subtitle = EXCLUDED.subtitle,\r\n status = EXCLUDED.status,\r\n feature_image_id = EXCLUDED.feature_image_id;\r\n\r\n INSERT INTO public.posts (language_id, title, slug, label, status, excerpt, subtitle, translation_group_id, feature_image_id)\r\n VALUES (\r\n v_fr_lang_id,\r\n 'Comment NextBlock™ Fonctionne : Regard Sous le Capot',\r\n 'comment-nextblock-fonctionne',\r\n 'Architecture',\r\n 'published',\r\n 'Sous le capot du monorepo, du registre de blocs et de l editeur qui propulsent NextBlock.',\r\n 'Une visite guidee du monorepo, du registre de blocs, de l editeur et de l architecture open-core de NextBlock.',\r\n v_how_it_works_post_group_id,\r\n v_architecture_media_id\r\n )\r\n ON CONFLICT (language_id, slug) DO UPDATE\r\n SET\r\n title = EXCLUDED.title,\r\n label = EXCLUDED.label,\r\n excerpt = EXCLUDED.excerpt,\r\n subtitle = EXCLUDED.subtitle,\r\n status = EXCLUDED.status,\r\n feature_image_id = EXCLUDED.feature_image_id;\r\n\r\n INSERT INTO public.posts (language_id, title, slug, label, status, excerpt, subtitle, translation_group_id, feature_image_id)\r\n VALUES (\r\n v_en_lang_id,\r\n 'How to Setup NextBlock: From Scratch',\r\n 'how-to-setup-nextblock',\r\n 'Getting Started',\r\n 'published',\r\n 'Installation paths, launch checklists, and setup notes for teams adopting NextBlock.',\r\n 'Two clear ways to launch NextBlock: the full monorepo for contributors, or the CLI for a fast standalone setup.',\r\n v_setup_post_group_id,\r\n v_setup_media_id\r\n )\r\n ON CONFLICT (language_id, slug) DO UPDATE\r\n SET\r\n title = EXCLUDED.title,\r\n label = EXCLUDED.label,\r\n excerpt = EXCLUDED.excerpt,\r\n subtitle = EXCLUDED.subtitle,\r\n status = EXCLUDED.status,\r\n feature_image_id = EXCLUDED.feature_image_id;\r\n\r\n INSERT INTO public.posts (language_id, title, slug, label, status, excerpt, subtitle, translation_group_id, feature_image_id)\r\n VALUES (\r\n v_fr_lang_id,\r\n 'Comment Configurer NextBlock™ : Guide Complet',\r\n 'comment-configurer-nextblock',\r\n 'Mise En Route',\r\n 'published',\r\n 'Parcours d installation, checklist de lancement et notes de configuration pour adopter NextBlock.',\r\n 'Deux chemins simples pour lancer NextBlock™ : le monorepo complet pour les contributeurs, ou le CLI pour un demarrage rapide.',\r\n v_setup_post_group_id,\r\n v_setup_media_id\r\n )\r\n ON CONFLICT (language_id, slug) DO UPDATE\r\n SET\r\n title = EXCLUDED.title,\r\n label = EXCLUDED.label,\r\n excerpt = EXCLUDED.excerpt,\r\n subtitle = EXCLUDED.subtitle,\r\n status = EXCLUDED.status,\r\n feature_image_id = EXCLUDED.feature_image_id;\r\n\r\n INSERT INTO public.posts (language_id, title, slug, label, status, excerpt, subtitle, translation_group_id, feature_image_id)\r\n VALUES (\r\n v_en_lang_id,\r\n 'NextBlock™ Commerce: Multi-Currency, Tax Sync & Beyond',\r\n 'nextblock-commerce-guide',\r\n 'Commerce',\r\n 'published',\r\n 'Storefront architecture, checkout flows, and premium commerce capabilities inside NextBlock.',\r\n 'A closer look at the commerce module, from multi-currency and tax sync to shipping, inventory, and provider-aware checkout.',\r\n v_commerce_post_group_id,\r\n v_commerce_media_id\r\n )\r\n ON CONFLICT (language_id, slug) DO UPDATE\r\n SET\r\n title = EXCLUDED.title,\r\n label = EXCLUDED.label,\r\n excerpt = EXCLUDED.excerpt,\r\n subtitle = EXCLUDED.subtitle,\r\n status = EXCLUDED.status,\r\n feature_image_id = EXCLUDED.feature_image_id;\r\n\r\n INSERT INTO public.posts (language_id, title, slug, status, excerpt, translation_group_id, feature_image_id)\r\n VALUES (\r\n v_fr_lang_id,\r\n 'NextBlock™ Commerce : Multi-Devises, Taxes Automatiques et Plus',\r\n 'guide-commerce-nextblock',\r\n 'published',\r\n 'Un apercu du module commerce : multi-devises, sync taxes, expédition, inventaire et paiements connectes.',\r\n v_commerce_post_group_id,\r\n v_commerce_media_id\r\n )\r\n ON CONFLICT (language_id, slug) DO UPDATE\r\n SET\r\n title = EXCLUDED.title,\r\n excerpt = EXCLUDED.excerpt,\r\n status = EXCLUDED.status,\r\n feature_image_id = EXCLUDED.feature_image_id;\r\n\r\n UPDATE public.posts\r\n SET\r\n label = 'Architecture',\r\n excerpt = 'Under the hood of the monorepo, block registry, and editor stack that power NextBlock.',\r\n subtitle = 'A guided tour of the monorepo, block registry, editor stack, and open-core architecture behind NextBlock.'\r\n WHERE slug = 'how-nextblock-works';\r\n\r\n UPDATE public.posts\r\n SET\r\n label = 'Architecture',\r\n excerpt = 'Sous le capot du monorepo, du registre de blocs et de l editeur qui propulsent NextBlock.',\r\n subtitle = 'Une visite guidee du monorepo, du registre de blocs, de l editeur et de l architecture open-core de NextBlock.'\r\n WHERE slug = 'comment-nextblock-fonctionne';\r\n\r\n UPDATE public.posts\r\n SET\r\n label = 'Getting Started',\r\n excerpt = 'Installation paths, launch checklists, and setup notes for teams adopting NextBlock.',\r\n subtitle = 'Two clear ways to launch NextBlock: the full monorepo for contributors, or the CLI for a fast standalone setup.'\r\n WHERE slug = 'how-to-setup-nextblock';\r\n\r\n UPDATE public.posts\r\n SET\r\n label = 'Mise En Route',\r\n excerpt = 'Parcours d installation, checklist de lancement et notes de configuration pour adopter NextBlock.',\r\n subtitle = 'Deux chemins simples pour lancer NextBlock™ : le monorepo complet pour les contributeurs, ou le CLI pour un demarrage rapide.'\r\n WHERE slug = 'comment-configurer-nextblock';\r\n\r\n UPDATE public.posts\r\n SET\r\n label = 'Commerce',\r\n excerpt = 'Storefront architecture, checkout flows, and premium commerce capabilities inside NextBlock.',\r\n subtitle = 'A closer look at the commerce module, from multi-currency and tax sync to shipping, inventory, and provider-aware checkout.'\r\n WHERE slug = 'nextblock-commerce-guide';\r\n\r\n UPDATE public.posts\r\n SET\r\n label = 'Commerce',\r\n excerpt = 'Architecture boutique, parcours de paiement et fonctions commerce premium au coeur de NextBlock.',\r\n subtitle = 'Un apercu du module commerce : multi-devises, sync taxes, expedition, inventaire et paiements connectes.'\r\n WHERE slug = 'guide-commerce-nextblock';\r\n\r\nEND $$;\r\n\r\nCOMMIT;\r\n-- English Home + Blog blocks\r\nDO $seed$\r\nDECLARE\r\n v_en_lang_id BIGINT;\r\n v_home_page_id BIGINT;\r\n v_blog_page_id BIGINT;\r\n v_contact_page_id BIGINT;\r\nBEGIN\r\n SELECT id INTO v_en_lang_id FROM public.languages WHERE code = 'en' LIMIT 1;\r\n IF v_en_lang_id IS NULL THEN RAISE EXCEPTION 'English language not found.'; END IF;\r\n\r\n SELECT id INTO v_home_page_id FROM public.pages WHERE slug = 'home' AND language_id = v_en_lang_id ORDER BY created_at DESC LIMIT 1;\r\n IF v_home_page_id IS NULL THEN RAISE EXCEPTION 'English Home page not found.'; END IF;\r\n\r\n SELECT id INTO v_blog_page_id FROM public.pages WHERE slug = 'articles' AND language_id = v_en_lang_id ORDER BY created_at DESC LIMIT 1;\r\n IF v_blog_page_id IS NULL THEN RAISE EXCEPTION 'English Articles page not found.'; END IF;\r\n\r\n SELECT id INTO v_contact_page_id FROM public.pages WHERE slug = 'contact' AND language_id = v_en_lang_id ORDER BY created_at DESC LIMIT 1;\r\n IF v_contact_page_id IS NULL THEN RAISE EXCEPTION 'English Contact page not found.'; END IF;\r\n\r\n DELETE FROM public.blocks WHERE page_id = v_home_page_id;\r\n DELETE FROM public.blocks WHERE page_id = v_blog_page_id;\r\n DELETE FROM public.blocks WHERE page_id = v_contact_page_id;\r\n\r\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\") VALUES\r\n (v_home_page_id, v_en_lang_id, 'hero',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#1e293b\",\"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\":\"<h1 class=''text-5xl md:text-6xl font-extrabold tracking-tight text-white text-center leading-tight''>Build <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''>Blazing-Fast</span></span><br class=''md:hidden'' /> Websites.</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, developer-first Next.js CMS that merges 100% Lighthouse scores with a powerful visual block editor.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Get Started\",\"url\":\"/article/how-to-setup-nextblock\",\"variant\":\"default\",\"size\":\"lg\",\"position\":\"center\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"View on GitHub\",\"url\":\"https://github.com/nextblock-cms/nextblock\",\"variant\":\"outline\",\"size\":\"lg\",\"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''>100% Lighthouse</p><p class=''text-base text-slate-300 mb-6''>Edge-rendered marketing sites, launches, and docs with uncompromising performance.</p><ul class=''space-y-3 text-sm text-slate-200''><li><span class=''text-blue-400 mr-2''>✓</span> Next.js 16 with ISR and edge caching</li><li><span class=''text-blue-400 mr-2''>✓</span> Supabase auth, data, and storage</li><li><span class=''text-blue-400 mr-2''>✓</span> Notion-style block editor powered by Tiptap</li></ul><div class=''mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg''><img src=''/images/NBcover.webp'' alt=''Nextblock cover showcasing dashboards and blocks'' class=''w-full h-auto object-cover transform group-hover:scale-105 transition-transform duration-700'' fetchpriority=''high'' /></div></div></div>\"}}]]}'::jsonb, 0),\r\n\r\n (v_home_page_id, v_en_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Key Features: The Three Pillars of NextBlock™\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ is a holistic platform that unites performance, editorial experience, and developer control so every stakeholder delivers their best work.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid gap-8 md:grid-cols-3 mt-12''><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 rounded-xl flex items-center justify-center text-black dark:text-white mb-6''><svg class=''w-6 h-6'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M13 10V3L4 14h7v7l9-11h-7z''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Built for Speed.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Architected for 100% Lighthouse scores with global delivery and near-instant FCP.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Edge Caching & ISR:</strong> Serve pages worldwide.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Critical CSS:</strong> Inline styles to eliminate blocking.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Image Opt:</strong> AVIF & blurred placeholders.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 rounded-xl flex items-center justify-center text-black dark:text-white mb-6''><svg class=''w-6 h-6'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Editor-First Experience.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>A low-code, Notion-style block editor empowers teams to ship pages without engineering help.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Notion-Style:</strong> Slash commands & drag-and-drop.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Bilingual:</strong> Manage locales from one interface.</li><li><strong class=''text-slate-800 dark:text-slate-200''>History:</strong> Restore any version with a click.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 bg-white/50 dark:bg-white/10 rounded-xl flex items-center justify-center mb-6''><svg class=''w-6 h-6 text-slate-900 dark:text-white'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Infinitely Extensible.</h3><p class=''text-sm text-slate-700 dark:text-slate-200 leading-relaxed''>Open-source control with a clean Nx monorepo and a typed SDK for limitless customization.</p><ul class=''mt-6 space-y-3 text-sm text-slate-700 dark:text-slate-200''><li><strong class=''text-slate-900 dark:text-white''>Open Source:</strong> Own the code & data forever.</li><li><strong class=''text-slate-900 dark:text-white''>Nx Monorepo:</strong> Scale confidently.</li><li><strong class=''text-slate-900 dark:text-white''>Developer SDK:</strong> Scaffold blocks in minutes.</li></ul></div></div>\"}}]]}'::jsonb, 1),\r\n\r\n (v_home_page_id, v_en_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"180deg\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#020817\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6''>Built with the Best.</h2>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-slate-400 text-center max-w-2xl mx-auto''>Every layer of NextBlock™ leans on proven developer-first technology so the platform feels familiar, performant, and trustworthy from day one.</p><div class=''grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 mt-10 text-sm font-semibold text-center text-white''><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Next.js</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>React</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Supabase</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Stripe</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tailwind</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tiptap</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Vercel</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Nx</div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6 mt-16''>Powerful for Developers. Intuitive for Editors.</h2>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid md:grid-cols-2 gap-8 mt-10 text-white''><div class=''p-8 rounded-3xl border border-white/10 bg-white/5 backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-blue-400''>For Content Creators</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Intuitive Block Editor</strong>Drag-and-drop layouts with a Notion-like interface.</li><li><strong class=''text-white block mb-1''>Rich Content Blocks</strong>Deploy heroes, galleries, testimonials, and more in one click.</li><li><strong class=''text-white block mb-1''>Effortless Media Management</strong>Organize assets with folders, tags, and bulk actions.</li><li><strong class=''text-white block mb-1''>Worry-Free Revisions</strong>Automatic version history with instant restore.</li></ul></div><div class=''p-8 rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 to-white/[0.02] backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-purple-400''>For Developers</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Next.js 16 Core</strong>Server Components, ISR, and Edge Functions ready out of the box.</li><li><strong class=''text-white block mb-1''>Supabase Integration</strong>Postgres, auth, storage, and real-time APIs without glue code.</li><li><strong class=''text-white block mb-1''>Monorepo Ready</strong>Nx-powered dev experience for scalable architectures.</li><li><strong class=''text-white block mb-1''>Extensible Block SDK</strong>Ship fully typed custom blocks and widgets.</li></ul></div></div>\"}}]]}'::jsonb, 2),\r\n (v_home_page_id, v_en_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#022c22\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#020817\",\"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.25em] text-emerald-400 font-bold mb-4''>Now Available — Premium Module</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Turn Your CMS Into<br/>a Full Storefront.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>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.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Explore Commerce Features →\",\"url\":\"/article/nextblock-commerce-guide\",\"variant\":\"default\",\"size\":\"lg\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Get a License\",\"url\":\"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\",\"variant\":\"outline\",\"size\":\"lg\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''rounded-3xl overflow-hidden border border-emerald-500/20 bg-gradient-to-br from-white/5 to-emerald-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/commerce-square.webp'' alt=''NextBlock™ Commerce dashboard showing product management'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>∞</p><p class=''text-xs text-slate-400''>Currencies</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>2</p><p class=''text-xs text-slate-400''>Providers</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>Auto</p><p class=''text-xs text-slate-400''>Tax Sync</p></div></div></div>\"}}]]}'::jsonb, 3),\r\n\r\n (v_home_page_id, v_en_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Everything You Need to Sell Online\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto mb-12''>NextBlock™ Commerce ships a complete e-commerce toolkit so you can go from catalog to checkout without third-party plugins.</p><div class=''grid gap-6 md:grid-cols-2 lg:grid-cols-3''><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Multi-Currency</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Real-time FX rates, rounding modes, charm pricing, and automatic product price sync across unlimited currencies.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Tax Automation</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Manual stacked tax rates (GST + PST) or fully automatic calculation via Stripe Tax — you choose.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Shipping Zones</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Zone-based rate resolution with country and state matching, per-currency pricing, and free-shipping thresholds.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Stripe & Freemius Checkout</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe for physical products, Freemius for digital licensing — provider-aware checkout with inventory validation.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Inventory Tracking</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Automatic quantity deduction on payment with resilient fallback paths and variant-level stock management.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Orders & Invoices</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Full order lifecycle management, stable invoice numbering, printable documents, and exportable order reports.</p></div></div>\"}}]]}'::jsonb, 4),\r\n\r\n (v_home_page_id, v_en_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#020817\",\"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\":\"<div class=''rounded-3xl overflow-hidden border border-violet-500/20 bg-gradient-to-br from-white/5 to-violet-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/cortex-ai-square.webp'' alt=''NextBlock™ Cortex AI dashboard showing block generator'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>OpenRouter</p><p class=''text-xs text-slate-400''>AI Gateway</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>BYOK</p><p class=''text-xs text-slate-400''>Cost Control</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>Zod</p><p class=''text-xs text-slate-400''>Typed Blocks</p></div></div></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-xs uppercase tracking-[0.25em] text-violet-400 font-bold mb-4''>Now Available — AI Copilot</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Supercharge Your<br/>Content with AI.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>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.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Explore AI Capabilities →\",\"url\":\"/article/nextblock-cortex-ai-guide\",\"variant\":\"default\",\"size\":\"lg\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Get a License\",\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"variant\":\"outline\",\"size\":\"lg\"}}]]}'::jsonb, 5),\r\n\r\n (v_home_page_id, v_en_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"More Than a CMS. An Ecosystem.\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ is building a sustainable open-core roadmap so the platform grows with your business.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid gap-6 lg:grid-cols-[0.75fr_1.25fr] mt-10 items-stretch''><div class=''overflow-hidden rounded-[2rem] border border-slate-200 dark:border-white/10 bg-slate-950 shadow-2xl''><img src=''/images/goals.webp'' alt=''Roadmap board outlining the NextBlock™ ecosystem and premium module direction'' class=''h-full w-full object-cover'' /><div class=''border-t border-white/10 bg-slate-950/95 px-6 py-5''><p class=''text-xs uppercase tracking-[0.24em] text-emerald-300 mb-2 font-bold''>Roadmap in motion</p><p class=''text-sm text-slate-300 mb-0''>Commerce ships first, then the broader ecosystem grows around plugins, blocks, and partner-built modules.</p></div></div><div class=''grid gap-6''><div class=''p-10 rounded-3xl border border-emerald-500/20 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-500/5 dark:to-white/5 hover:border-emerald-500/40 transition-colors''><p class=''text-xs uppercase tracking-wide text-emerald-600 dark:text-emerald-400 mb-2 font-bold''>Available now</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>NextBlock™ Commerce</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Transform your site into a composable storefront with products, checkout, multi-currency pricing, tax automation, and commerce blocks that live beside your editorial content.</p></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-violet-500/30 transition-colors''><p class=''text-xs uppercase tracking-wide text-violet-700 dark:text-violet-300 mb-2 font-bold''>Build the future</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Plugin and block marketplace</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>A community marketplace gives developers room to publish, sell, and distribute custom blocks, themes, integrations, and partner modules.</p></div></div></div>\"}},{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Join Our Community.\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-slate-600 dark:text-slate-400 text-center mx-auto''>NextBlock™ is being built in the open. Star the repo, share feedback, and help define the future of performance-first content management.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid gap-4 md:grid-cols-3 mt-10 text-sm''><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>GitHub</strong><span class=''text-slate-600 dark:text-slate-400''>Star the repo & contribute</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>X (Twitter)</strong><span class=''text-slate-600 dark:text-slate-400''>Follow updates & announcements</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>Dev.to</strong><span class=''text-slate-600 dark:text-slate-400''>Read technical deep dives</span></a></div>\"}}]]}'::jsonb, 6),\r\n\r\n (v_home_page_id, v_en_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"180deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h2 class=''text-3xl md:text-4xl font-bold text-center text-white mb-4''>Have Questions?</h2>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-center text-lg text-slate-300 mx-auto mb-8''>NextBlock™ partners with early adopters to co-build features, sponsor modules, and shape the product direction.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Get in Touch\",\"url\":\"/contact\",\"variant\":\"default\",\"size\":\"lg\",\"position\":\"center\"}}]]}'::jsonb, 6),\r\n\r\n (v_blog_page_id, v_en_lang_id, 'hero',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#1e293b\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"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\":\"<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>\"}},{\"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''>Explore architectural walkthroughs, Supabase recipes, and block editor experiments written by the Nextblock core team.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Explore Articles\",\"url\":\"/articles#latest\",\"variant\":\"default\",\"size\":\"lg\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Subscribe for Updates\",\"url\":\"https://github.com/nextblock-cms/nextblock/discussions\",\"variant\":\"outline\",\"size\":\"lg\"}}],[{\"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>\"}}]]}'::jsonb, 0),\r\n\r\n (v_blog_page_id, v_en_lang_id, 'posts_grid',\r\n '{\"postsPerPage\":6,\"columns\":3,\"showPagination\":true,\"title\":\"Latest Deep Dives\"}'::jsonb, 1);\r\n\r\n DELETE FROM public.navigation_items WHERE menu_key = 'HEADER' AND language_id = v_en_lang_id;\r\n\r\n INSERT INTO public.navigation_items (language_id, menu_key, label, url, \"order\", page_id) VALUES\r\n (v_en_lang_id, 'HEADER', 'Home', '/', 0, v_home_page_id),\r\n (v_en_lang_id, 'HEADER', 'Articles', '/articles', 1, v_blog_page_id),\r\n (v_en_lang_id, 'HEADER', 'Contact', '/contact', 3, v_contact_page_id);\r\n\r\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\") VALUES\r\n (v_contact_page_id, v_en_lang_id, 'hero', '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":1,\"text_content\":\"Let''s Build the Future Together\",\"textAlign\":\"center\",\"textColor\":\"white\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4''>NextBlock™ is an open-source project driven by community feedback. We''d love to hear your thoughts, ideas, or questions.</p>\"}}]]}'::jsonb, 0),\r\n (v_contact_page_id, v_en_lang_id, 'section', '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<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>\"}}]]}'::jsonb, 1),\r\n (v_contact_page_id, v_en_lang_id, 'form', '{\"recipient_email\":\"foo@bar.com\",\"submit_button_text\":\"Send Message\",\"success_message\":\"Thank you for your feedback! We''ll get back to you soon.\",\"fields\":[{\"temp_id\":\"name\",\"label\":\"Name\",\"field_type\":\"text\",\"is_required\":true,\"placeholder\":\"Your name\"},{\"temp_id\":\"email\",\"label\":\"Email\",\"field_type\":\"email\",\"is_required\":true,\"placeholder\":\"your@email.com\"},{\"temp_id\":\"message\",\"label\":\"Message\",\"field_type\":\"textarea\",\"is_required\":true,\"placeholder\":\"How can we help?\"}]}'::jsonb, 2);\r\nEND;\r\n$seed$;\r\nSELECT id AS home_page_id\r\nFROM public.pages\r\nWHERE slug = 'home'\r\n AND language_id = (SELECT id FROM public.languages WHERE code = 'en' LIMIT 1)\r\nORDER BY created_at DESC\r\nLIMIT 1;\r\n\r\nSELECT id AS blog_page_id\r\nFROM public.pages\r\nWHERE slug = 'articles'\r\n AND language_id = (SELECT id FROM public.languages WHERE code = 'en' LIMIT 1)\r\nORDER BY created_at DESC\r\nLIMIT 1;\r\n-- French Home + Blog blocks\r\nDO $seed_fr$\r\nDECLARE\r\n v_fr_lang_id BIGINT;\r\n v_home_page_fr_id BIGINT;\r\n v_blog_page_fr_id BIGINT;\r\n v_contact_page_fr_id BIGINT;\r\nBEGIN\r\n SELECT id INTO v_fr_lang_id FROM public.languages WHERE code = 'fr' LIMIT 1;\r\n IF v_fr_lang_id IS NULL THEN RAISE EXCEPTION 'French language not found.'; END IF;\r\n\r\n SELECT id INTO v_home_page_fr_id FROM public.pages WHERE slug = 'accueil' AND language_id = v_fr_lang_id ORDER BY created_at DESC LIMIT 1;\r\n SELECT id INTO v_blog_page_fr_id FROM public.pages WHERE slug = 'articles' AND language_id = v_fr_lang_id ORDER BY created_at DESC LIMIT 1;\r\n SELECT id INTO v_contact_page_fr_id FROM public.pages WHERE slug = 'contact' AND language_id = v_fr_lang_id ORDER BY created_at DESC LIMIT 1;\r\n\r\n IF v_home_page_fr_id IS NULL THEN RAISE EXCEPTION 'French home page not found.'; END IF;\r\n IF v_blog_page_fr_id IS NULL THEN RAISE EXCEPTION 'French articles page not found.'; END IF;\r\n IF v_contact_page_fr_id IS NULL THEN RAISE EXCEPTION 'French contact page not found.'; END IF;\r\n\r\n DELETE FROM public.blocks WHERE page_id IN (v_home_page_fr_id, v_blog_page_fr_id, v_contact_page_fr_id);\r\n\r\n DELETE FROM public.navigation_items WHERE menu_key = 'HEADER' AND language_id = v_fr_lang_id;\r\n\r\n INSERT INTO public.navigation_items (language_id, menu_key, label, url, \"order\", page_id) VALUES\r\n (v_fr_lang_id, 'HEADER', 'Accueil', '/accueil', 0, v_home_page_fr_id),\r\n (v_fr_lang_id, 'HEADER', 'Articles', '/articles', 1, v_blog_page_fr_id),\r\n (v_fr_lang_id, 'HEADER', 'Contact', '/contact', 3, v_contact_page_fr_id);\r\n\r\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\") VALUES\r\n (v_contact_page_fr_id, v_fr_lang_id, 'hero', '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":1,\"text_content\":\"Bâtissons le futur ensemble\",\"textAlign\":\"center\",\"textColor\":\"white\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4''>NextBlock™ est un projet open-source propulsé par vos retours. Nous serions ravis d''entendre vos idées ou vos questions.</p>\"}}]]}'::jsonb, 0),\r\n (v_contact_page_fr_id, v_fr_lang_id, 'section', '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<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>\"}}]]}'::jsonb, 1),\r\n (v_contact_page_fr_id, v_fr_lang_id, 'form', '{\"recipient_email\":\"foo@bar.com\",\"submit_button_text\":\"Envoyer le message\",\"success_message\":\"Merci pour vos retours ! Nous vous répondrons bientôt.\",\"fields\":[{\"temp_id\":\"nom\",\"label\":\"Nom\",\"field_type\":\"text\",\"is_required\":true,\"placeholder\":\"Votre nom\"},{\"temp_id\":\"email\",\"label\":\"Email\",\"field_type\":\"email\",\"is_required\":true,\"placeholder\":\"votre@email.com\"},{\"temp_id\":\"message\",\"label\":\"Message\",\"field_type\":\"textarea\",\"is_required\":true,\"placeholder\":\"Comment pouvons-nous vous aider ?\"}]}'::jsonb, 2);\r\n\r\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\") VALUES\r\n (v_home_page_fr_id, v_fr_lang_id, 'hero',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#1e293b\",\"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\":\"<h1 class=''text-5xl md:text-6xl font-bold tracking-tight text-white text-center drop-shadow-lg''>Créez des sites <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''>Ultra-Rapides</span></span><br class=''md:hidden'' />.</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™ est le CMS Next.js open-source alliant scores Lighthouse parfaits et éditeur visuel puissant.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Commencer\",\"url\":\"/article/comment-configurer-nextblock\",\"variant\":\"default\",\"size\":\"lg\",\"position\":\"center\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Voir sur GitHub\",\"url\":\"https://github.com/nextblock-cms/nextblock\",\"variant\":\"outline\",\"size\":\"lg\",\"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''>Pourquoi migrer</p><p class=''text-3xl font-bold text-white mb-2''>100% Lighthouse</p><p class=''text-base text-slate-300 mb-6''>Sites marketing et docs rendus à l''edge avec des performances irréprochables.</p><ul class=''space-y-3 text-sm text-slate-200''><li><span class=''text-blue-400 mr-2''>✓</span> Next.js 16 avec ISR et cache edge</li><li><span class=''text-blue-400 mr-2''>✓</span> Supabase pour l''auth, les données et le stockage</li><li><span class=''text-blue-400 mr-2''>✓</span> Éditeur de blocs type Notion sur Tiptap</li></ul><div class=''mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg''><img src=''/images/NBcover.webp'' alt=''Couverture Nextblock'' class=''w-full h-auto object-cover transform group-hover:scale-105 transition-transform duration-700'' fetchpriority=''high'' /></div></div></div>\"}}]]}'::jsonb, 0),\r\n\r\n (v_home_page_fr_id, v_fr_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Fonctionnalités clés : les trois piliers de NextBlock™\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ unifie performances, expérience éditoriale et contrôle développeur pour que chaque équipe livre son meilleur travail.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid gap-8 md:grid-cols-3 mt-12''><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Vitesse Extrême.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Pensé pour des scores Lighthouse parfaits avec une diffusion mondiale.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Edge Caching:</strong> Servez vos pages partout.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Critical CSS:</strong> Styles en ligne pour éviter les blocages.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Images Opt:</strong> AVIF et placeholders floutés.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Expérience Éditeur.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Un éditeur façon Notion pour publier sans dépendre des développeurs.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Visuel:</strong> Héros, galeries, témoignages.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Média:</strong> Dossiers, tags et actions groupées.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Historique:</strong> Restauration complète.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Extensible à l''Infini.</h3><p class=''text-sm text-slate-700 dark:text-slate-200 leading-relaxed''>Un socle Next.js + Supabase modulaire, extensible et auto-hébergeable.</p><ul class=''mt-6 space-y-3 text-sm text-slate-700 dark:text-slate-200''><li><strong class=''text-slate-900 dark:text-white''>SDK de blocs:</strong> Composants typés.</li><li><strong class=''text-slate-900 dark:text-white''>CLI:</strong> Générez modules en minutes.</li><li><strong class=''text-slate-900 dark:text-white''>Monorepo Nx:</strong> Dépendances maintenables.</li></ul></div></div>\"}}]]}'::jsonb, 1),\r\n\r\n (v_home_page_fr_id, v_fr_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"180deg\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#020817\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6''>Conçu avec les meilleurs outils.</h2>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-slate-400 text-center max-w-2xl mx-auto''>Chaque couche de NextBlock™ repose sur des technologies éprouvées pour une expérience familière et performante.</p><div class=''grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 mt-10 text-sm font-semibold text-center text-white''><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Next.js</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>React</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Supabase</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Stripe</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tailwind</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tiptap</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Vercel</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Nx</div></div>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6 mt-16''>Puissant pour les développeurs. Intuitif pour les éditeurs.</h2>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid md:grid-cols-2 gap-8 mt-10 text-white''><div class=''p-8 rounded-3xl border border-white/10 bg-white/5 backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-blue-400''>Pour les créateurs</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Éditeur de blocs</strong>Glisser-déposer façon Notion.</li><li><strong class=''text-white block mb-1''>Blocs riches</strong>Héros, galeries, témoignages.</li><li><strong class=''text-white block mb-1''>Médiathèque</strong>Dossiers, tags et actions groupées.</li><li><strong class=''text-white block mb-1''>Versions sécurisées</strong>Historique et restauration instantanée.</li></ul></div><div class=''p-8 rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 to-white/[0.02] backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-purple-400''>Pour les développeurs</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Next.js 16</strong>Server Components, ISR et Edge prêts à l''emploi.</li><li><strong class=''text-white block mb-1''>Supabase</strong>Postgres, auth, stockage, temps réel.</li><li><strong class=''text-white block mb-1''>Monorepo Nx</strong>Dépendances lisibles et centrales.</li><li><strong class=''text-white block mb-1''>SDK de blocs</strong>Widgets typés et extensibles.</li></ul></div></div>\"}}]]}'::jsonb, 2),\r\n (v_home_page_fr_id, v_fr_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#022c22\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#020817\",\"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.25em] text-emerald-400 font-bold mb-4''>Disponible — Module Premium</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Transformez votre CMS<br/>en vitrine complète.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>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à.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Découvrir Commerce →\",\"url\":\"/article/guide-commerce-nextblock\",\"variant\":\"default\",\"size\":\"lg\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Obtenir une licence\",\"url\":\"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\",\"variant\":\"outline\",\"size\":\"lg\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''rounded-3xl overflow-hidden border border-emerald-500/20 bg-gradient-to-br from-white/5 to-emerald-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/commerce-square.webp'' alt=''Tableau de bord NextBlock™ Commerce'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>∞</p><p class=''text-xs text-slate-400''>Devises</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>2</p><p class=''text-xs text-slate-400''>Fournisseurs</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>Auto</p><p class=''text-xs text-slate-400''>Taxes</p></div></div></div>\"}}]]}'::jsonb, 3),\r\n\r\n (v_home_page_fr_id, v_fr_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Tout pour vendre en ligne\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto mb-12''>NextBlock™ Commerce livre une boîte à outils e-commerce complète pour aller du catalogue au paiement sans plugins tiers.</p><div class=''grid gap-6 md:grid-cols-2 lg:grid-cols-3''><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Multi-Devises</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Taux de change en temps réel, modes d''arrondi, prix charme et synchronisation automatique sur toutes les devises.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Taxes Automatiques</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Taux manuels empilés (TPS + TVQ) ou calcul automatique via Stripe Tax — à vous de choisir.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Zones d''Expédition</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Résolution par pays et état, tarification par devise et seuils de livraison gratuite.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Stripe & Freemius</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe pour les produits physiques, Freemius pour les licences numériques — checkout intelligent avec validation d''inventaire.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Suivi d''Inventaire</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Déduction automatique des quantités au paiement avec gestion des stocks par variante.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Commandes & Factures</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Gestion du cycle de vie des commandes, numérotation stable des factures et rapports de commandes exportables.</p></div></div>\"}}]]}'::jsonb, 4),\r\n\r\n (v_home_page_fr_id, v_fr_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":50},{\"color\":\"#020817\",\"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\":\"<div class=''rounded-3xl overflow-hidden border border-violet-500/20 bg-gradient-to-br from-white/5 to-violet-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/cortex-ai-square.webp'' alt=''Tableau de bord NextBlock™ Cortex AI montrant le générateur de blocs'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>OpenRouter</p><p class=''text-xs text-slate-400''>Passerelle IA</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>BYOK</p><p class=''text-xs text-slate-400''>Contrôle des coûts</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>Zod</p><p class=''text-xs text-slate-400''>Blocs typés</p></div></div></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-xs uppercase tracking-[0.25em] text-violet-400 font-bold mb-4''>Disponible — Copilote IA</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Boostez votre<br/>contenu avec l''IA.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>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.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Découvrir l''IA →\",\"url\":\"/article/nextblock-cortex-ai-guide\",\"variant\":\"default\",\"size\":\"lg\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Obtenir une licence\",\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"variant\":\"outline\",\"size\":\"lg\"}}]]}'::jsonb, 5),\r\n\r\n (v_home_page_fr_id, v_fr_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Plus qu''un CMS. Un écosystème.\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ construit une feuille de route open-core durable qui évolue avec votre activité.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid gap-6 lg:grid-cols-[0.75fr_1.25fr] mt-10 items-stretch''><div class=''overflow-hidden rounded-[2rem] border border-slate-200 dark:border-white/10 bg-slate-950 shadow-2xl''><img src=''/images/goals.webp'' alt=''Tableau de roadmap montrant la direction de l''ecosysteme NextBlock™ et des modules premium'' class=''h-full w-full object-cover'' /><div class=''border-t border-white/10 bg-slate-950/95 px-6 py-5''><p class=''text-xs uppercase tracking-[0.24em] text-emerald-300 mb-2 font-bold''>Roadmap en mouvement</p><p class=''text-sm text-slate-300 mb-0''>Le commerce arrive en premier, puis l''ecosysteme s''etend avec des plugins, des blocs et des modules construits par les partenaires.</p></div></div><div class=''grid gap-6''><div class=''p-10 rounded-3xl border border-emerald-500/20 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-500/5 dark:to-white/5 hover:border-emerald-500/40 transition-colors''><p class=''text-xs uppercase tracking-wide text-emerald-600 dark:text-emerald-400 mb-2 font-bold''>Disponible maintenant</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>NextBlock™ Commerce</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Transformez votre site en vitrine composable avec produits, checkout, tarification multi-devise, taxes automatiques et blocs commerce relies a votre contenu editorial.</p></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-violet-500/30 transition-colors''><p class=''text-xs uppercase tracking-wide text-violet-700 dark:text-violet-300 mb-2 font-bold''>Construire la suite</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Marketplace de plugins et blocs</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Une marketplace communautaire ouvrira la voie a la publication, la vente et la distribution de blocs, themes, integrations et modules partenaires.</p></div></div></div>\"}},{\"block_type\":\"heading\",\"content\":{\"level\":2,\"text_content\":\"Rejoignez la communauté.\",\"textAlign\":\"center\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ se construit en public. Ajoutez une étoile, partagez vos retours et façonnez l''avenir du CMS orienté performance.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''grid gap-4 md:grid-cols-3 mt-10 text-sm''><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>GitHub</strong><span class=''text-slate-600 dark:text-slate-400''>Ajoutez une étoile & contribuez</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>X (Twitter)</strong><span class=''text-slate-600 dark:text-slate-400''>Suivez les annonces</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>Dev.to</strong><span class=''text-slate-600 dark:text-slate-400''>Lisez nos articles techniques</span></a></div>\"}}]]}'::jsonb, 6),\r\n\r\n (v_home_page_fr_id, v_fr_lang_id, 'section',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"180deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<h2 class=''text-3xl md:text-4xl font-bold text-center text-white mb-4''>Des questions ?</h2>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=''text-center text-base text-slate-300 max-w-2xl mx-auto''>NextBlock™ co-construit avec des partenaires : fonctionnalités, modules sponsorisés et direction produit.</p>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Nous contacter\",\"url\":\"mailto:info@nextblock.dev\",\"variant\":\"default\",\"size\":\"lg\",\"position\":\"center\"}}]]}'::jsonb, 7),\r\n\r\n (v_blog_page_fr_id, v_fr_lang_id, 'hero',\r\n '{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#1e293b\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"lg\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"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\":\"<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>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<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>\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"Explorer les articles\",\"url\":\"/articles#latest\",\"variant\":\"default\",\"size\":\"lg\"}},{\"block_type\":\"button\",\"content\":{\"text\":\"S''abonner aux mises à jour\",\"url\":\"https://github.com/nextblock-cms/nextblock/discussions\",\"variant\":\"outline\",\"size\":\"lg\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=''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>\"}}]]}'::jsonb, 0),\r\n\r\n (v_blog_page_fr_id, v_fr_lang_id, 'posts_grid',\r\n '{\"postsPerPage\":6,\"columns\":3,\"showPagination\":true,\"title\":\"Derniers articles\"}'::jsonb, 1);\r\nEND;\r\n$seed_fr$;\r\n\r\n-- Convert seeded 'hero' block types to 'section' with is_hero = true\r\nUPDATE public.blocks\r\nSET\r\n block_type = 'section',\r\n content = COALESCE(content, '{}'::jsonb) || '{\"is_hero\": true}'::jsonb\r\nWHERE block_type = 'hero';\r\n\r\n-- Post content blocks for all 3 posts (EN + FR)\r\nWITH target_posts AS (\r\n SELECT id, language_id, slug\r\n FROM public.posts\r\n WHERE slug IN ('how-nextblock-works', 'comment-nextblock-fonctionne', 'how-to-setup-nextblock', 'comment-configurer-nextblock', 'nextblock-commerce-guide', 'guide-commerce-nextblock')\r\n),\r\npurged AS (\r\n DELETE FROM public.blocks\r\n WHERE post_id IN (SELECT id FROM target_posts)\r\n)\r\nINSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\r\n\r\n-- Post 1 EN: How NextBlock™ Works\r\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\r\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock™ is designed so the hosted CMS, the open-source starter, and the developer tooling all feel like the same product. The shared Nx workspace, typed block contracts, and reusable editor package keep product polish and developer velocity moving together.</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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Shared foundation</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Marketing pages, CMS screens, and the starter template evolve 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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Blocks with guardrails</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Zod schemas, defaults, and renderer contracts make every custom block safer to 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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Product-grade editing</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>The Tiptap layer gives editors a richer surface without hiding the underlying HTML power.</p>\r\n </div>\r\n</div>\r\n\r\n<div class='flex flex-col md:flex-row gap-8 items-start my-12'>\r\n <div class='w-full md:w-3/5 space-y-4'>\r\n <h2>Monorepo Layout and Dependency Flow</h2>\r\n <p>The <code>apps/nextblock</code> directory contains the production Next.js experience, including the public site and authenticated CMS shell. The <code>apps/create-nextblock</code> CLI mirrors that foundation so teams can start from the same product decisions instead of rebuilding them from scratch.</p>\r\n <ul class='list-disc pl-6 space-y-2 text-sm'>\r\n <li><strong>@nextblock-cms/ui</strong> - UI components, tokens, and shared design primitives</li>\r\n <li><strong>@nextblock-cms/utils</strong> - translations, environment guards, and storage helpers</li>\r\n <li><strong>@nextblock-cms/db</strong> - migrations, typed database access, and generated types</li>\r\n <li><strong>@nextblock-cms/editor</strong> - the reusable Tiptap v3 editing surface</li>\r\n <li><strong>@nextblock-cms/sdk</strong> - typed contracts for block authorship and validation</li>\r\n <li><strong>@nextblock-cms/ecommerce</strong> - the premium commerce module when activated</li>\r\n </ul>\r\n <p>Run <code>nx graph</code> and you can see exactly how changes ripple through the workspace. Path aliases from <code>tsconfig.base.json</code> and the shared Tailwind setup help keep design parity between marketing pages, admin screens, and generated projects.</p>\r\n </div>\r\n <aside class='w-full md:w-2/5 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5'>\r\n <img src='/images/nx-graph.webp' alt='Nx project graph preview showing apps and shared libraries linked together' class='w-full h-auto rounded-2xl object-cover' />\r\n <p class='mt-3 text-sm text-slate-500 dark:text-slate-400'>Nx makes every workspace relationship visible, which is exactly why the 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'>A single visual system spans content modeling, editing, and future premium modules like commerce.</figcaption>\r\n</figure>\r\n\r\n<h2>Block Registry as Product Surface</h2>\r\n<p>The block registry in <code>apps/nextblock/lib/blocks/blockRegistry.ts</code> is the source of truth for available block types, Zod schemas, starter content, and editor or renderer components. Today that includes everything from <code>text</code> and <code>heading</code> to <code>section</code>, <code>posts_grid</code>, <code>checkout</code>, and <code>product_details</code>.</p>\r\n<p>Sections support nested column arrays, so layouts can be composed like real pages instead of flat content lists. Helpers such as <code>getBlockDefinition()</code>, <code>getInitialContent()</code>, and <code>validateBlockContent()</code> keep that flexibility strongly typed.</p>\r\n\r\n<h2>The Editing Layer</h2>\r\n<p>The <code>@nextblock-cms/editor</code> package wraps Tiptap v3 into a reusable editorial surface with slash commands, floating and bubble menus, drag handles, tables, task lists, character counts, and syntax-highlighted code blocks. It deliberately preserves richer HTML so advanced teams are not boxed into a simplified subset.</p>\r\n\r\n<h2>Inside the CMS Shell</h2>\r\n<p>Within <code>apps/nextblock/app/cms</code>, each feature area follows a repeatable pattern: list pages, create and edit routes, scoped client components, and server actions that wrap Supabase mutations. The result feels consistent for editors while keeping credentials and permissions on the server side.</p>\r\n\r\n<h2>Open Core Without Product Drift</h2>\r\n<p>The core CMS is open source under AGPL. Premium modules like <code>@nextblock-cms/ecommerce</code> remain source-available but are activated through <code>package_activations</code> and <code>verifyPackageOnline()</code>. That means the same shell can stay clean for open-source users while revealing commerce surfaces only when the license is active.</p>\r\n\r\n<h2>Why It Holds Together</h2>\r\n<p>The Nx workspace keeps libraries honest, the Next.js app enforces UI consistency, Supabase migrations codify access rules, and the Tiptap editor gives collaborators the same authoring experience regardless of deployment. When a team runs <code>npm create nextblock</code>, they inherit the full operating model, not just a pile of files.</p>$$\r\n), 0 FROM target_posts tp WHERE tp.slug = 'how-nextblock-works'\r\n\r\nUNION ALL\r\n\r\n-- Post 1 FR: Comment NextBlock™ fonctionne\r\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\r\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock™ relie le CMS hébergé, le starter open source et les outils dev dans un même socle produit. Le workspace Nx, les contrats de blocs typés et l'éditeur partagé permettent d'avancer vite sans sacrifier la cohérence.</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'>Socle unique</p>\r\n <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Une même base</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Le site public, le shell CMS et le starter gardent les mêmes choix produit et la même direction visuelle.</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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Blocs avec garde-fous</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Schémas Zod, contenus par défaut et contrats de rendu rendent les extensions plus sûres à maintenir.</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'>Expérience éditoriale</p>\r\n <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Edition premium</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>La couche Tiptap donne aux éditeurs une interface riche sans masquer la puissance HTML pour les cas avancés.</p>\r\n </div>\r\n</div>\r\n\r\n<div class='flex flex-col md:flex-row gap-8 items-start my-12'>\r\n <div class='w-full md:w-3/5 space-y-4'>\r\n <h2>Architecture monorepo et flux de dépendances</h2>\r\n <p>Le dossier <code>apps/nextblock</code> contient l'expérience Next.js en production, incluant le site public et le shell CMS authentifié. Le CLI <code>apps/create-nextblock</code> reprend cette base pour que les nouveaux projets partent des mêmes décisions produit.</p>\r\n <ul class='list-disc pl-6 space-y-2 text-sm'>\r\n <li><strong>@nextblock-cms/ui</strong> - composants UI, tokens et primitives visuelles partagées</li>\r\n <li><strong>@nextblock-cms/utils</strong> - traductions, gardes d'environnement et helpers de stockage</li>\r\n <li><strong>@nextblock-cms/db</strong> - migrations, accès base typé et types générés</li>\r\n <li><strong>@nextblock-cms/editor</strong> - la surface d'édition Tiptap v3 réutilisable</li>\r\n <li><strong>@nextblock-cms/sdk</strong> - contrats typés pour l'auteuring et la validation des blocs</li>\r\n <li><strong>@nextblock-cms/ecommerce</strong> - le module commerce premium lorsqu'il est activé</li>\r\n </ul>\r\n <p>Lancez <code>nx graph</code> et vous voyez immédiatement comment un changement se propage. Les alias de <code>tsconfig.base.json</code> et la configuration Tailwind partagée aident à garder une vraie parité entre marketing, back-office et projets générés.</p>\r\n </div>\r\n <aside class='w-full md:w-2/5 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5'>\r\n <img src='/images/nx-graph.webp' alt='Apercu du graphe Nx montrant les applications et librairies partagees' class='w-full h-auto rounded-2xl object-cover' />\r\n <p class='mt-3 text-sm text-slate-500 dark:text-slate-400'>Nx rend visibles les relations du workspace, ce qui aide le starter, le CMS et les packages a rester alignes.</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='Visuel NextBlock™ montrant le CMS relie a des modules reutilisables et des 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'>Un seul langage visuel relie la modelisation de contenu, l'edition et les futurs modules premium comme le commerce.</figcaption>\r\n</figure>\r\n\r\n<h2>Le registre de blocs comme surface produit</h2>\r\n<p>Le registre dans <code>apps/nextblock/lib/blocks/blockRegistry.ts</code> définit les types disponibles, les schémas Zod, les contenus de départ et les composants d'édition ou de rendu. On y trouve aujourd'hui des blocs comme <code>text</code>, <code>heading</code>, <code>section</code>, <code>posts_grid</code>, <code>checkout</code> et <code>product_details</code>.</p>\r\n<p>Les sections supportent des colonnes imbriquées, ce qui permet de composer de vraies pages plutôt qu'une simple liste de contenu. Des helpers comme <code>getBlockDefinition()</code>, <code>getInitialContent()</code> and <code>validateBlockContent()</code> gardent cette flexibilité bien typée.</p>\r\n\r\n<h2>La couche d'edition</h2>\r\n<p>Le package <code>@nextblock-cms/editor</code> enveloppe Tiptap v3 dans une surface éditoriale réutilisable avec slash commands, menus contextuels, drag handles, tableaux, listes de taches, compteurs et blocs de code. Le but est de conserver un HTML riche quand une equipe en a besoin.</p>\r\n\r\n<h2>A l'interieur du shell CMS</h2>\r\n<p>Dans <code>apps/nextblock/app/cms</code>, chaque zone suit un motif lisible : pages de liste, routes de creation et d'edition, composants clients cibles et server actions qui encapsulent les mutations Supabase. Les editeurs y gagnent une interface coherente et les identifiants restent cote serveur.</p>\r\n\r\n<h2>Open core sans derive produit</h2>\r\n<p>Le coeur du CMS est open source sous AGPL. Les modules premium comme <code>@nextblock-cms/ecommerce</code> restent disponibles en source mais sont actives via <code>package_activations</code> et <code>verifyPackageOnline()</code>. Le meme shell peut donc rester simple pour l'open source tout en deverrouillant les surfaces commerce au bon moment.</p>\r\n\r\n<h2>Pourquoi l'ensemble tient</h2>\r\n<p>Le workspace Nx garde les librairies honnetes, l'app Next.js maintient la coherence UI, les migrations Supabase codifient les regles d'acces, et l'editeur Tiptap donne la meme experience de contribution quel que soit le deploiement. Quand une equipe lance <code>npm create nextblock</code>, elle recupere une facon de travailler complete, pas juste des fichiers.</p>$$\r\n), 0 FROM target_posts tp WHERE tp.slug = 'comment-nextblock-fonctionne'\r\n\r\nUNION ALL\r\n\r\n-- Post 2 EN: How to Setup NextBlock™\r\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\r\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>There are two strong ways to start with NextBlock: clone the full monorepo if you want the whole platform, or scaffold a standalone app if you want to ship quickly. Both paths land you on the same editorial model, design system, and CMS foundation.</p>\r\n\r\n<div class='rounded-[2rem] border border-blue-200 bg-blue-50/80 p-6 my-10 dark:border-blue-500/20 dark:bg-blue-500/10'>\r\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Choose your path</p>\r\n <div class='grid gap-6 md:grid-cols-2 mt-5'>\r\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\r\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Monorepo</h3>\r\n <p class='text-sm text-slate-600 dark:text-slate-300'>Best for contributors, plugin authors, and teams that want direct access to every app and shared package.</p>\r\n </div>\r\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\r\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>CLI starter</h3>\r\n <p class='text-sm text-slate-600 dark:text-slate-300'>Best for launching a production-ready Next.js project with NextBlock™ already wired in and easy to deploy.</p>\r\n </div>\r\n </div>\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/included.webp' alt='NextBlock™ platform artwork showing the CMS, blocks, and integrations that ship together' 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'>Whichever path you choose, you still inherit the same block editor, CMS shell, and shared product language.</figcaption>\r\n</figure>\r\n\r\n<h2>Path 1: Clone the Monorepo</h2>\r\n<p>This route is ideal when you want the full Nx workspace and every internal package available locally.</p>\r\n\r\n<div class='grid gap-6 md:grid-cols-2 my-8'>\r\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\r\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>You get</p>\r\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\r\n <li>The public site, CMS app, CLI source, and shared libraries</li>\r\n <li>Direct access to <code>libs/</code> for custom block and package work</li>\r\n <li>Workspace tools like <code>nx graph</code> for dependency visibility</li>\r\n </ul>\r\n </div>\r\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\r\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Good fit for</p>\r\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\r\n <li>Core contributors and maintainers</li>\r\n <li>Teams building custom modules or premium extensions</li>\r\n <li>Agencies that want end-to-end control over the platform</li>\r\n </ul>\r\n </div>\r\n</div>\r\n\r\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\r\ncd nextblock\r\nnpm install\r\nnpm run setup</code></pre>\r\n\r\n<p>The <code>npm run setup</code> wizard creates <code>.env.local</code>, asks for your Supabase keys, can wire up R2 and SMTP, links the Supabase CLI, and pushes the schema with <code>npm run db:push</code>.</p>\r\n\r\n<p>Then start the app:</p>\r\n<pre><code>npx nx serve nextblock</code></pre>\r\n\r\n<p>Useful monorepo commands:</p>\r\n<pre><code># Build every workspace package\r\nnpm run all-builds\r\n\r\n# Lint the main application\r\nnpm run nx:lint:nextblock\r\n\r\n# Regenerate database types\r\nnpm run db:types\r\n\r\n# Inspect workspace relationships\r\nnpx nx graph</code></pre>\r\n\r\n<h2>Path 2: Use the CLI Starter</h2>\r\n<p>If your goal is to launch quickly, the CLI gives you a standalone Next.js app with NextBlock™ already embedded.</p>\r\n\r\n<pre><code>npm create nextblock@latest my-site\r\ncd my-site</code></pre>\r\n\r\n<p>The CLI copies a production-ready template, rewrites workspace imports to published packages, and can run the same setup flow for you. Your result is a normal Next.js app with no Nx requirement.</p>\r\n\r\n<p>Configure your environment in <code>.env.local</code>:</p>\r\n<pre><code>NEXT_PUBLIC_SUPABASE_URL=your-project-url\r\nNEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key\r\nSUPABASE_SERVICE_ROLE_KEY=your-service-role-key\r\nNEXT_PUBLIC_URL=http://localhost:3000</code></pre>\r\n\r\n<p>Push the schema and start developing:</p>\r\n<pre><code>npm run db:push\r\nnpm run dev</code></pre>\r\n\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='text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Tip</p>\r\n <p class='mb-0 text-sm text-slate-700 dark:text-slate-200'>The CLI path is the fastest way to evaluate NextBlock™ with your own content model before you decide whether you need the full workspace.</p>\r\n</div>\r\n\r\n<h2>Activating Premium Modules</h2>\r\n<p>For CLI-generated projects, the commerce package can be activated with a single command:</p>\r\n<pre><code>npx create-nextblock activate ecommerce</code></pre>\r\n<p>This injects wrappers for <code>/cms/orders</code>, <code>/cms/products</code>, <code>/checkout</code>, and the checkout API, all gated through <code>verifyPackageOnline()</code> so premium routes stay aligned with your license.</p>\r\n\r\n<h2>Deployment</h2>\r\n<p>NextBlock™ deploys like a standard Next.js app. Push to Vercel, Netlify, or any Node.js host, then make sure your server-side environment variables such as the Supabase service role, Stripe keys, and <code>CRON_SECRET</code> are configured in that environment.</p>$$\r\n), 0 FROM target_posts tp WHERE tp.slug = 'how-to-setup-nextblock'\r\n\r\nUNION ALL\r\n\r\n-- Post 2 FR: Comment configurer NextBlock™\r\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\r\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>Il existe deux bonnes facons de lancer NextBlock™ : cloner le monorepo complet si vous voulez toute la plateforme, ou partir du CLI si vous voulez aller vite. Dans les deux cas, vous retrouvez le meme modele editorial, le meme shell CMS et la meme base produit.</p>\r\n\r\n<div class='rounded-[2rem] border border-blue-200 bg-blue-50/80 p-6 my-10 dark:border-blue-500/20 dark:bg-blue-500/10'>\r\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Choisissez votre chemin</p>\r\n <div class='grid gap-6 md:grid-cols-2 mt-5'>\r\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\r\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Monorepo</h3>\r\n <p class='text-sm text-slate-600 dark:text-slate-300'>Ideal pour les contributeurs, auteurs de plugins et equipes qui veulent travailler directement dans tous les packages partages.</p>\r\n </div>\r\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\r\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Starter CLI</h3>\r\n <p class='text-sm text-slate-600 dark:text-slate-300'>Ideal pour demarrer une app Next.js prete a deployer avec NextBlock™ deja integre.</p>\r\n </div>\r\n </div>\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/included.webp' alt='Visuel NextBlock™ montrant le CMS, les blocs et les integrations qui arrivent ensemble' 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 heritez du meme editeur de blocs, du meme shell CMS et du meme langage produit.</figcaption>\r\n</figure>\r\n\r\n<h2>Chemin 1 : cloner le monorepo</h2>\r\n<p>Cette option est la meilleure si vous voulez tout le workspace Nx et chaque package interne disponible en local.</p>\r\n\r\n<div class='grid gap-6 md:grid-cols-2 my-8'>\r\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\r\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Vous obtenez</p>\r\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\r\n <li>Le site public, l'app CMS, le code du CLI et les librairies partagees</li>\r\n <li>Un acces direct a <code>libs/</code> pour les blocs et modules personnalises</li>\r\n <li>Les outils de workspace comme <code>nx graph</code> pour visualiser les dependances</li>\r\n </ul>\r\n </div>\r\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\r\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Bon choix pour</p>\r\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\r\n <li>Les mainteneurs et contributeurs coeur</li>\r\n <li>Les equipes qui construisent des extensions sur mesure</li>\r\n <li>Les agences qui veulent un controle complet de la plateforme</li>\r\n </ul>\r\n </div>\r\n</div>\r\n\r\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\r\ncd nextblock\r\nnpm install\r\nnpm run setup</code></pre>\r\n\r\n<p>L'assistant <code>npm run setup</code> cree <code>.env.local</code>, demande vos cles Supabase, peut brancher R2 et SMTP, lie le CLI Supabase, puis pousse le schema avec <code>npm run db:push</code>.</p>\r\n\r\n<p>Puis lancez l'application :</p>\r\n<pre><code>npx nx serve nextblock</code></pre>\r\n\r\n<p>Commandes utiles dans le monorepo :</p>\r\n<pre><code># Build de tous les packages\r\nnpm run all-builds\r\n\r\n# Lint de l'application principale\r\nnpm run nx:lint:nextblock\r\n\r\n# Regenerer les types base de donnees\r\nnpm run db:types\r\n\r\n# Inspecter les relations du workspace\r\nnpx nx graph</code></pre>\r\n\r\n<h2>Chemin 2 : utiliser le starter CLI</h2>\r\n<p>Si votre but est d'aller vite, le CLI vous donne une app Next.js autonome avec NextBlock™ deja integre.</p>\r\n\r\n<pre><code>npm create nextblock@latest mon-site\r\ncd mon-site</code></pre>\r\n\r\n<p>Le CLI copie un template pret pour la production, remplace les imports workspace par les packages publies, et peut lancer la meme configuration initiale. Le resultat reste une app Next.js classique, sans dependance a Nx.</p>\r\n\r\n<p>Configurez votre environnement dans <code>.env.local</code> :</p>\r\n<pre><code>NEXT_PUBLIC_SUPABASE_URL=your-project-url\r\nNEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key\r\nSUPABASE_SERVICE_ROLE_KEY=your-service-role-key\r\nNEXT_PUBLIC_URL=http://localhost:3000</code></pre>\r\n\r\n<p>Poussez le schema puis demarrez :</p>\r\n<pre><code>npm run db:push\r\nnpm run dev</code></pre>\r\n\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='text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Conseil</p>\r\n <p class='mb-0 text-sm text-slate-700 dark:text-slate-200'>Le chemin CLI est le moyen le plus rapide d'evaluer NextBlock™ avec votre propre modele de contenu avant de passer, si besoin, au workspace complet.</p>\r\n</div>\r\n\r\n<h2>Activer les modules premium</h2>\r\n<p>Pour un projet genere via le CLI, le package commerce peut etre active avec une seule commande :</p>\r\n<pre><code>npx create-nextblock activate ecommerce</code></pre>\r\n<p>Cette commande injecte les wrappers pour <code>/cms/orders</code>, <code>/cms/products</code>, <code>/checkout</code> et l'API checkout, le tout protege par <code>verifyPackageOnline()</code> afin de garder les routes premium alignees avec la licence.</p>\r\n\r\n<h2>Deploiement</h2>\r\n<p>NextBlock™ se deploie comme une app Next.js standard. Publiez sur Vercel, Netlify ou tout hebergeur Node.js, puis configurez les variables serveur comme la cle service role Supabase, les cles Stripe et <code>CRON_SECRET</code>.</p>$$\r\n), 0 FROM target_posts tp WHERE tp.slug = 'comment-configurer-nextblock'\r\n\r\nUNION ALL\r\n\r\n-- Post 3 EN: NextBlock™ Commerce Guide\r\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\r\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock™ Commerce is the first premium module in the ecosystem: a source-available storefront layer that plugs directly into the same editorial system as the CMS. It is built for teams that want content, catalog, and checkout to live inside one product surface instead of three disconnected tools.</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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Catalog + checkout</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Products, variants, orders, shipping, and invoices all plug into the existing 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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Multi-currency ready</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Automatic FX sync, rounding strategies, and per-product overrides keep international pricing practical.</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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Provider-aware flow</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Stripe and Freemius are handled differently so the storefront can stay clean without hiding complexity.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Product Catalog</h2>\r\n<p>Commerce supports physical and digital products with variants, attributes, localized product media, independent pricing, SKUs, and stock levels. Product assets stay in the same media library editors already use for marketing pages, so content and commerce teams are not working in separate silos.</p>\r\n\r\n<h2>Multi-Currency Engine</h2>\r\n<p>The pricing engine is built for real-world stores, not just a demo checkout:</p>\r\n<ul>\r\n <li><strong>Unlimited currencies</strong> with ISO codes, symbols, and stored exchange rates</li>\r\n <li><strong>Automatic FX sync</strong> from Frankfurter or a custom provider via <code>FX_API_BASE_URL</code></li>\r\n <li><strong>Rounding modes</strong> including nearest, up, down, and charm pricing like <code>9.99</code></li>\r\n <li><strong>Store-managed auto-sync</strong> so product prices convert when rates refresh</li>\r\n <li><strong>Rebasing</strong> when the default currency changes</li>\r\n <li><strong>Per-product overrides</strong> when a catalog item needs explicit pricing in specific markets</li>\r\n</ul>\r\n\r\n<h2>Tax Automation</h2>\r\n<p>Teams can stay manual when they need control, or delegate tax math to Stripe Tax when they want automation:</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 <h4 class='font-bold text-slate-900 dark:text-white mb-2'>Manual mode</h4>\r\n <p class='text-sm text-slate-600 dark:text-slate-400'>Define rates by country and optional state or province. Stacked taxes such as GST + PST are supported, and tax lines are stored in <code>orders.tax_details</code>.</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 <h4 class='font-bold text-slate-900 dark:text-white mb-2'>Automatic mode</h4>\r\n <p class='text-sm text-slate-600 dark:text-slate-400'>Stripe Tax calculates the final amounts. Product and shipping tax codes travel with the line items, and the webhook resync replaces provisional values with final totals.</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 or province, support localized method names, per-currency pricing, free-shipping thresholds, and priority-based fallbacks when an exact match is not found.</p>\r\n<p>The checkout layer is provider-aware:</p>\r\n<ul>\r\n <li><strong>Stripe</strong> handles physical goods, inventory checks, shipping calculation, tax, customer upserts, and Checkout Sessions</li>\r\n <li><strong>Freemius</strong> handles digital licensing, plan resolution, and checkout URLs with sandbox support</li>\r\n <li>Mixed-provider carts are rejected so the buyer journey stays understandable</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 positioned as the first premium module in a larger roadmap, which makes it feel like part of a growing platform instead of a bolt-on add-on.</figcaption>\r\n</figure>\r\n\r\n<h2>Inventory, Orders, and Invoices</h2>\r\n<p>When quantity tracking is enabled, checkout validates requested quantities against <code>inventory_items</code>. On payment confirmation, <code>apply_order_inventory_deduction()</code> reduces stock with a resilient fallback path that can use direct SQL if the RPC layer fails.</p>\r\n<ul>\r\n <li>Order statuses move from <code>pending</code> to <code>paid</code> to <code>shipped</code>, with cancellation and refund states available too</li>\r\n <li>Invoice numbering is generated through database functions for consistency</li>\r\n <li>Printable invoice documents pull from <code>invoice_settings</code></li>\r\n <li>Customers can review order history and invoice access from the storefront side</li>\r\n <li><strong>Coming soon:</strong> exportable order reporting and analytics dashboards</li>\r\n</ul>\r\n\r\n<h2>Commerce Surfaces Inside the CMS</h2>\r\n<p>When the ecommerce package is active, the CMS exposes product list, create, and edit views with media and variants, inventory management, order detail screens, shipping configuration, payment provider settings, tax setup, and currency management. The important part is not only that those screens exist, but that they feel native inside the same shell your content team is already using.</p>$$\r\n), 0 FROM target_posts tp WHERE tp.slug = 'nextblock-commerce-guide'\r\n\r\nUNION ALL\r\n\r\n-- Post 3 FR: Guide Commerce NextBlock™\r\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\r\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock™ Commerce est le premier module premium de l'ecosysteme : une couche storefront source-available qui se branche directement sur le meme systeme editorial que le CMS. L'objectif est de rapprocher contenu, catalogue et checkout dans une seule surface produit.</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'>Base commerce</p>\r\n <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Catalogue + checkout</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Produits, variantes, commandes, livraison et factures vivent dans le meme shell 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 globale</p>\r\n <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Multi-devise</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Sync FX automatique, strategies d'arrondi et overrides par produit rendent les prix internationaux realistes.</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'>Workflow operateur</p>\r\n <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Par fournisseur</h3>\r\n <p class='mt-3 text-sm text-slate-600 dark:text-slate-300'>Stripe et Freemius sont traites differemment pour garder un parcours d'achat propre sans cacher la complexite.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Catalogue produits</h2>\r\n<p>Le module gere produits physiques et numeriques avec variantes, attributs, medias localises, prix independants, SKU et niveaux de stock. Les assets produits restent dans la meme bibliotheque media que les pages marketing, ce qui evite de separer equipes contenu et equipes commerce.</p>\r\n\r\n<h2>Moteur multi-devise</h2>\r\n<p>Le moteur tarifaire vise un vrai usage boutique, pas seulement une demo :</p>\r\n<ul>\r\n <li><strong>Devises illimitees</strong> avec codes ISO, symboles et taux stockes</li>\r\n <li><strong>Synchronisation FX automatique</strong> depuis Frankfurter ou un provider custom via <code>FX_API_BASE_URL</code></li>\r\n <li><strong>Modes d'arrondi</strong> dont nearest, up, down et prix charme comme <code>9.99</code></li>\r\n <li><strong>Auto-sync magasin</strong> pour convertir les prix quand les taux changent</li>\r\n <li><strong>Rebasement</strong> lorsqu'on change la devise par defaut</li>\r\n <li><strong>Overrides par produit</strong> quand un article demande un prix fixe sur certains marches</li>\r\n</ul>\r\n\r\n<h2>Taxes automatiques</h2>\r\n<p>Les equipes peuvent rester en mode manuel ou confier le calcul a Stripe Tax :</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 <h4 class='font-bold text-slate-900 dark:text-white mb-2'>Mode manuel</h4>\r\n <p class='text-sm text-slate-600 dark:text-slate-400'>Definition des taux par pays et eventuellement par province. Les taxes empilees comme TPS + TVQ sont supportees, avec stockage dans <code>orders.tax_details</code>.</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 <h4 class='font-bold text-slate-900 dark:text-white mb-2'>Mode automatique</h4>\r\n <p class='text-sm text-slate-600 dark:text-slate-400'>Stripe Tax calcule les montants finaux. Les codes fiscaux voyagent avec les line items et le webhook remplace les valeurs provisoires par les montants definitifs.</p>\r\n </div>\r\n</div>\r\n\r\n<h2>Livraison et checkout</h2>\r\n<p>Les zones de livraison correspondent par pays et etat ou province, gerent des noms localises, des prix par devise, des seuils de livraison gratuite, et des fallbacks par priorite quand aucune correspondance exacte n'est trouvee.</p>\r\n<p>Le checkout est conscient du fournisseur :</p>\r\n<ul>\r\n <li><strong>Stripe</strong> gere les biens physiques, les verifications d'inventaire, la livraison, les taxes, les clients et les Checkout Sessions</li>\r\n <li><strong>Freemius</strong> gere les licences numeriques, la resolution des plans et les URLs de checkout avec support sandbox</li>\r\n <li>Les paniers melangeant plusieurs fournisseurs sont refuses pour garder un parcours plus clair</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='Tableau de roadmap commerce montrant les objectifs premium et les futures capacites storefront de 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'>Le commerce est presente comme le premier module premium d'une feuille de route plus large, ce qui renforce l'idee d'une vraie plateforme en croissance.</figcaption>\r\n</figure>\r\n\r\n<h2>Inventaire, commandes et factures</h2>\r\n<p>Quand le suivi des quantites est actif, le checkout valide les demandes contre <code>inventory_items</code>. A la confirmation du paiement, <code>apply_order_inventory_deduction()</code> retire le stock avec un chemin de repli resilient si la couche RPC echoue.</p>\r\n<ul>\r\n <li>Les statuts de commande passent de <code>pending</code> a <code>paid</code> puis <code>shipped</code>, avec annulation et remboursement si besoin</li>\r\n <li>La numerotation des factures est geree par des fonctions SQL pour rester coherente</li>\r\n <li>Les documents facture tirent leurs informations de <code>invoice_settings</code></li>\r\n <li>Les clients peuvent consulter leur historique et leurs factures</li>\r\n <li><strong>Bientot :</strong> exports de commandes et tableaux de bord analytiques</li>\r\n</ul>\r\n\r\n<h2>Surfaces commerce dans le CMS</h2>\r\n<p>Quand le package ecommerce est actif, le CMS expose les vues produit, edition avec medias et variantes, gestion d'inventaire, detail des commandes, configuration livraison, parametres de paiement, taxes et devises. L'enjeu principal est que tout cela paraisse natif dans le meme shell que l'equipe contenu utilise deja.</p>$$\r\n), 0 FROM target_posts tp WHERE tp.slug = 'guide-commerce-nextblock';\r\n"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"version": "00000000000011",
|
|
74
|
+
"name": "00000000000011_setup_cortex_ai_settings.sql",
|
|
75
|
+
"sql": "-- 00000000000011_setup_cortex_ai_settings.sql\n-- NextBlock Cortex AI settings and sensitive BYOK policy hardening.\n\nCOMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys such as Cortex AI BYOK are protected by row-level policies.';\n\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_sensitive_read_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_sensitive_insert_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_sensitive_update_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_sensitive_delete_policy ON public.site_settings;\n\nCREATE POLICY site_settings_read_policy\n ON public.site_settings\n FOR SELECT\n TO public\n USING (\n key <> 'cortex_ai_openrouter_api_key'\n OR (\n key = 'cortex_ai_openrouter_api_key'\n AND (SELECT auth.role()) = 'authenticated'\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n\nCREATE POLICY site_settings_insert_policy\n ON public.site_settings\n FOR INSERT\n TO authenticated\n WITH CHECK (\n (\n key <> 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key = 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n\nCREATE POLICY site_settings_update_policy\n ON public.site_settings\n FOR UPDATE\n TO authenticated\n USING (\n (\n key <> 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key = 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n )\n WITH CHECK (\n (\n key <> 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key = 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n\nCREATE POLICY site_settings_delete_policy\n ON public.site_settings\n FOR DELETE\n TO authenticated\n USING (\n (\n key <> 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key = 'cortex_ai_openrouter_api_key'\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"version": "00000000000012",
|
|
79
|
+
"name": "00000000000012_setup_commerce_coupons.sql",
|
|
80
|
+
"sql": "-- 00000000000012_setup_commerce_coupons.sql\r\n-- Unified commerce coupons for Stripe and Freemius checkout.\r\n\r\nCREATE TABLE public.coupons (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n code text NOT NULL,\r\n name text NOT NULL,\r\n internal_note text,\r\n provider_scope text NOT NULL DEFAULT 'all',\r\n discount_type text NOT NULL,\r\n discount_amount integer NOT NULL,\r\n is_active boolean NOT NULL DEFAULT true,\r\n starts_at timestamptz,\r\n ends_at timestamptz,\r\n redemption_limit integer,\r\n redemptions_count integer NOT NULL DEFAULT 0,\r\n freemius_sync_status text NOT NULL DEFAULT 'not_synced',\r\n freemius_sync_error text,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now(),\r\n CONSTRAINT coupons_code_not_blank CHECK (char_length(btrim(code)) > 0),\r\n CONSTRAINT coupons_name_not_blank CHECK (char_length(btrim(name)) > 0),\r\n CONSTRAINT coupons_provider_scope_valid\r\n CHECK (provider_scope IN ('all', 'stripe', 'freemius')),\r\n CONSTRAINT coupons_discount_type_valid\r\n CHECK (discount_type IN ('percent', 'fixed')),\r\n CONSTRAINT coupons_discount_amount_positive\r\n CHECK (discount_amount > 0),\r\n CONSTRAINT coupons_percent_amount_valid\r\n CHECK (discount_type <> 'percent' OR discount_amount <= 100),\r\n CONSTRAINT coupons_redemption_limit_positive\r\n CHECK (redemption_limit IS NULL OR redemption_limit > 0),\r\n CONSTRAINT coupons_redemptions_count_nonnegative\r\n CHECK (redemptions_count >= 0),\r\n CONSTRAINT coupons_date_window_valid\r\n CHECK (starts_at IS NULL OR ends_at IS NULL OR starts_at < ends_at),\r\n CONSTRAINT coupons_freemius_sync_status_valid\r\n CHECK (freemius_sync_status IN ('not_synced', 'pending', 'synced', 'failed', 'not_required'))\r\n);\r\n\r\nCREATE UNIQUE INDEX coupons_code_unique\r\n ON public.coupons (upper(code));\r\n\r\nCOMMENT ON TABLE public.coupons IS\r\n 'Unified commerce coupons managed by NextBlock CMS and applied through provider-aware checkout.';\r\nCOMMENT ON COLUMN public.coupons.provider_scope IS\r\n 'Provider eligibility: all, stripe, or freemius.';\r\nCOMMENT ON COLUMN public.coupons.discount_amount IS\r\n 'Percent value for percent coupons, or fixed minor-unit amount for fixed coupons.';\r\nCOMMENT ON COLUMN public.coupons.freemius_sync_status IS\r\n 'Aggregate status for syncing this coupon to Freemius product coupon records.';\r\n\r\nCREATE TABLE public.coupon_products (\r\n coupon_id uuid NOT NULL REFERENCES public.coupons(id) ON DELETE CASCADE,\r\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n PRIMARY KEY (coupon_id, product_id)\r\n);\r\n\r\nCOMMENT ON TABLE public.coupon_products IS\r\n 'Optional product allow-list for coupons. No rows means all products in the provider scope are eligible.';\r\n\r\nCREATE TABLE public.coupon_freemius_mappings (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n coupon_id uuid NOT NULL REFERENCES public.coupons(id) ON DELETE CASCADE,\r\n product_id uuid REFERENCES public.products(id) ON DELETE SET NULL,\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 NOT NULL DEFAULT 'pending',\r\n sync_error text,\r\n remote_payload jsonb,\r\n last_synced_at timestamptz,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now(),\r\n CONSTRAINT coupon_freemius_mappings_product_not_blank\r\n CHECK (char_length(btrim(freemius_product_id)) > 0),\r\n CONSTRAINT coupon_freemius_mappings_code_not_blank\r\n CHECK (char_length(btrim(freemius_coupon_code)) > 0),\r\n CONSTRAINT coupon_freemius_mappings_sync_status_valid\r\n CHECK (sync_status IN ('pending', 'synced', 'failed', 'deleted'))\r\n);\r\n\r\nCREATE UNIQUE INDEX coupon_freemius_mappings_coupon_product_unique\r\n ON public.coupon_freemius_mappings (coupon_id, freemius_product_id);\r\n\r\nCREATE TABLE public.coupon_redemptions (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n coupon_id uuid REFERENCES public.coupons(id) ON DELETE SET NULL,\r\n order_id uuid REFERENCES public.orders(id) ON DELETE CASCADE,\r\n coupon_code text NOT NULL,\r\n provider text NOT NULL CHECK (provider IN ('stripe', 'freemius')),\r\n discount_total integer NOT NULL DEFAULT 0 CHECK (discount_total >= 0),\r\n user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,\r\n customer_email text,\r\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n redeemed_at timestamptz NOT NULL DEFAULT now(),\r\n CONSTRAINT coupon_redemptions_code_not_blank CHECK (char_length(btrim(coupon_code)) > 0)\r\n);\r\n\r\nCREATE UNIQUE INDEX coupon_redemptions_order_unique\r\n ON public.coupon_redemptions (order_id)\r\n WHERE order_id IS NOT NULL;\r\n\r\nALTER TABLE public.orders\r\n ADD COLUMN coupon_id uuid REFERENCES public.coupons(id) ON DELETE SET NULL,\r\n ADD COLUMN coupon_code text,\r\n ADD COLUMN discount_total integer NOT NULL DEFAULT 0 CHECK (discount_total >= 0),\r\n ADD COLUMN discount_details jsonb;\r\n\r\nCOMMENT ON COLUMN public.orders.coupon_code IS\r\n 'Coupon code applied to the order at checkout time.';\r\nCOMMENT ON COLUMN public.orders.discount_total IS\r\n 'Total discount applied to this order in the smallest currency unit.';\r\nCOMMENT ON COLUMN public.orders.discount_details IS\r\n 'Provider-aware coupon quote details captured at checkout.';\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_coupons_write()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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_coupon_freemius_mappings_write()\r\nRETURNS trigger\r\nLANGUAGE plpgsql\r\nSET search_path = ''\r\nAS $$\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\nDROP TRIGGER IF EXISTS on_coupons_write ON public.coupons;\r\nCREATE TRIGGER on_coupons_write\r\n BEFORE INSERT OR UPDATE ON public.coupons\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_coupons_write();\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\r\n BEFORE INSERT OR UPDATE ON public.coupon_freemius_mappings\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_coupon_freemius_mappings_write();\r\n\r\nCREATE INDEX idx_coupons_active_dates\r\n ON public.coupons (is_active, starts_at, ends_at);\r\n\r\nCREATE INDEX idx_coupon_products_product_id\r\n ON public.coupon_products (product_id);\r\n\r\nCREATE INDEX idx_coupon_freemius_mappings_product_id\r\n ON public.coupon_freemius_mappings (product_id)\r\n WHERE product_id IS NOT NULL;\r\n\r\nCREATE INDEX idx_coupon_freemius_mappings_freemius_product_id\r\n ON public.coupon_freemius_mappings (freemius_product_id);\r\n\r\nCREATE INDEX idx_coupon_redemptions_coupon_id\r\n ON public.coupon_redemptions (coupon_id);\r\n\r\nCREATE INDEX idx_coupon_redemptions_user_id\r\n ON public.coupon_redemptions (user_id)\r\n WHERE user_id IS NOT NULL;\r\n\r\nCREATE INDEX idx_orders_coupon_id\r\n ON public.orders (coupon_id)\r\n WHERE coupon_id IS NOT NULL;\r\n\r\nALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY;\r\nALTER TABLE public.coupon_products ENABLE ROW LEVEL SECURITY;\r\nALTER TABLE public.coupon_freemius_mappings ENABLE ROW LEVEL SECURITY;\r\nALTER TABLE public.coupon_redemptions ENABLE ROW LEVEL SECURITY;\r\n\r\nGRANT ALL ON public.coupons TO authenticated, service_role;\r\nGRANT ALL ON public.coupon_products TO authenticated, service_role;\r\nGRANT ALL ON public.coupon_freemius_mappings TO authenticated, service_role;\r\nGRANT ALL ON public.coupon_redemptions TO authenticated, service_role;\r\n\r\nCREATE POLICY coupons_admin_select_policy\r\n ON public.coupons\r\n FOR SELECT\r\n TO authenticated\r\n USING (((SELECT public.is_admin()) IS TRUE));\r\n\r\nCREATE POLICY coupons_admin_insert_policy\r\n ON public.coupons\r\n FOR INSERT\r\n TO authenticated\r\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\r\n\r\nCREATE POLICY coupons_admin_update_policy\r\n ON public.coupons\r\n FOR UPDATE\r\n TO authenticated\r\n USING (((SELECT public.is_admin()) IS TRUE))\r\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\r\n\r\nCREATE POLICY coupons_admin_delete_policy\r\n ON public.coupons\r\n FOR DELETE\r\n TO authenticated\r\n USING (((SELECT public.is_admin()) IS TRUE));\r\n\r\nCREATE POLICY coupons_service_role_policy\r\n ON public.coupons\r\n FOR ALL\r\n TO service_role\r\n USING (true)\r\n WITH CHECK (true);\r\n\r\nCREATE POLICY coupon_products_admin_policy\r\n ON public.coupon_products\r\n FOR ALL\r\n TO authenticated\r\n USING (((SELECT public.is_admin()) IS TRUE))\r\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\r\n\r\nCREATE POLICY coupon_products_service_role_policy\r\n ON public.coupon_products\r\n FOR ALL\r\n TO service_role\r\n USING (true)\r\n WITH CHECK (true);\r\n\r\nCREATE POLICY coupon_freemius_mappings_admin_policy\r\n ON public.coupon_freemius_mappings\r\n FOR ALL\r\n TO authenticated\r\n USING (((SELECT public.is_admin()) IS TRUE))\r\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\r\n\r\nCREATE POLICY coupon_freemius_mappings_service_role_policy\r\n ON public.coupon_freemius_mappings\r\n FOR ALL\r\n TO service_role\r\n USING (true)\r\n WITH CHECK (true);\r\n\r\nCREATE POLICY coupon_redemptions_admin_select_policy\r\n ON public.coupon_redemptions\r\n FOR SELECT\r\n TO authenticated\r\n USING (((SELECT public.is_admin()) IS TRUE));\r\n\r\nCREATE POLICY coupon_redemptions_service_role_policy\r\n ON public.coupon_redemptions\r\n FOR ALL\r\n TO service_role\r\n USING (true)\r\n WITH CHECK (true);\r\n"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"version": "00000000000013",
|
|
84
|
+
"name": "00000000000013_setup_cortex_ai_db_mutation_audit.sql",
|
|
85
|
+
"sql": "-- 00000000000013_setup_cortex_ai_db_mutation_audit.sql\r\n-- Durable audit trail for confirmed Cortex AI database mutations.\r\n\r\nCREATE TABLE IF NOT EXISTS public.cortex_ai_db_mutation_audit (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n actor_user_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\r\n tool_name text NOT NULL,\r\n action_name text NOT NULL,\r\n target_tables text[] NOT NULL DEFAULT '{}'::text[],\r\n operation_summary text NOT NULL,\r\n payload_hash text NOT NULL,\r\n payload jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n preview jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n status text NOT NULL CHECK (status IN ('success', 'failure')),\r\n error_message text,\r\n created_at timestamptz NOT NULL DEFAULT now()\r\n);\r\n\r\nCOMMENT ON TABLE public.cortex_ai_db_mutation_audit IS\r\n 'Audit trail for confirmed Cortex AI database mutation attempts.';\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.payload IS\r\n 'Redacted tool input payload for the confirmed mutation attempt.';\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.preview IS\r\n 'Redacted confirmation preview shown before the mutation was confirmed.';\r\n\r\nALTER TABLE public.cortex_ai_db_mutation_audit ENABLE ROW LEVEL SECURITY;\r\n\r\nGRANT SELECT, INSERT ON public.cortex_ai_db_mutation_audit TO authenticated, service_role;\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_admin_read_policy\r\n ON public.cortex_ai_db_mutation_audit;\r\n\r\nCREATE POLICY cortex_ai_db_mutation_audit_admin_read_policy\r\n ON public.cortex_ai_db_mutation_audit\r\n FOR SELECT\r\n TO authenticated\r\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_service_role_policy\r\n ON public.cortex_ai_db_mutation_audit;\r\n\r\nCREATE POLICY cortex_ai_db_mutation_audit_service_role_policy\r\n ON public.cortex_ai_db_mutation_audit\r\n FOR ALL\r\n TO service_role\r\n USING (true)\r\n WITH CHECK (true);\r\n"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"version": "00000000000014",
|
|
89
|
+
"name": "00000000000014_setup_content_drafts.sql",
|
|
90
|
+
"sql": "-- 00000000000014_setup_content_drafts.sql\r\n-- Draft snapshots for front-end visual editing.\r\n\r\nCREATE TABLE IF NOT EXISTS public.content_drafts (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n parent_type text NOT NULL CHECK (parent_type IN ('page', 'post')),\n parent_id bigint NOT NULL,\n author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\r\n base_version integer NOT NULL DEFAULT 1,\r\n meta jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n blocks jsonb NOT NULL DEFAULT '[]'::jsonb,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now(),\r\n CONSTRAINT content_drafts_parent_unique UNIQUE (parent_type, parent_id),\r\n CONSTRAINT content_drafts_blocks_array CHECK (jsonb_typeof(blocks) = 'array'),\r\n CONSTRAINT content_drafts_meta_object CHECK (jsonb_typeof(meta) = 'object')\n);\n\nALTER TABLE public.content_drafts\n ADD COLUMN IF NOT EXISTS parent_type text,\n ADD COLUMN IF NOT EXISTS parent_id bigint,\n ADD COLUMN IF NOT EXISTS author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\n ADD COLUMN IF NOT EXISTS base_version integer NOT NULL DEFAULT 1,\n ADD COLUMN IF NOT EXISTS meta jsonb NOT NULL DEFAULT '{}'::jsonb,\n ADD COLUMN IF NOT EXISTS blocks jsonb NOT NULL DEFAULT '[]'::jsonb,\n ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(),\n ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();\n\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = 'content_drafts_parent_unique'\n AND conrelid = 'public.content_drafts'::regclass\n ) THEN\n ALTER TABLE public.content_drafts\n ADD CONSTRAINT content_drafts_parent_unique UNIQUE (parent_type, parent_id);\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = 'content_drafts_blocks_array'\n AND conrelid = 'public.content_drafts'::regclass\n ) THEN\n ALTER TABLE public.content_drafts\n ADD CONSTRAINT content_drafts_blocks_array CHECK (jsonb_typeof(blocks) = 'array');\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = 'content_drafts_meta_object'\n AND conrelid = 'public.content_drafts'::regclass\n ) THEN\n ALTER TABLE public.content_drafts\n ADD CONSTRAINT content_drafts_meta_object CHECK (jsonb_typeof(meta) = 'object');\n END IF;\nEND $$;\n\nCOMMENT ON TABLE public.content_drafts IS\n 'Draft snapshots used by Draft Mode and front-end visual editing before publishing to live page/post rows.';\nCOMMENT ON COLUMN public.content_drafts.parent_type IS\r\n 'The content table this draft belongs to: page or post.';\r\nCOMMENT ON COLUMN public.content_drafts.parent_id IS\r\n 'ID of the page or post being drafted.';\r\nCOMMENT ON COLUMN public.content_drafts.base_version IS\r\n 'Published page/post version the draft was created from.';\r\nCOMMENT ON COLUMN public.content_drafts.meta IS\r\n 'Draft page/post metadata snapshot.';\r\nCOMMENT ON COLUMN public.content_drafts.blocks IS\r\n 'Ordered draft block snapshot, including block ids, block types, content, language ids, and order values.';\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_author_id_idx ON public.content_drafts (author_id);\nCREATE INDEX IF NOT EXISTS content_drafts_parent_idx ON public.content_drafts (parent_type, parent_id);\nCREATE INDEX IF NOT EXISTS content_drafts_updated_at_idx ON public.content_drafts (updated_at DESC);\n\r\nDROP TRIGGER IF EXISTS on_content_drafts_update ON public.content_drafts;\r\nCREATE TRIGGER on_content_drafts_update\r\n BEFORE UPDATE ON public.content_drafts\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nALTER TABLE public.content_drafts ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS content_drafts_select_policy\n ON public.content_drafts;\n\nCREATE POLICY content_drafts_select_policy\n ON public.content_drafts\n FOR SELECT\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS content_drafts_insert_policy\n ON public.content_drafts;\n\nCREATE POLICY content_drafts_insert_policy\n ON public.content_drafts\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS content_drafts_update_policy\n ON public.content_drafts;\n\nCREATE POLICY content_drafts_update_policy\n ON public.content_drafts\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS content_drafts_delete_policy\n ON public.content_drafts;\n\nCREATE POLICY content_drafts_delete_policy\n ON public.content_drafts\n FOR DELETE\n TO authenticated\r\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\r\n"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"version": "00000000000015",
|
|
94
|
+
"name": "00000000000015_setup_product_drafts.sql",
|
|
95
|
+
"sql": "-- 00000000000015_setup_product_drafts.sql\r\n-- Product draft snapshots for front-end visual editing.\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_drafts (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\n author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\r\n meta jsonb NOT NULL DEFAULT '{}'::jsonb,\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now(),\r\n CONSTRAINT product_drafts_product_unique UNIQUE (product_id),\r\n CONSTRAINT product_drafts_meta_object CHECK (jsonb_typeof(meta) = 'object')\n);\n\nALTER TABLE public.product_drafts\n ADD COLUMN IF NOT EXISTS product_id uuid REFERENCES public.products(id) ON DELETE CASCADE,\n ADD COLUMN IF NOT EXISTS author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,\n ADD COLUMN IF NOT EXISTS meta jsonb NOT NULL DEFAULT '{}'::jsonb,\n ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(),\n ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();\n\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = 'product_drafts_product_unique'\n AND conrelid = 'public.product_drafts'::regclass\n ) THEN\n ALTER TABLE public.product_drafts\n ADD CONSTRAINT product_drafts_product_unique UNIQUE (product_id);\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = 'product_drafts_meta_object'\n AND conrelid = 'public.product_drafts'::regclass\n ) THEN\n ALTER TABLE public.product_drafts\n ADD CONSTRAINT product_drafts_meta_object CHECK (jsonb_typeof(meta) = 'object');\n END IF;\nEND $$;\n\nCOMMENT ON TABLE public.product_drafts IS\n 'Draft product metadata snapshots used by Draft Mode and front-end visual editing before publishing to live product rows.';\nCOMMENT ON COLUMN public.product_drafts.product_id IS\r\n 'ID of the product being drafted.';\r\nCOMMENT ON COLUMN public.product_drafts.meta IS\r\n 'Draft product metadata snapshot. Front-end visual editing currently mutates visible title, short_description, and description_json fields.';\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_author_id_idx ON public.product_drafts (author_id);\nCREATE INDEX IF NOT EXISTS product_drafts_product_id_idx ON public.product_drafts (product_id);\nCREATE INDEX IF NOT EXISTS product_drafts_updated_at_idx ON public.product_drafts (updated_at DESC);\n\r\nDROP TRIGGER IF EXISTS on_product_drafts_update ON public.product_drafts;\r\nCREATE TRIGGER on_product_drafts_update\r\n BEFORE UPDATE ON public.product_drafts\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nALTER TABLE public.product_drafts ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_drafts_select_policy\n ON public.product_drafts;\n\nCREATE POLICY product_drafts_select_policy\n ON public.product_drafts\n FOR SELECT\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS product_drafts_insert_policy\n ON public.product_drafts;\n\nCREATE POLICY product_drafts_insert_policy\n ON public.product_drafts\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS product_drafts_update_policy\n ON public.product_drafts;\n\nCREATE POLICY product_drafts_update_policy\n ON public.product_drafts\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS product_drafts_delete_policy\n ON public.product_drafts;\n\nCREATE POLICY product_drafts_delete_policy\n ON public.product_drafts\n FOR DELETE\n TO authenticated\r\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\r\n"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"version": "00000000000016",
|
|
99
|
+
"name": "00000000000016_add_feature_image_to_pages.sql",
|
|
100
|
+
"sql": "ALTER TABLE public.pages\nADD COLUMN IF NOT EXISTS feature_image_id uuid REFERENCES public.media(id) ON DELETE SET NULL;\n\nCOMMENT ON COLUMN public.pages.feature_image_id IS\n 'ID of the media item to be used as the page feature image.';\n\nCREATE INDEX IF NOT EXISTS idx_pages_feature_image_id\n ON public.pages (feature_image_id);\n"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"version": "00000000000017",
|
|
104
|
+
"name": "00000000000017_add_product_blocks.sql",
|
|
105
|
+
"sql": "-- 00000000000017_add_product_blocks.sql\n-- Migration to support modular block editor for product descriptions.\n\n-- 1. Add product_id column to public.blocks\nALTER TABLE public.blocks\n ADD COLUMN product_id uuid REFERENCES public.products(id) ON DELETE CASCADE;\n\n-- 2. Drop the old parent constraint on public.blocks\nALTER TABLE public.blocks\n DROP CONSTRAINT IF EXISTS check_exactly_one_parent;\n\n-- 3. Add the updated parent constraint supporting product_id\nALTER TABLE public.blocks\n ADD CONSTRAINT check_exactly_one_parent CHECK (\n (page_id IS NOT NULL AND post_id IS NULL AND product_id IS NULL)\n OR (post_id IS NOT NULL AND page_id IS NULL AND product_id IS NULL)\n OR (product_id IS NOT NULL AND page_id IS NULL AND post_id IS NULL)\n );\n\n-- 4. Add blocks column to product_drafts table\nALTER TABLE public.product_drafts\n ADD COLUMN IF NOT EXISTS blocks jsonb NOT NULL DEFAULT '[]'::jsonb;\n\n-- 5. Add check constraint to ensure product_drafts.blocks is a JSON array\nALTER TABLE public.product_drafts\n DROP CONSTRAINT IF EXISTS product_drafts_blocks_array;\n\nALTER TABLE public.product_drafts\n ADD CONSTRAINT product_drafts_blocks_array CHECK (jsonb_typeof(blocks) = 'array');\n\n-- 6. Migrate existing product description_json to blocks\nINSERT INTO public.blocks (product_id, language_id, block_type, content, \"order\")\nSELECT\n id as product_id,\n language_id,\n 'text' as block_type,\n jsonb_build_object('html_content', COALESCE(description_json#>>'{}', '')) as content,\n 0 as \"order\"\nFROM public.products\nWHERE description_json IS NOT NULL AND (description_json#>>'{}') <> ''\nON CONFLICT DO NOTHING;\n\n-- 7. Recreate blocks_anon_read_policy\nDROP POLICY IF EXISTS blocks_anon_read_policy ON public.blocks;\nCREATE POLICY blocks_anon_read_policy\n ON public.blocks\n FOR SELECT\n TO anon\n USING (\n (\n page_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.pages AS p\n WHERE p.id = blocks.page_id\n AND p.status = 'published'\n )\n )\n OR (\n post_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.posts AS pt\n WHERE pt.id = blocks.post_id\n AND pt.status = 'published'\n AND (pt.published_at IS NULL OR pt.published_at <= now())\n )\n )\n OR (\n product_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.products AS pr\n WHERE pr.id = blocks.product_id\n AND pr.status = 'active'\n )\n )\n );\n\n-- 8. Recreate blocks_read_policy\nDROP POLICY IF EXISTS blocks_read_policy ON public.blocks;\nCREATE POLICY blocks_read_policy\n ON public.blocks\n FOR SELECT\n TO authenticated\n USING (\n ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n OR (\n (\n page_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.pages AS p\n WHERE p.id = blocks.page_id\n AND p.status = 'published'\n )\n )\n OR (\n post_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.posts AS pt\n WHERE pt.id = blocks.post_id\n AND pt.status = 'published'\n AND (pt.published_at IS NULL OR pt.published_at <= now())\n )\n )\n OR (\n product_id IS NOT NULL\n AND EXISTS (\n SELECT 1\n FROM public.products AS pr\n WHERE pr.id = blocks.product_id\n AND pr.status = 'active'\n )\n )\n )\n );\n"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"version": "00000000000018",
|
|
109
|
+
"name": "00000000000018_setup_bot_protection_settings.sql",
|
|
110
|
+
"sql": "-- 00000000000018_setup_bot_protection_settings.sql\n-- NextBlock Bot Protection settings and sensitive keys policy hardening.\n\nCOMMENT 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.';\n\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\n\nCREATE POLICY site_settings_read_policy\n ON public.site_settings\n FOR SELECT\n TO public\n USING (\n key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n OR (\n key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT auth.role()) = 'authenticated'\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n\nCREATE POLICY site_settings_insert_policy\n ON public.site_settings\n FOR INSERT\n TO authenticated\n WITH CHECK (\n (\n key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n\nCREATE POLICY site_settings_update_policy\n ON public.site_settings\n FOR UPDATE\n TO authenticated\n USING (\n (\n key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n )\n WITH CHECK (\n (\n key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n\nCREATE POLICY site_settings_delete_policy\n ON public.site_settings\n FOR DELETE\n TO authenticated\n USING (\n (\n key NOT IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER')\n )\n OR (\n key IN ('cortex_ai_openrouter_api_key', 'bot_protection_secret')\n AND (SELECT public.get_current_user_role()) = 'ADMIN'\n )\n );\n"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"version": "00000000000019",
|
|
114
|
+
"name": "00000000000019_add_product_categories.sql",
|
|
115
|
+
"sql": "-- 00000000000019_add_product_categories.sql\n-- Migration adding categories and many-to-many product_categories join table.\n\nCREATE TABLE IF NOT EXISTS public.categories (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n name text NOT NULL,\n slug text NOT NULL UNIQUE,\n description text,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.product_categories (\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\n category_id uuid NOT NULL REFERENCES public.categories(id) ON DELETE CASCADE,\n PRIMARY KEY (product_id, category_id)\n);\n\n-- Indexing foreign keys for performance\nCREATE INDEX IF NOT EXISTS idx_product_categories_product_id ON public.product_categories(product_id);\nCREATE INDEX IF NOT EXISTS idx_product_categories_category_id ON public.product_categories(category_id);\n\n-- Documenting the tables\nCOMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';\nCOMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';\n\n-- Enable Row Level Security (RLS)\nALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.product_categories ENABLE ROW LEVEL SECURITY;\n\n-- Define RLS Policies\nCREATE POLICY \"Public can view categories\"\n ON public.categories\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY \"Public can view product_categories\"\n ON public.product_categories\n FOR SELECT\n TO public\n USING (true);\n\nCREATE POLICY \"Admin can manage categories\"\n ON public.categories\n FOR ALL\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY \"Admin can manage product_categories\"\n ON public.product_categories\n FOR ALL\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\n-- Define Grants for roles\nGRANT SELECT ON public.categories TO anon;\nGRANT ALL ON public.categories TO authenticated;\nGRANT ALL ON public.categories TO service_role;\n\nGRANT SELECT ON public.product_categories TO anon;\nGRANT ALL ON public.product_categories TO authenticated;\nGRANT ALL ON public.product_categories TO service_role;\n"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"version": "00000000000020",
|
|
119
|
+
"name": "00000000000020_add_category_translations.sql",
|
|
120
|
+
"sql": "-- 00000000000020_add_category_translations.sql\n-- Add name_translations and description_translations columns to public.categories.\n\nALTER TABLE public.categories\nADD COLUMN IF NOT EXISTS name_translations jsonb NOT NULL DEFAULT '{}'::jsonb,\nADD COLUMN IF NOT EXISTS description_translations jsonb NOT NULL DEFAULT '{}'::jsonb;\n\n-- Comment on columns\nCOMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {\"fr\": \"Numérique\"}).';\nCOMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';\n"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"version": "00000000000021",
|
|
124
|
+
"name": "00000000000021_migrate_hero_blocks_to_sections.sql",
|
|
125
|
+
"sql": "-- 00000000000021_migrate_hero_blocks_to_sections.sql\n-- Migration to convert 'hero' block types to 'section' block types with {\"is_hero\": true} content.\n\n-- 1. Migrate active blocks\nUPDATE public.blocks\nSET\n block_type = 'section',\n content = COALESCE(content, '{}'::jsonb) || '{\"is_hero\": true}'::jsonb\nWHERE block_type = 'hero';\n\n-- 2. Migrate content drafts containing hero blocks\nUPDATE public.content_drafts\nSET blocks = (\n SELECT COALESCE(\n jsonb_agg(\n CASE\n WHEN elem.value->>'block_type' = 'hero' THEN\n jsonb_set(elem.value, '{block_type}', '\"section\"'::jsonb) ||\n jsonb_build_object('content', COALESCE(elem.value->'content', '{}'::jsonb) || '{\"is_hero\": true}'::jsonb)\n ELSE elem.value\n END\n ),\n '[]'::jsonb\n )\n FROM jsonb_array_elements(public.content_drafts.blocks) AS elem(value)\n)\nWHERE public.content_drafts.blocks @> '[{\"block_type\": \"hero\"}]';\n\n\n"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"version": "00000000000022",
|
|
129
|
+
"name": "00000000000022_seed_cortex_ai_guide_post.sql",
|
|
130
|
+
"sql": "-- 00000000000022_seed_cortex_ai_guide_post.sql\n-- Adds the Cortex AI guide post linked from the seeded home page CTA.\n\nBEGIN;\n\nDO $$\nDECLARE\n v_en_lang_id bigint;\n v_cortex_media_id uuid;\n v_cortex_post_id bigint;\nBEGIN\n SELECT id INTO v_en_lang_id\n FROM public.languages\n WHERE code = 'en'\n LIMIT 1;\n\n IF v_en_lang_id IS NULL THEN\n RAISE EXCEPTION 'English language not found.';\n END IF;\n\n INSERT INTO public.media AS seed_media (\n file_name,\n object_key,\n file_path,\n file_type,\n size_bytes,\n width,\n height,\n folder,\n description\n )\n VALUES (\n 'cortex-ai.webp',\n 'images/cortex-ai.webp',\n 'images/cortex-ai.webp',\n 'image/webp',\n 298588,\n 1024,\n 571,\n 'images',\n 'NextBlock Cortex AI editorial feature image'\n )\n ON CONFLICT (object_key) DO UPDATE\n SET\n file_name = COALESCE(seed_media.file_name, EXCLUDED.file_name),\n file_path = COALESCE(seed_media.file_path, EXCLUDED.file_path),\n file_type = COALESCE(seed_media.file_type, EXCLUDED.file_type),\n size_bytes = COALESCE(seed_media.size_bytes, EXCLUDED.size_bytes),\n width = COALESCE(seed_media.width, EXCLUDED.width),\n height = COALESCE(seed_media.height, EXCLUDED.height),\n folder = COALESCE(seed_media.folder, EXCLUDED.folder),\n description = COALESCE(seed_media.description, EXCLUDED.description),\n updated_at = now()\n RETURNING id INTO v_cortex_media_id;\n\n INSERT INTO public.posts (\n language_id,\n title,\n slug,\n label,\n status,\n excerpt,\n subtitle,\n published_at,\n meta_title,\n meta_description,\n translation_group_id,\n feature_image_id\n )\n VALUES (\n v_en_lang_id,\n 'NextBlock Cortex AI Guide',\n 'nextblock-cortex-ai-guide',\n 'AI Copilot',\n 'published',\n 'A practical guide to Cortex AI, the block-aware assistant for model routing, BYOK controls, and faster editorial production inside NextBlock.',\n 'See how Cortex AI brings structured generation, provider choice, and safer content workflows directly into the NextBlock editor.',\n now(),\n 'NextBlock Cortex AI Guide',\n 'Learn how NextBlock Cortex AI helps teams generate, refine, and translate structured block content with model routing and BYOK controls.',\n gen_random_uuid(),\n v_cortex_media_id\n )\n ON CONFLICT (language_id, slug) DO NOTHING\n RETURNING id INTO v_cortex_post_id;\n\n IF v_cortex_post_id IS NULL THEN\n SELECT id INTO v_cortex_post_id\n FROM public.posts\n WHERE language_id = v_en_lang_id\n AND slug = 'nextblock-cortex-ai-guide'\n LIMIT 1;\n\n UPDATE public.posts\n SET\n feature_image_id = v_cortex_media_id,\n updated_at = now()\n WHERE id = v_cortex_post_id\n AND feature_image_id IS NULL;\n END IF;\n\n IF v_cortex_post_id IS NULL THEN\n RAISE EXCEPTION 'Unable to create or find Cortex AI guide post.';\n END IF;\n\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n SELECT\n v_cortex_post_id,\n v_en_lang_id,\n 'text',\n jsonb_build_object('html_content', $cortex_ai_html$\n<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>\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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Pick the right model</h3>\n <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>\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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Use your own keys</h3>\n <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>\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 <h3 class='mt-3 text-xl font-semibold text-slate-900 dark:text-white'>Generate valid blocks</h3>\n <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>\n </div>\n</div>\n\n<h2>Why Cortex AI Belongs Inside the Editor</h2>\n<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>\n<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>\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 structured prompt and get a section, article outline, or product narrative that already matches the site voice.</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, more technical, more polished, or locale-ready output without leaving the content screen.</p>\n </div>\n </div>\n</div>\n\n<h2>Model Routing and Cost Control</h2>\n<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>\n<ul>\n <li>Use fast, inexpensive models for drafts, variations, and rewrites</li>\n <li>Reserve stronger models for long-form strategy, technical copy, or difficult transformations</li>\n <li>Keep provider keys and routing defaults in the server-side configuration layer</li>\n <li>Make cost and quality decisions without changing the public rendering system</li>\n</ul>\n\n<h2>Block-Aware Generation</h2>\n<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>\n<p>Because the output is shaped around existing block contracts, the generated content feels native in the editor and predictable on the frontend.</p>\n\n<h2>Safer Team Workflows</h2>\n<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>\n\n<h2>A Practical Launch Flow</h2>\n<ol>\n <li>Draft the article, landing section, or product story from a focused prompt.</li>\n <li>Refine the result against the brand voice and target audience.</li>\n <li>Generate a localized version or shorter excerpt for cards and metadata.</li>\n <li>Review the content in the NextBlock editor, publish, and keep iterating through normal revisions.</li>\n</ol>\n\n<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>\n$cortex_ai_html$),\n 0\n WHERE NOT EXISTS (\n SELECT 1\n FROM public.blocks\n WHERE post_id = v_cortex_post_id\n );\nEND $$;\n\nCOMMIT;\n"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"version": "00000000000023",
|
|
134
|
+
"name": "00000000000023_setup_custom_block_definitions.sql",
|
|
135
|
+
"sql": "-- Custom block definition registry for data-rendered user blocks.\n\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb)\nRETURNS boolean\nLANGUAGE sql\nIMMUTABLE\nSET search_path = ''\nAS $$\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)\nRETURNS boolean\nLANGUAGE sql\nIMMUTABLE\nSET search_path = ''\nAS $$\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 PRIMARY KEY DEFAULT gen_random_uuid(),\n slug text NOT NULL UNIQUE CHECK (slug ~ '^[a-z][a-z0-9-]*$'),\n name text NOT NULL CHECK (length(trim(name)) > 0),\n description text NOT NULL DEFAULT '',\n fields jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (public.is_valid_custom_block_fields(fields)),\n layout_schema jsonb NOT NULL CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),\n is_original boolean NOT NULL DEFAULT true\n);\n\nCOMMENT ON TABLE public.custom_block_definitions IS\n 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';\nCOMMENT ON COLUMN public.custom_block_definitions.fields IS\n 'Strict JSONB field declarations for data-rendered custom blocks.';\nCOMMENT ON COLUMN public.custom_block_definitions.layout_schema IS\n 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';\nCOMMENT ON COLUMN public.custom_block_definitions.is_original IS\n 'False when a definition was created by duplicating an existing registry row.';\n\nCREATE INDEX IF NOT EXISTS idx_custom_block_definitions_is_original\n ON public.custom_block_definitions (is_original);\n\nCREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid)\nRETURNS public.custom_block_definitions\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public\nAS $$\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\nALTER TABLE public.custom_block_definitions ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT ON public.custom_block_definitions TO anon, authenticated, service_role;\nGRANT INSERT, UPDATE, DELETE ON public.custom_block_definitions TO authenticated;\nGRANT ALL ON public.custom_block_definitions TO service_role;\n\nDROP POLICY IF EXISTS custom_block_definitions_public_read_policy\n ON public.custom_block_definitions;\n\nCREATE POLICY custom_block_definitions_public_read_policy\n ON public.custom_block_definitions\n FOR SELECT\n TO public\n USING (true);\n\nDROP POLICY IF EXISTS custom_block_definitions_insert_policy\n ON public.custom_block_definitions;\n\nCREATE POLICY custom_block_definitions_insert_policy\n ON public.custom_block_definitions\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS custom_block_definitions_update_policy\n ON public.custom_block_definitions;\n\nCREATE POLICY custom_block_definitions_update_policy\n ON public.custom_block_definitions\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n WITH CHECK ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS custom_block_definitions_delete_policy\n ON public.custom_block_definitions;\n\nCREATE POLICY custom_block_definitions_delete_policy\n ON public.custom_block_definitions\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'));\n\nDROP POLICY IF EXISTS custom_block_definitions_service_role_policy\n ON public.custom_block_definitions;\n\nCREATE POLICY custom_block_definitions_service_role_policy\n ON public.custom_block_definitions\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nREVOKE ALL ON FUNCTION public.duplicate_block_definition(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.duplicate_block_definition(uuid) TO authenticated, service_role;\n"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"version": "00000000000024",
|
|
139
|
+
"name": "00000000000024_setup_ucp_cart_sessions.sql",
|
|
140
|
+
"sql": "-- 00000000000019_setup_ucp_cart_sessions.sql\n-- Persist Universal Commerce Protocol cart sessions for agentic cart handoff.\n\nCREATE TABLE IF NOT EXISTS public.ucp_cart_sessions (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n status text NOT NULL DEFAULT 'active'\n CHECK (status IN ('active', 'cancelled', 'completed')),\n currency text NOT NULL DEFAULT 'USD',\n locale text,\n buyer_identity jsonb NOT NULL DEFAULT '{}'::jsonb,\n context jsonb NOT NULL DEFAULT '{}'::jsonb,\n signals jsonb NOT NULL DEFAULT '{}'::jsonb,\n attribution jsonb NOT NULL DEFAULT '{}'::jsonb,\n line_items jsonb NOT NULL DEFAULT '[]'::jsonb,\n totals jsonb NOT NULL DEFAULT '[]'::jsonb,\n checkout_url text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n expires_at timestamptz NOT NULL DEFAULT (now() + interval '7 days'),\n CONSTRAINT ucp_cart_sessions_buyer_identity_object\n CHECK (jsonb_typeof(buyer_identity) = 'object'),\n CONSTRAINT ucp_cart_sessions_context_object\n CHECK (jsonb_typeof(context) = 'object'),\n CONSTRAINT ucp_cart_sessions_signals_object\n CHECK (jsonb_typeof(signals) = 'object'),\n CONSTRAINT ucp_cart_sessions_attribution_object\n CHECK (jsonb_typeof(attribution) = 'object'),\n CONSTRAINT ucp_cart_sessions_line_items_array\n CHECK (jsonb_typeof(line_items) = 'array'),\n CONSTRAINT ucp_cart_sessions_totals_array\n CHECK (jsonb_typeof(totals) = 'array'),\n CONSTRAINT ucp_cart_sessions_metadata_object\n CHECK (jsonb_typeof(metadata) = 'object')\n);\n\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_status_expires_at\n ON public.ucp_cart_sessions (status, expires_at);\n\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_created_at\n ON public.ucp_cart_sessions (created_at DESC);\n\nCREATE OR REPLACE FUNCTION public.handle_ucp_cart_sessions_update()\nRETURNS trigger AS $$\nBEGIN\n NEW.updated_at = now();\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nDROP TRIGGER IF EXISTS trg_handle_ucp_cart_sessions_update\n ON public.ucp_cart_sessions;\n\nCREATE TRIGGER trg_handle_ucp_cart_sessions_update\n BEFORE UPDATE ON public.ucp_cart_sessions\n FOR EACH ROW\n EXECUTE FUNCTION public.handle_ucp_cart_sessions_update();\n\nALTER TABLE public.ucp_cart_sessions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS ucp_cart_sessions_service_role_policy\n ON public.ucp_cart_sessions;\n\nCREATE POLICY ucp_cart_sessions_service_role_policy\n ON public.ucp_cart_sessions\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.ucp_cart_sessions TO service_role;\nGRANT EXECUTE ON FUNCTION public.handle_ucp_cart_sessions_update() TO service_role;\n"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"version": "00000000000025",
|
|
144
|
+
"name": "00000000000025_add_sale_schedule_columns.sql",
|
|
145
|
+
"sql": "-- 00000000000025_add_sale_schedule_columns.sql\n-- Scheduled pricing for products and variants:\n-- * sale_start_at / sale_end_at gate the existing sale_price / sale_prices into a time window.\n-- * scheduled_price / scheduled_prices / scheduled_price_at hold a pending permanent\n-- regular-price change that takes effect once scheduled_price_at has passed.\n-- Enforcement is read-time (see resolveEffectivePriceForCurrency); no cron is required.\n-- Also adds a mapping table for auto-generated, time-bounded Freemius sale coupons.\n\n-- ---------------------------------------------------------------------------\n-- 1. Schedule columns on products\n-- ---------------------------------------------------------------------------\nALTER TABLE public.products\n ADD COLUMN IF NOT EXISTS sale_start_at timestamptz,\n ADD COLUMN IF NOT EXISTS sale_end_at timestamptz,\n ADD COLUMN IF NOT EXISTS scheduled_price integer,\n ADD COLUMN IF NOT EXISTS scheduled_prices jsonb,\n ADD COLUMN IF NOT EXISTS scheduled_price_at timestamptz;\n\nALTER TABLE public.products\n ADD CONSTRAINT products_sale_window_valid\n CHECK (sale_start_at IS NULL OR sale_end_at IS NULL OR sale_start_at < sale_end_at);\n\nCOMMENT ON COLUMN public.products.sale_start_at IS\n 'Inclusive start of the scheduled sale window (UTC). NULL means no lower bound.';\nCOMMENT ON COLUMN public.products.sale_end_at IS\n 'Exclusive end of the scheduled sale window (UTC). NULL means no upper bound. Both NULL = always-on sale.';\nCOMMENT ON COLUMN public.products.scheduled_price IS\n 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\nCOMMENT ON COLUMN public.products.scheduled_prices IS\n 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\nCOMMENT ON COLUMN public.products.scheduled_price_at IS\n 'Effective timestamp (UTC) for the pending regular-price change.';\n\nCREATE INDEX IF NOT EXISTS products_sale_window_idx\n ON public.products (sale_start_at, sale_end_at)\n WHERE sale_start_at IS NOT NULL OR sale_end_at IS NOT NULL;\n\nCREATE INDEX IF NOT EXISTS products_scheduled_price_idx\n ON public.products (scheduled_price_at)\n WHERE scheduled_price_at IS NOT NULL;\n\n-- ---------------------------------------------------------------------------\n-- 2. Schedule columns on product_variants\n-- ---------------------------------------------------------------------------\nALTER TABLE public.product_variants\n ADD COLUMN IF NOT EXISTS sale_start_at timestamptz,\n ADD COLUMN IF NOT EXISTS sale_end_at timestamptz,\n ADD COLUMN IF NOT EXISTS scheduled_price integer,\n ADD COLUMN IF NOT EXISTS scheduled_prices jsonb,\n ADD COLUMN IF NOT EXISTS scheduled_price_at timestamptz;\n\nALTER TABLE public.product_variants\n ADD CONSTRAINT product_variants_sale_window_valid\n CHECK (sale_start_at IS NULL OR sale_end_at IS NULL OR sale_start_at < sale_end_at);\n\nCOMMENT ON COLUMN public.product_variants.sale_start_at IS\n 'Inclusive start of the scheduled sale window (UTC) for this variant. NULL means no lower bound.';\nCOMMENT ON COLUMN public.product_variants.sale_end_at IS\n 'Exclusive end of the scheduled sale window (UTC) for this variant. NULL means no upper bound.';\nCOMMENT ON COLUMN public.product_variants.scheduled_price IS\n 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\nCOMMENT ON COLUMN public.product_variants.scheduled_prices IS\n 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\nCOMMENT ON COLUMN public.product_variants.scheduled_price_at IS\n 'Effective timestamp (UTC) for the pending regular-price change.';\n\nCREATE INDEX IF NOT EXISTS product_variants_sale_window_idx\n ON public.product_variants (sale_start_at, sale_end_at)\n 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\n ON public.product_variants (scheduled_price_at)\n WHERE scheduled_price_at IS NOT NULL;\n\n-- ---------------------------------------------------------------------------\n-- 3. Redefine upsert_product_with_variants to persist the sale-window columns.\n-- The form write path goes exclusively through this RPC; the scheduled_price*\n-- columns are written by the bulk importer via direct service-role UPDATE and\n-- therefore are intentionally NOT part of this function.\n-- Body copied from migration 00000000000005 with sale_start_at / sale_end_at added.\n-- ---------------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb)\nRETURNS uuid\nLANGUAGE plpgsql\nSET search_path = public\nAS $function$\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$function$;\n\nGRANT EXECUTE ON FUNCTION public.upsert_product_with_variants(jsonb) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.upsert_product_with_variants(jsonb) TO service_role;\n\n-- ---------------------------------------------------------------------------\n-- 4. Mapping table for auto-generated, time-bounded Freemius sale coupons.\n-- One auto-sale coupon per product. Kept separate from the admin `coupons`\n-- table so generated sale coupons do not appear in the Coupons UI.\n-- ---------------------------------------------------------------------------\nCREATE TABLE public.product_freemius_sale_coupons (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\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 timestamptz,\n ends_at timestamptz,\n is_active boolean NOT NULL DEFAULT false,\n sync_status text NOT NULL DEFAULT 'pending',\n sync_error text,\n remote_payload jsonb,\n last_synced_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT product_freemius_sale_coupons_product_unique UNIQUE (product_id),\n CONSTRAINT product_freemius_sale_coupons_fm_product_not_blank\n CHECK (char_length(btrim(freemius_product_id)) > 0),\n CONSTRAINT product_freemius_sale_coupons_code_not_blank\n CHECK (char_length(btrim(freemius_coupon_code)) > 0),\n CONSTRAINT product_freemius_sale_coupons_discount_valid\n CHECK (discount_percent IS NULL OR (discount_percent > 0 AND discount_percent <= 100)),\n CONSTRAINT product_freemius_sale_coupons_window_valid\n CHECK (starts_at IS NULL OR ends_at IS NULL OR starts_at < ends_at),\n CONSTRAINT product_freemius_sale_coupons_sync_status_valid\n CHECK (sync_status IN ('pending', 'synced', 'failed', 'deleted'))\n);\n\nCOMMENT ON TABLE public.product_freemius_sale_coupons IS\n 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';\n\nCREATE INDEX idx_product_freemius_sale_coupons_freemius_product_id\n ON public.product_freemius_sale_coupons (freemius_product_id);\n\nCREATE OR REPLACE FUNCTION public.handle_product_freemius_sale_coupons_write()\nRETURNS trigger\nLANGUAGE plpgsql\nSET search_path = ''\nAS $$\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\nDROP TRIGGER IF EXISTS on_product_freemius_sale_coupons_write ON public.product_freemius_sale_coupons;\nCREATE TRIGGER on_product_freemius_sale_coupons_write\n BEFORE INSERT OR UPDATE ON public.product_freemius_sale_coupons\n FOR EACH ROW\n EXECUTE FUNCTION public.handle_product_freemius_sale_coupons_write();\n\nALTER TABLE public.product_freemius_sale_coupons ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON public.product_freemius_sale_coupons TO authenticated, service_role;\n\nCREATE POLICY product_freemius_sale_coupons_admin_policy\n ON public.product_freemius_sale_coupons\n FOR ALL\n TO authenticated\n USING (((SELECT public.is_admin()) IS TRUE))\n WITH CHECK (((SELECT public.is_admin()) IS TRUE));\n\nCREATE POLICY product_freemius_sale_coupons_service_role_policy\n ON public.product_freemius_sale_coupons\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"version": "00000000000026",
|
|
149
|
+
"name": "00000000000026_seed_on_sale_translation.sql",
|
|
150
|
+
"sql": "-- 00000000000026_seed_on_sale_translation.sql\n-- Storefront \"On Sale\" badge label (shown on product cards when a scheduled\n-- sale is currently active). Seeded across the supported storefront languages.\n\nINSERT INTO public.translations (key, translations)\nVALUES\n ('ecommerce.on_sale', '{\"en\": \"On Sale\", \"fr\": \"En solde\", \"es\": \"En oferta\"}'::jsonb)\nON CONFLICT (key) DO UPDATE\nSET translations = EXCLUDED.translations;\n"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"version": "00000000000027",
|
|
154
|
+
"name": "00000000000027_setup_privacy_and_mfa.sql",
|
|
155
|
+
"sql": "-- 00000000000027_setup_privacy_and_mfa.sql\n-- Canadian privacy compliance (Quebec Law 25 / CASL) + two-factor authentication.\n--\n-- Adds:\n-- * privacy_consent_logs - immutable audit trail of consent decisions (service-role writes only)\n-- * user_security_settings - per-user MFA configuration (totp | email)\n-- * user_trusted_devices - revocable \"remember this device\" records that gate the 2FA bypass\n-- * email_2fa_challenges - short-lived hashed 6-digit email codes (service-role only)\n-- Seeds:\n-- * privacy_settings / security_settings defaults into site_settings\n-- * /privacy-policy + /terms-of-service pages (EN + FR) as text blocks, with the\n-- Terms aligned to NextBlock's actual license (AGPL-3.0, see LICENSE.md)\n-- * the remember_this_device sign-in translation\n--\n-- Conventions mirror existing migrations: gen_random_uuid(), timestamptz DEFAULT now(),\n-- explicit per-table GRANTs, RLS with service_role FOR ALL and role-scoped authenticated policies.\n\n-- ---------------------------------------------------------------------------\n-- 1. privacy_consent_logs\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.privacy_consent_logs (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n consent_token text NOT NULL UNIQUE,\n categories jsonb NOT NULL DEFAULT '{\"necessary\": true, \"analytics\": false, \"marketing\": false}'::jsonb\n CHECK (jsonb_typeof(categories) = 'object'),\n ip_masked text,\n user_agent text,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.privacy_consent_logs IS\n 'Immutable audit log of visitor consent decisions for Quebec Law 25 / PIPEDA accountability.';\nCOMMENT ON COLUMN public.privacy_consent_logs.consent_token IS\n 'Opaque token also stored in the nb_consent_preference cookie to correlate a decision with its record.';\nCOMMENT ON COLUMN public.privacy_consent_logs.ip_masked IS\n 'Partially masked IP (e.g. 203.0.113.x) - never store a full address for an analytics/marketing consent log.';\n\nCREATE INDEX IF NOT EXISTS idx_privacy_consent_logs_created_at\n ON public.privacy_consent_logs (created_at DESC);\n\nALTER TABLE public.privacy_consent_logs ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT ON public.privacy_consent_logs TO authenticated;\nGRANT ALL ON public.privacy_consent_logs TO service_role;\n\nDROP POLICY IF EXISTS privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs;\nCREATE POLICY privacy_consent_logs_admin_read_policy\n ON public.privacy_consent_logs\n FOR SELECT\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nDROP POLICY IF EXISTS privacy_consent_logs_service_role_policy ON public.privacy_consent_logs;\nCREATE POLICY privacy_consent_logs_service_role_policy\n ON public.privacy_consent_logs\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 2. user_security_settings\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.user_security_settings (\n user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,\n mfa_enabled boolean NOT NULL DEFAULT false,\n mfa_type text CHECK (mfa_type IN ('totp', 'email')),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.user_security_settings IS\n 'Per-user multi-factor configuration. mfa_type is NULL until a factor is enrolled.';\n\nALTER TABLE public.user_security_settings ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, INSERT, UPDATE ON public.user_security_settings TO authenticated;\nGRANT ALL ON public.user_security_settings TO service_role;\n\nDROP POLICY IF EXISTS user_security_settings_select_own_policy ON public.user_security_settings;\nCREATE POLICY user_security_settings_select_own_policy\n ON public.user_security_settings\n FOR SELECT\n TO authenticated\n USING ((SELECT auth.uid()) = user_id);\n\nDROP POLICY IF EXISTS user_security_settings_insert_own_policy ON public.user_security_settings;\nCREATE POLICY user_security_settings_insert_own_policy\n ON public.user_security_settings\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT auth.uid()) = user_id);\n\nDROP POLICY IF EXISTS user_security_settings_update_own_policy ON public.user_security_settings;\nCREATE POLICY user_security_settings_update_own_policy\n ON public.user_security_settings\n FOR UPDATE\n TO authenticated\n USING ((SELECT auth.uid()) = user_id)\n WITH CHECK ((SELECT auth.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\n ON public.user_security_settings\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 3. user_trusted_devices (\"Remember this device\")\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.user_trusted_devices (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,\n device_hash text NOT NULL UNIQUE,\n browser_metadata text,\n expires_at timestamptz NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.user_trusted_devices IS\n '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.';\nCOMMENT ON COLUMN public.user_trusted_devices.device_hash IS\n 'SHA-256 of the raw token held in the nb_trusted_device cookie. The raw token is never stored.';\n\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_user_id\n ON public.user_trusted_devices (user_id);\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_expires_at\n ON public.user_trusted_devices (expires_at);\n\nALTER TABLE public.user_trusted_devices ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, DELETE ON public.user_trusted_devices TO authenticated;\nGRANT ALL ON public.user_trusted_devices TO service_role;\n\n-- Users can list their own devices (Security panel) and revoke them, but creation\n-- happens server-side via the service role so the raw token is hashed before storage.\nDROP POLICY IF EXISTS user_trusted_devices_select_own_policy ON public.user_trusted_devices;\nCREATE POLICY user_trusted_devices_select_own_policy\n ON public.user_trusted_devices\n FOR SELECT\n TO authenticated\n USING ((SELECT auth.uid()) = user_id);\n\nDROP POLICY IF EXISTS user_trusted_devices_delete_own_policy ON public.user_trusted_devices;\nCREATE POLICY user_trusted_devices_delete_own_policy\n ON public.user_trusted_devices\n FOR DELETE\n TO authenticated\n USING ((SELECT auth.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\n ON public.user_trusted_devices\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 4. email_2fa_challenges (service-role only - codes are sensitive)\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.email_2fa_challenges (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,\n token_hash text NOT NULL,\n expires_at timestamptz NOT NULL,\n consumed_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE public.email_2fa_challenges IS\n 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';\n\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_user_id\n ON public.email_2fa_challenges (user_id);\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_expires_at\n ON public.email_2fa_challenges (expires_at);\n\nALTER TABLE public.email_2fa_challenges ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON public.email_2fa_challenges TO service_role;\n\n-- No authenticated/anon grants and no permissive authenticated policy: all access\n-- flows through service-role server actions.\nDROP POLICY IF EXISTS email_2fa_challenges_service_role_policy ON public.email_2fa_challenges;\nCREATE POLICY email_2fa_challenges_service_role_policy\n ON public.email_2fa_challenges\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 5. Default site_settings (do not clobber existing admin edits)\n-- ---------------------------------------------------------------------------\nINSERT INTO public.site_settings (key, value) VALUES\n ('privacy_settings', '{\n \"banner_enabled\": true,\n \"gtm_id\": \"\",\n \"ga_measurement_id\": \"\",\n \"custom_scripts\": \"\",\n \"corporate\": { \"legal_name\": \"\", \"address\": \"\", \"support_email\": \"\" }\n }'::jsonb),\n ('security_settings', '{\n \"trusted_device_days\": 30,\n \"enforce_staff_2fa\": false\n }'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\n-- ---------------------------------------------------------------------------\n-- 6. Seed /privacy-policy and /terms-of-service pages (EN + FR) as text blocks.\n-- Pages are metadata-only; the actual copy lives in blocks.content.html_content.\n-- The Terms reflect NextBlock's real license (AGPL-3.0, per LICENSE.md).\n-- ---------------------------------------------------------------------------\nDO $seed$\nDECLARE\n v_en bigint;\n v_fr bigint;\n v_privacy_group uuid;\n v_terms_group uuid;\n v_page_id bigint;\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 IF v_en IS NULL THEN\n RAISE NOTICE 'Default language \"en\" not found; skipping privacy/terms page seed.';\n RETURN;\n END IF;\n\n -- Reuse an existing translation_group_id if these pages were seeded before.\n SELECT translation_group_id INTO v_privacy_group\n FROM public.pages WHERE slug = 'privacy-policy' AND language_id = v_en LIMIT 1;\n IF v_privacy_group IS NULL THEN v_privacy_group := gen_random_uuid(); END IF;\n\n SELECT translation_group_id INTO v_terms_group\n FROM public.pages WHERE slug = 'terms-of-service' AND language_id = v_en LIMIT 1;\n IF v_terms_group IS NULL THEN v_terms_group := gen_random_uuid(); END IF;\n\n -- ----- Privacy Policy (EN) -----\n INSERT INTO public.pages (language_id, title, slug, status, meta_title, meta_description, translation_group_id)\n VALUES (\n v_en, 'Privacy Policy', 'privacy-policy', 'published',\n 'Privacy Policy',\n 'How we collect, use, disclose, and protect your personal information under Quebec Law 25, PIPEDA, and CASL.',\n v_privacy_group\n )\n ON CONFLICT (language_id, slug) DO UPDATE\n SET title = EXCLUDED.title, status = EXCLUDED.status,\n meta_title = EXCLUDED.meta_title, meta_description = EXCLUDED.meta_description\n RETURNING id INTO v_page_id;\n\n DELETE FROM public.blocks WHERE page_id = v_page_id;\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_page_id, v_en, 'text', jsonb_build_object('html_content', $html$\n<h1>Privacy Policy</h1>\n<p><em>Last updated: June 4, 2026</em></p>\n<p>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).</p>\n\n<h2>1. Person responsible for personal information</h2>\n<p>Our Privacy Officer is responsible for our compliance with applicable privacy laws. You may reach them at <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>.</p>\n\n<h2>2. What we collect</h2>\n<ul>\n <li><strong>Account information</strong> — name, email address, and credentials when you register.</li>\n <li><strong>Usage and device data</strong> — collected only with your consent through analytics technologies.</li>\n <li><strong>Communications</strong> — messages you send us and your marketing preferences.</li>\n</ul>\n\n<h2>3. Why we collect it and your consent</h2>\n<p>We collect personal information for clearly identified purposes: to provide and secure our services, to communicate with you, and — only with your express, opt-in consent — 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>\n\n<h2>4. Cookies and tracking technologies</h2>\n<p>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.</p>\n\n<h2>5. Disclosure and sharing</h2>\n<p>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.</p>\n\n<h2>6. Retention</h2>\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>\n\n<h2>7. Your rights</h2>\n<p>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 <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>.</p>\n\n<h2>8. Commercial electronic messages (CASL)</h2>\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>\n\n<h2>9. Safeguards</h2>\n<p>We use appropriate physical, organizational, and technological measures — including encryption in transit and access controls — to protect personal information against loss, theft, and unauthorized access.</p>\n\n<h2>10. Open-source software</h2>\n<p>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.</p>\n\n<h2>11. Changes to this policy</h2>\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>\n\n<h2>12. Contact us</h2>\n<p>Questions or complaints? Contact NextBlock™ CMS at <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>. You may also contact the Commission d'accès à l'information du Québec or the Office of the Privacy Commissioner of Canada.</p>\n$html$), 0);\n\n -- ----- Privacy Policy (FR) -----\n IF v_fr IS NOT NULL THEN\n INSERT INTO public.pages (language_id, title, slug, status, meta_title, meta_description, translation_group_id)\n VALUES (\n v_fr, 'Politique de confidentialité', 'politique-de-confidentialite', 'published',\n 'Politique de confidentialité',\n 'Comment nous recueillons, utilisons, communiquons et protégeons vos renseignements personnels en vertu de la Loi 25, de la LPRPDE et de la LCAP.',\n v_privacy_group\n )\n ON CONFLICT (language_id, slug) DO UPDATE\n SET title = EXCLUDED.title, status = EXCLUDED.status,\n meta_title = EXCLUDED.meta_title, meta_description = EXCLUDED.meta_description\n RETURNING id INTO v_page_id;\n\n DELETE FROM public.blocks WHERE page_id = v_page_id;\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_page_id, v_fr, 'text', jsonb_build_object('html_content', $html$\n<h1>Politique de confidentialité</h1>\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\n<p>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).</p>\n\n<h2>1. Responsable de la protection des renseignements personnels</h2>\n<p>Notre responsable de la protection des renseignements personnels veille au respect des lois applicables. Vous pouvez le joindre à <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>.</p>\n\n<h2>2. Renseignements que nous recueillons</h2>\n<ul>\n <li><strong>Renseignements de compte</strong> — nom, adresse courriel et identifiants lors de l'inscription.</li>\n <li><strong>Données d'utilisation et d'appareil</strong> — recueillies uniquement avec votre consentement au moyen de technologies d'analyse.</li>\n <li><strong>Communications</strong> — les messages que vous nous envoyez et vos préférences marketing.</li>\n</ul>\n\n<h2>3. Finalités et consentement</h2>\n<p>Nous recueillons des renseignements personnels à des fins clairement déterminées : fournir et sécuriser nos services, communiquer avec vous et — uniquement avec votre consentement exprès — à 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>\n\n<h2>4. Témoins et technologies de suivi</h2>\n<p>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.</p>\n\n<h2>5. Communication à des tiers</h2>\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>\n\n<h2>6. Conservation</h2>\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>\n\n<h2>7. Vos droits</h2>\n<p>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 à <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>.</p>\n\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\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>\n\n<h2>9. Mesures de sécurité</h2>\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées — dont le chiffrement en transit et le contrôle des accès — pour protéger vos renseignements.</p>\n\n<h2>10. Logiciel libre</h2>\n<p>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.</p>\n\n<h2>11. Modifications</h2>\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>\n\n<h2>12. Nous joindre</h2>\n<p>Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>. Vous pouvez aussi vous adresser à la Commission d'accès à l'information du Québec.</p>\n$html$), 0);\n END IF;\n\n -- ----- Terms of Service (EN) -- aligned with the AGPL-3.0 (LICENSE.md) -----\n INSERT INTO public.pages (language_id, title, slug, status, meta_title, meta_description, translation_group_id)\n VALUES (\n v_en, 'Terms of Service', 'terms-of-service', 'published',\n 'Terms of Service',\n 'The terms governing your use of NextBlock™ CMS, free and open-source software licensed under the AGPL-3.0.',\n v_terms_group\n )\n ON CONFLICT (language_id, slug) DO UPDATE\n SET title = EXCLUDED.title, status = EXCLUDED.status,\n meta_title = EXCLUDED.meta_title, meta_description = EXCLUDED.meta_description\n RETURNING id INTO v_page_id;\n\n DELETE FROM public.blocks WHERE page_id = v_page_id;\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_page_id, v_en, 'text', jsonb_build_object('html_content', $html$\n<h1>Terms of Service</h1>\n<p><em>Last updated: June 4, 2026</em></p>\n\n<h2>1. Acceptance of terms</h2>\n<p>By accessing or using NextBlock™ CMS and the services we provide (the \"Services\"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.</p>\n\n<h2>2. Free and open-source software</h2>\n<p>NextBlock™ CMS is free, open-source software licensed under the <strong>GNU Affero General Public License, version 3 (AGPL-3.0)</strong> or, at your option, any later version. You are free to run, study, share, and modify the software under the terms of that license. A copy of the license is distributed with the software and is also available at <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>\n<p>Copyright © 2025 NextBlock™ CMS.</p>\n\n<h2>3. Source code availability</h2>\n<p>In accordance with section 13 of the AGPL-3.0, if you run a modified version of NextBlock™ CMS and make it available to users over a network, you must prominently offer those users access to the Corresponding Source of your modified version, free of charge, through a standard or customary means of facilitating copying of software.</p>\n\n<h2>4. Trademarks</h2>\n<p>The AGPL-3.0 grants broad rights to the software's source code but does <strong>not</strong> grant any rights to our trade names, trademarks, or service marks. \"NextBlock™\", the NextBlock™ CMS name, and associated logos remain our property and may not be used in a way that suggests endorsement or affiliation without our prior written permission.</p>\n\n<h2>5. Accounts and acceptable use</h2>\n<p>If you create an account, you are responsible for safeguarding your credentials and for all activity under your account, and you agree to notify us promptly of any unauthorized use. You agree not to misuse the Services, including by attempting to disrupt them, access them without authorization, or use them for unlawful purposes.</p>\n\n<h2>6. No warranty</h2>\n<p>As stated in section 15 of the AGPL-3.0, the software is provided \"as is\", without warranty of any kind, either expressed or implied, including, without limitation, the implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of the software is with you.</p>\n\n<h2>7. Limitation of liability</h2>\n<p>As stated in section 16 of the AGPL-3.0, and to the fullest extent permitted by applicable law, in no event will any copyright holder, or any other party who modifies or conveys the software, be liable to you for damages, including any general, special, incidental, or consequential damages arising out of the use or inability to use the software.</p>\n\n<h2>8. Governing law</h2>\n<p>These Terms are governed by the laws of the Province of Quebec and the federal laws of Canada applicable therein, without regard to conflict-of-law principles. Nothing in these Terms limits any mandatory consumer-protection rights you may have under those laws.</p>\n\n<h2>9. Changes</h2>\n<p>We may revise these Terms from time to time. Material changes will be communicated through the Services, and continued use of the Services after changes take effect constitutes acceptance of the revised Terms.</p>\n\n<h2>10. Contact</h2>\n<p>Questions about these Terms? Contact NextBlock™ CMS at <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>.</p>\n$html$), 0);\n\n -- ----- Terms of Service (FR) -- aligned with the AGPL-3.0 (LICENSE.md) -----\n IF v_fr IS NOT NULL THEN\n INSERT INTO public.pages (language_id, title, slug, status, meta_title, meta_description, translation_group_id)\n VALUES (\n v_fr, 'Conditions d''utilisation', 'conditions-utilisation', 'published',\n 'Conditions d''utilisation',\n 'Les conditions régissant votre utilisation de NextBlock™ CMS, un logiciel libre sous licence AGPL-3.0.',\n v_terms_group\n )\n ON CONFLICT (language_id, slug) DO UPDATE\n SET title = EXCLUDED.title, status = EXCLUDED.status,\n meta_title = EXCLUDED.meta_title, meta_description = EXCLUDED.meta_description\n RETURNING id INTO v_page_id;\n\n DELETE FROM public.blocks WHERE page_id = v_page_id;\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_page_id, v_fr, 'text', jsonb_build_object('html_content', $html$\n<h1>Conditions d'utilisation</h1>\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\n\n<h2>1. Acceptation des conditions</h2>\n<p>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.</p>\n\n<h2>2. Logiciel libre et à code source ouvert</h2>\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>\n<p>Droit d'auteur © 2025 NextBlock™ CMS.</p>\n\n<h2>3. Disponibilité du code source</h2>\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>\n\n<h2>4. Marques de commerce</h2>\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>\n\n<h2>5. Comptes et utilisation acceptable</h2>\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>\n\n<h2>6. Absence de garantie</h2>\n<p>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.</p>\n\n<h2>7. Limitation de responsabilité</h2>\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>\n\n<h2>8. Droit applicable</h2>\n<p>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.</p>\n\n<h2>9. Modifications</h2>\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>\n\n<h2>10. Nous joindre</h2>\n<p>Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\"mailto:privacy@nextblock.dev\">privacy@nextblock.dev</a>.</p>\n$html$), 0);\n END IF;\n\n RAISE NOTICE 'Seeded privacy-policy and terms-of-service pages.';\nEND;\n$seed$;\n\n-- ---------------------------------------------------------------------------\n-- 7. UI translations introduced with this feature\n-- ---------------------------------------------------------------------------\nINSERT INTO public.translations (key, translations) VALUES\n ('remember_this_device', '{\"en\": \"Remember this device\", \"fr\": \"Se souvenir de cet appareil\"}'::jsonb)\nON CONFLICT (key) DO UPDATE\nSET translations = EXCLUDED.translations;\n"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"version": "00000000000028",
|
|
159
|
+
"name": "00000000000028_clear_advisor_warnings.sql",
|
|
160
|
+
"sql": "-- 00000000000028_clear_advisor_warnings.sql\n-- Resolve Supabase Advisor (database linter) warnings without changing application\n-- behaviour. Every statement is idempotent and safe to re-run.\n--\n-- 1. 0011 function_search_path_mutable\n-- public.handle_ucp_cart_sessions_update() had no pinned search_path.\n-- 2. 0029 authenticated_security_definer_function_executable\n-- public.duplicate_block_definition(uuid) ran as SECURITY DEFINER and was\n-- callable by every signed-in user. RLS + per-action grants already enforce\n-- ADMIN/WRITER, so it is switched to SECURITY INVOKER (it also keeps its own\n-- explicit role check). ADMIN/WRITER behaviour is unchanged.\n-- 3/4. 0006 multiple_permissive_policies\n-- public.categories and public.product_categories each had a FOR ALL\n-- \"Admin can manage ...\" policy that overlapped the \"Public can view ...\"\n-- SELECT policy for the authenticated role. The FOR ALL policy is split into\n-- INSERT/UPDATE/DELETE so a SELECT is evaluated against a single permissive\n-- policy while admin write access is preserved.\n--\n-- (The leaked-password and MFA-options advisor items are Auth dashboard settings,\n-- not schema, and are intentionally not addressed here.)\n\n-- ---------------------------------------------------------------------------\n-- 1. Pin search_path on the UCP cart-session updated_at trigger function.\n-- The body only calls now() (pg_catalog, always implicitly searched), so an\n-- empty search_path is safe and matches the convention already used by\n-- public.is_valid_custom_block_fields (migration 00000000000023).\n-- ---------------------------------------------------------------------------\nALTER FUNCTION public.handle_ucp_cart_sessions_update() SET search_path = '';\n\n-- ---------------------------------------------------------------------------\n-- 2. duplicate_block_definition: SECURITY DEFINER -> SECURITY INVOKER.\n-- The function still raises 42501 for non-ADMIN/WRITER callers, and the\n-- underlying SELECT/INSERT are already gated by custom_block_definitions RLS\n-- plus the authenticated SELECT/INSERT grants, so the function no longer needs\n-- to run with the definer's elevated privileges.\n-- ---------------------------------------------------------------------------\nALTER FUNCTION public.duplicate_block_definition(uuid) SECURITY INVOKER;\n\n-- ---------------------------------------------------------------------------\n-- 3. categories: split the FOR ALL admin policy so SELECT has one permissive\n-- policy (the public-read policy). Admin reads still flow through that policy\n-- (USING true); admin writes are preserved via the per-action policies below.\n-- ---------------------------------------------------------------------------\nDROP POLICY IF EXISTS \"Admin can manage categories\" ON public.categories;\n\nDROP POLICY IF EXISTS \"Admin can insert categories\" ON public.categories;\nCREATE POLICY \"Admin can insert categories\"\n ON public.categories\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.is_admin()) IS TRUE);\n\nDROP POLICY IF EXISTS \"Admin can update categories\" ON public.categories;\nCREATE POLICY \"Admin can update categories\"\n ON public.categories\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.is_admin()) IS TRUE)\n WITH CHECK ((SELECT public.is_admin()) IS TRUE);\n\nDROP POLICY IF EXISTS \"Admin can delete categories\" ON public.categories;\nCREATE POLICY \"Admin can delete categories\"\n ON public.categories\n FOR DELETE\n TO authenticated\n USING ((SELECT public.is_admin()) IS TRUE);\n\n-- ---------------------------------------------------------------------------\n-- 4. product_categories: same split as categories.\n-- ---------------------------------------------------------------------------\nDROP POLICY IF EXISTS \"Admin can manage product_categories\" ON public.product_categories;\n\nDROP POLICY IF EXISTS \"Admin can insert product_categories\" ON public.product_categories;\nCREATE POLICY \"Admin can insert product_categories\"\n ON public.product_categories\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.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\"\n ON public.product_categories\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.is_admin()) IS TRUE)\n WITH CHECK ((SELECT public.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\"\n ON public.product_categories\n FOR DELETE\n TO authenticated\n USING ((SELECT public.is_admin()) IS TRUE);\n"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"version": "00000000000029",
|
|
164
|
+
"name": "00000000000029_refresh_setup_article.sql",
|
|
165
|
+
"sql": "-- Refresh the seeded \"How to Setup NextBlock\" tutorial (EN + FR) so it matches the\n-- current installer flow:\n-- * `npm run setup` now requires Supabase + Cloudflare R2 + SMTP up front,\n-- * `npx nx serve nextblock` serves on http://localhost:4200, and\n-- * the first account to sign up automatically becomes the admin (email confirmation on).\n--\n-- Forward-only and idempotent: it replaces the single text block of the two setup-article\n-- posts seeded in 00000000000010_seed_content_scaffold.sql. Safe to re-run; a no-op if the\n-- posts do not exist.\n\nWITH target_posts AS (\n SELECT id, language_id, slug\n FROM public.posts\n WHERE slug IN ('how-to-setup-nextblock', 'comment-configurer-nextblock')\n),\npurged AS (\n DELETE FROM public.blocks\n WHERE post_id IN (SELECT id FROM target_posts)\n)\nINSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n\n-- EN: How to Setup NextBlock\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>There are two strong ways to start with NextBlock: clone the full monorepo if you want the whole platform, or scaffold a standalone app if you want to ship quickly. Both paths land you on the same editorial model, design system, and CMS foundation.</p>\n\n<div class='rounded-[2rem] border border-blue-200 bg-blue-50/80 p-6 my-10 dark:border-blue-500/20 dark:bg-blue-500/10'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Choose your path</p>\n <div class='grid gap-6 md:grid-cols-2 mt-5'>\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Monorepo</h3>\n <p class='text-sm text-slate-600 dark:text-slate-300'>Best for contributors, plugin authors, and teams that want direct access to every app and shared package.</p>\n </div>\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>CLI starter</h3>\n <p class='text-sm text-slate-600 dark:text-slate-300'>Best for launching a production-ready Next.js project with NextBlock™ already wired in and easy to deploy.</p>\n </div>\n </div>\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/included.webp' alt='NextBlock™ platform artwork showing the CMS, blocks, and integrations that ship together' 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 still inherit the same block editor, CMS shell, and shared product language.</figcaption>\n</figure>\n\n<div class='rounded-[2rem] border border-amber-200 bg-amber-50/80 p-6 my-10 dark:border-amber-500/20 dark:bg-amber-500/10'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Before you start</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>The setup wizard asks for credentials from three services, so create them first:</p>\n <ul class='mt-4 list-disc pl-6 space-y-2 text-sm text-slate-700 dark:text-slate-200'>\n <li><strong>Supabase project</strong> – Reference ID (Project Settings > General), connection string (Connect > Direct connection > URI), the anon and service_role keys, and a Personal Access Token (Account > Access Tokens).</li>\n <li><strong>Cloudflare R2 bucket</strong> – create a bucket, enable its Public Development URL, and create an Account API token with Object Read & Write. Copy the Access Key ID and Secret Access Key (shown only once).</li>\n <li><strong>SMTP credentials</strong> – SMTP2GO works very well; required so Supabase can email the confirmation link your first admin needs to sign in.</li>\n </ul>\n</div>\n\n<h2>Path 1: Clone the Monorepo</h2>\n<p>This route is ideal when you want the full Nx workspace and every internal package available locally.</p>\n\n<div class='grid gap-6 md:grid-cols-2 my-8'>\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>You get</p>\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\n <li>The public site, CMS app, CLI source, and shared libraries</li>\n <li>Direct access to <code>libs/</code> for custom block and package work</li>\n <li>Workspace tools like <code>nx graph</code> for dependency visibility</li>\n </ul>\n </div>\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Good fit for</p>\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\n <li>Core contributors and maintainers</li>\n <li>Teams building custom modules or premium extensions</li>\n <li>Agencies that want end-to-end control over the platform</li>\n </ul>\n </div>\n</div>\n\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\ncd nextblock\nnpm install\nnpm run setup</code></pre>\n\n<p>The <code>npm run setup</code> wizard creates <code>.env.local</code>, collects your Supabase, Cloudflare R2, and SMTP details, generates local secrets (<code>CRON_SECRET</code>, <code>DRAFT_MODE_SECRET</code>, <code>REVALIDATE_SECRET_TOKEN</code>), links the Supabase CLI, and applies the full schema to your database with <code>npm run db:migrate:fresh</code>.</p>\n\n<p>Then start the app:</p>\n<pre><code>npx nx serve nextblock</code></pre>\n\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='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>First login</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>The dev server runs at <code>http://localhost:4200</code>. Open <code>/sign-up</code> and create your account – the first account to register automatically becomes the admin. Confirm your email (or confirm the user in Supabase > Authentication > Users), then sign in to reach the CMS at <code>/cms/dashboard</code>.</p>\n</div>\n\n<p>Useful monorepo commands:</p>\n<pre><code># Build every workspace package\nnpm run all-builds\n\n# Lint the main application\nnpm run nx:lint:nextblock\n\n# Regenerate database types\nnpm run db:types\n\n# Inspect workspace relationships\nnpx nx graph</code></pre>\n\n<h2>Path 2: Use the CLI Starter</h2>\n<p>If your goal is to launch quickly, the CLI gives you a standalone Next.js app with NextBlock™ already embedded.</p>\n\n<pre><code>npm create nextblock@latest my-site\ncd my-site</code></pre>\n\n<p>The CLI copies a production-ready template, rewrites workspace imports to published packages, and can run the same setup flow for you. Your result is a normal Next.js app with no Nx requirement, so <code>npm run dev</code> serves it on <code>http://localhost:3000</code>.</p>\n\n<p>Configure your environment in <code>.env.local</code>:</p>\n<pre><code>NEXT_PUBLIC_SUPABASE_URL=your-project-url\nNEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key\nSUPABASE_SERVICE_ROLE_KEY=your-service-role-key\nNEXT_PUBLIC_URL=http://localhost:3000</code></pre>\n\n<p>Push the schema and start developing:</p>\n<pre><code>npm run db:push\nnpm run dev</code></pre>\n\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='text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Tip</p>\n <p class='mb-0 text-sm text-slate-700 dark:text-slate-200'>The CLI path is the fastest way to evaluate NextBlock™ with your own content model before you decide whether you need the full workspace.</p>\n</div>\n\n<h2>Activating Premium Modules</h2>\n<p>For CLI-generated projects, the commerce package can be activated with a single command:</p>\n<pre><code>npx create-nextblock activate ecommerce</code></pre>\n<p>This injects wrappers for <code>/cms/orders</code>, <code>/cms/products</code>, <code>/checkout</code>, and the checkout API, all gated through <code>verifyPackageOnline()</code> so premium routes stay aligned with your license.</p>\n\n<h2>Deployment</h2>\n<p>NextBlock™ deploys like a standard Next.js app. Push to Vercel, Netlify, or any Node.js host, then make sure your server-side environment variables such as the Supabase service role, R2 credentials, SMTP, and <code>CRON_SECRET</code> are configured in that environment, and set <code>NEXT_PUBLIC_URL</code> to your production domain.</p>$$\n), 0 FROM target_posts tp WHERE tp.slug = 'how-to-setup-nextblock'\n\nUNION ALL\n\n-- FR: Comment configurer NextBlock\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>Il existe deux bonnes facons de lancer NextBlock™ : cloner le monorepo complet si vous voulez toute la plateforme, ou partir du CLI si vous voulez aller vite. Dans les deux cas, vous retrouvez le meme modele editorial, le meme shell CMS et la meme base produit.</p>\n\n<div class='rounded-[2rem] border border-blue-200 bg-blue-50/80 p-6 my-10 dark:border-blue-500/20 dark:bg-blue-500/10'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Choisissez votre chemin</p>\n <div class='grid gap-6 md:grid-cols-2 mt-5'>\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Monorepo</h3>\n <p class='text-sm text-slate-600 dark:text-slate-300'>Ideal pour les contributeurs, auteurs de plugins et equipes qui veulent travailler directement dans tous les packages partages.</p>\n </div>\n <div class='rounded-2xl border border-blue-100 bg-white p-5 dark:border-blue-500/10 dark:bg-slate-900/40'>\n <h3 class='mt-0 text-xl text-slate-900 dark:text-white'>Starter CLI</h3>\n <p class='text-sm text-slate-600 dark:text-slate-300'>Ideal pour demarrer une app Next.js prete a deployer avec NextBlock™ deja integre.</p>\n </div>\n </div>\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/included.webp' alt='Visuel NextBlock™ montrant le CMS, les blocs et les integrations qui arrivent ensemble' 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 heritez du meme editeur de blocs, du meme shell CMS et du meme langage produit.</figcaption>\n</figure>\n\n<div class='rounded-[2rem] border border-amber-200 bg-amber-50/80 p-6 my-10 dark:border-amber-500/20 dark:bg-amber-500/10'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Avant de commencer</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>L'assistant de configuration demande des identifiants de trois services, alors creez-les d'abord :</p>\n <ul class='mt-4 list-disc pl-6 space-y-2 text-sm text-slate-700 dark:text-slate-200'>\n <li><strong>Projet Supabase</strong> – Reference ID (Project Settings > General), chaine de connexion (Connect > Direct connection > URI), les cles anon et service_role, et un Personal Access Token (Account > Access Tokens).</li>\n <li><strong>Bucket Cloudflare R2</strong> – creez un bucket, activez son Public Development URL, et creez un Account API token avec Object Read & Write. Copiez l'Access Key ID et la Secret Access Key (affichee une seule fois).</li>\n <li><strong>Identifiants SMTP</strong> – SMTP2GO fonctionne tres bien ; requis pour que Supabase envoie le lien de confirmation dont votre premier admin a besoin pour se connecter.</li>\n </ul>\n</div>\n\n<h2>Chemin 1 : cloner le monorepo</h2>\n<p>Cette option est la meilleure si vous voulez tout le workspace Nx et chaque package interne disponible en local.</p>\n\n<div class='grid gap-6 md:grid-cols-2 my-8'>\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Vous obtenez</p>\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\n <li>Le site public, l'app CMS, le code du CLI et les librairies partagees</li>\n <li>Un acces direct a <code>libs/</code> pour les blocs et modules personnalises</li>\n <li>Les outils de workspace comme <code>nx graph</code> pour visualiser les dependances</li>\n </ul>\n </div>\n <div class='rounded-3xl border border-slate-200/80 bg-slate-50 p-6 dark:border-white/10 dark:bg-white/5'>\n <p class='text-xs font-semibold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400'>Bon choix pour</p>\n <ul class='mt-4 list-disc pl-5 space-y-2 text-sm'>\n <li>Les mainteneurs et contributeurs coeur</li>\n <li>Les equipes qui construisent des extensions sur mesure</li>\n <li>Les agences qui veulent un controle complet de la plateforme</li>\n </ul>\n </div>\n</div>\n\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\ncd nextblock\nnpm install\nnpm run setup</code></pre>\n\n<p>L'assistant <code>npm run setup</code> cree <code>.env.local</code>, collecte vos identifiants Supabase, Cloudflare R2 et SMTP, genere les secrets locaux (<code>CRON_SECRET</code>, <code>DRAFT_MODE_SECRET</code>, <code>REVALIDATE_SECRET_TOKEN</code>), lie le CLI Supabase et applique le schema complet a votre base avec <code>npm run db:migrate:fresh</code>.</p>\n\n<p>Puis lancez l'application :</p>\n<pre><code>npx nx serve nextblock</code></pre>\n\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='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Premiere connexion</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Le serveur de dev tourne sur <code>http://localhost:4200</code>. Ouvrez <code>/sign-up</code> et creez votre compte – le premier compte inscrit devient automatiquement l'administrateur. Confirmez votre email (ou confirmez l'utilisateur dans Supabase > Authentication > Users), puis connectez-vous pour acceder au CMS sur <code>/cms/dashboard</code>.</p>\n</div>\n\n<p>Commandes utiles dans le monorepo :</p>\n<pre><code># Build de tous les packages\nnpm run all-builds\n\n# Lint de l'application principale\nnpm run nx:lint:nextblock\n\n# Regenerer les types base de donnees\nnpm run db:types\n\n# Inspecter les relations du workspace\nnpx nx graph</code></pre>\n\n<h2>Chemin 2 : utiliser le starter CLI</h2>\n<p>Si votre but est d'aller vite, le CLI vous donne une app Next.js autonome avec NextBlock™ deja integre.</p>\n\n<pre><code>npm create nextblock@latest mon-site\ncd mon-site</code></pre>\n\n<p>Le CLI copie un template pret pour la production, remplace les imports workspace par les packages publies, et peut lancer la meme configuration initiale. Le resultat reste une app Next.js classique, sans dependance a Nx, donc <code>npm run dev</code> la sert sur <code>http://localhost:3000</code>.</p>\n\n<p>Configurez votre environnement dans <code>.env.local</code> :</p>\n<pre><code>NEXT_PUBLIC_SUPABASE_URL=your-project-url\nNEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key\nSUPABASE_SERVICE_ROLE_KEY=your-service-role-key\nNEXT_PUBLIC_URL=http://localhost:3000</code></pre>\n\n<p>Poussez le schema puis demarrez :</p>\n<pre><code>npm run db:push\nnpm run dev</code></pre>\n\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='text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Conseil</p>\n <p class='mb-0 text-sm text-slate-700 dark:text-slate-200'>Le chemin CLI est le moyen le plus rapide d'evaluer NextBlock™ avec votre propre modele de contenu avant de passer, si besoin, au workspace complet.</p>\n</div>\n\n<h2>Activer les modules premium</h2>\n<p>Pour un projet genere via le CLI, le package commerce peut etre active avec une seule commande :</p>\n<pre><code>npx create-nextblock activate ecommerce</code></pre>\n<p>Cette commande injecte les wrappers pour <code>/cms/orders</code>, <code>/cms/products</code>, <code>/checkout</code> et l'API checkout, le tout protege par <code>verifyPackageOnline()</code> afin de garder les routes premium alignees avec la licence.</p>\n\n<h2>Deploiement</h2>\n<p>NextBlock™ se deploie comme une app Next.js standard. Publiez sur Vercel, Netlify ou tout hebergeur Node.js, puis configurez les variables serveur comme la cle service role Supabase, les identifiants R2, le SMTP et <code>CRON_SECRET</code>, et definissez <code>NEXT_PUBLIC_URL</code> sur votre domaine de production.</p>$$\n), 0 FROM target_posts tp WHERE tp.slug = 'comment-configurer-nextblock';\n"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"version": "00000000000030",
|
|
169
|
+
"name": "00000000000030_setup_system_configuration.sql",
|
|
170
|
+
"sql": "-- 00000000000030_setup_system_configuration.sql\n-- First-Boot Setup Wizard: global system configuration.\n--\n-- Adds a dedicated, RLS-locked `system_configuration` table that holds settings the\n-- browser /setup wizard manages and that don't belong in the public key-value\n-- `site_settings` store. It is a singleton (exactly one row, id = 1).\n--\n-- Shape:\n-- auto_accept_signups boolean -- when true, new public sign-ups skip outbound email\n-- verification (the signup route uses a service-role\n-- admin.createUser({ email_confirm: true }) path).\n-- settings jsonb -- forward-compatible catch-all for future feature\n-- toggles ({} by default). Do NOT store true secrets\n-- here (Turnstile/AI secrets keep living in their\n-- existing site_settings sensitive keys).\n--\n-- Access is locked to the ADMIN role for normal clients (NextBlock has no separate\n-- \"super-admin\" tier — ADMIN is the top level). The service_role retains full access\n-- so the wizard can seed/read it before any admin exists.\n\nCREATE TABLE IF NOT EXISTS public.system_configuration (\n id integer PRIMARY KEY DEFAULT 1,\n auto_accept_signups boolean NOT NULL DEFAULT false,\n settings jsonb NOT NULL DEFAULT '{}'::jsonb,\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT system_configuration_singleton CHECK (id = 1)\n);\n\nCOMMENT ON TABLE public.system_configuration IS\n 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';\n\n-- Seed the single row so reads always find it.\nINSERT INTO public.system_configuration (id, auto_accept_signups, settings)\nVALUES (1, false, '{}'::jsonb)\nON CONFLICT (id) DO NOTHING;\n\nALTER TABLE public.system_configuration ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.system_configuration TO authenticated;\nGRANT ALL ON public.system_configuration TO service_role;\n\n-- ADMIN-only for every operation by authenticated clients.\nDROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;\nCREATE POLICY system_configuration_admin_select\n ON public.system_configuration\n FOR SELECT\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nDROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;\nCREATE POLICY system_configuration_admin_insert\n ON public.system_configuration\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nDROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;\nCREATE POLICY system_configuration_admin_update\n ON public.system_configuration\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN')\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nDROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;\nCREATE POLICY system_configuration_admin_delete\n ON public.system_configuration\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\n\n-- Service role bypasses the ADMIN checks (used by the wizard before an admin exists,\n-- and by the signup route to read auto_accept_signups as an anonymous visitor).\nDROP POLICY IF EXISTS system_configuration_service_role_all ON public.system_configuration;\nCREATE POLICY system_configuration_service_role_all\n ON public.system_configuration\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n"
|
|
171
|
+
}
|
|
172
|
+
];
|