create-nextblock 0.16.3 → 0.16.4
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/api/cron/reset-sandbox/sandboxResetSql.ts +176 -1
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -40
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +19 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +14 -2
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +3 -3
- package/templates/nextblock-template/lib/seo/page-document.test.ts +34 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +4 -463
- package/templates/nextblock-template/lib/seo/robots-txt.ts +5 -4
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +10 -0
- package/templates/nextblock-template/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nextblock",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
4
4
|
"description": "Scaffold a production-ready NextBlock CMS project — the open-source, full-stack AI-native CMS for Next.js 16, Supabase, and Tailwind CSS.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9853,6 +9853,179 @@ BEGIN
|
|
|
9853
9853
|
END $$;
|
|
9854
9854
|
|
|
9855
9855
|
|
|
9856
|
+
-- >>> FROM: 00000000000035_digital_products_seo_optimizations.sql <<<
|
|
9857
|
+
-- Migration: 00000000000035_digital_products_seo_optimizations.sql
|
|
9858
|
+
-- Purpose: Set meta_title and meta_description for the 2 real digital products (EN + FR)
|
|
9859
|
+
-- and optimize their block heading levels and copy readability to achieve 100/100 SEO scores.
|
|
9860
|
+
-- Safety: Append-only, forward-only, content-guarded updates.
|
|
9861
|
+
|
|
9862
|
+
|
|
9863
|
+
-- 1. Digital Products SEO Metadata (Titles & Descriptions)
|
|
9864
|
+
-- NextBlock Cortex AI - Cortex AI License (EN)
|
|
9865
|
+
UPDATE public.products
|
|
9866
|
+
SET meta_title = 'NextBlock Cortex AI License | Native AI Content Engine',
|
|
9867
|
+
meta_description = 'Add native AI copywriting, real-time block translation, and structure refactoring to your NextBlock editor with our BYOK Cortex AI license.',
|
|
9868
|
+
updated_at = NOW()
|
|
9869
|
+
WHERE id = 'd48a4bb4-5119-4576-9c1d-32c404dd84f8';
|
|
9870
|
+
|
|
9871
|
+
-- Licence NextBlock™ Cortex AI (FR)
|
|
9872
|
+
UPDATE public.products
|
|
9873
|
+
SET meta_title = 'Licence NextBlock Cortex AI | Moteur IA pour Éditeur',
|
|
9874
|
+
meta_description = 'Intégrez la rédaction assistée par IA, la traduction de blocs et l''optimisation de structure dans NextBlock grâce à la licence Cortex AI.',
|
|
9875
|
+
updated_at = NOW()
|
|
9876
|
+
WHERE id = '69848224-a6d6-43c9-9748-f41a8e8e424b';
|
|
9877
|
+
|
|
9878
|
+
-- NextBlock Commerce Pro - Commerce License (EN)
|
|
9879
|
+
UPDATE public.products
|
|
9880
|
+
SET meta_title = 'NextBlock Commerce Pro | Global Next.js Storefronts',
|
|
9881
|
+
meta_description = 'Launch high-performance digital and physical stores with headless Stripe checkout, Freemius licensing, and multi-currency pricing in NextBlock.',
|
|
9882
|
+
updated_at = NOW()
|
|
9883
|
+
WHERE id = 'ed86bd77-da61-4e6f-826c-956f9a55b7aa';
|
|
9884
|
+
|
|
9885
|
+
-- Licence NextBlock™ Commerce Pro (FR)
|
|
9886
|
+
UPDATE public.products
|
|
9887
|
+
SET meta_title = 'Licence NextBlock Commerce Pro | E-Commerce Next.js',
|
|
9888
|
+
meta_description = 'Créez des boutiques Next.js performantes avec paiement Stripe headless, licences logicielles Freemius et gestion multi-devises dans NextBlock.',
|
|
9889
|
+
updated_at = NOW()
|
|
9890
|
+
WHERE id = '5c9e1b99-eeed-4fe0-adfa-31e81004c2f9';
|
|
9891
|
+
|
|
9892
|
+
-- 2. Digital Products Block Content & Heading Optimizations
|
|
9893
|
+
-- Block ID 39
|
|
9894
|
+
UPDATE public.blocks
|
|
9895
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#064e3b","position":0},{"color":"#0f172a","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\">Moteur E-Commerce d''Entreprise</p><h2 class=\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\">Faites de Next.js une Boutique Mondiale.</h2><p class=\\"text-base text-slate-300 leading-relaxed\\">Commerce Pro est un moteur de boutique moderne conçu pour NextBlock. Il gère vos catalogues physiques et vos produits numériques avec rapidité. Tout tourne sur votre propre serveur avec un contrôle total de vos données.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\"><h3 class=\\"text-lg font-bold text-white mb-4\\">Pourquoi choisir Commerce Pro</h3><ul class=\\"space-y-3 text-sm text-slate-300\\"><li class=\\"flex items-start gap-3\\"><span class=\\"text-emerald-400 font-bold\\">✓</span><span><strong>Multi-Devises</strong> — Taux de change en direct, règles d''arrondis et détection automatique du pays de l''acheteur.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-emerald-400 font-bold\\">✓</span><span><strong>Taxes automatiques</strong> — Prise en charge native de Stripe Tax pour respecter les règles fiscales locales.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-emerald-400 font-bold\\">✓</span><span><strong>Catalogue hybride</strong> — Vendez des produits physiques avec gestion de stock et des licences logicielles.</span></li></ul></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9896
|
+
updated_at = NOW()
|
|
9897
|
+
WHERE id = 39;
|
|
9898
|
+
|
|
9899
|
+
-- Block ID 41
|
|
9900
|
+
UPDATE public.blocks
|
|
9901
|
+
SET content = '{"padding":{"top":"lg","bottom":"lg"},"background":{"type":"solid","color":"transparent"},"column_gap":"lg","column_blocks":[[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\">💳</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Checkout Stripe Headless</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Paiement direct et sécurisé pour un ou plusieurs articles. Les commandes se valident automatiquement grâce aux webhooks Stripe.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\">🔑</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Licences Numériques Freemius</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Distribuez vos logiciels, activez vos clés et gérez les périodes d''essai SaaS. Tout fonctionne directement avec Freemius sans souci.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\">🛍️</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Merchandising Visuel</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Concevez vos fiches produits, mettez en valeur vos promotions et créez vos rayons directement dans l''éditeur de blocs.</p></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":2,"desktop":3},"vertical_alignment":"stretch"}'::jsonb,
|
|
9902
|
+
updated_at = NOW()
|
|
9903
|
+
WHERE id = 41;
|
|
9904
|
+
|
|
9905
|
+
-- Block ID 42
|
|
9906
|
+
UPDATE public.blocks
|
|
9907
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#0f172a","position":0},{"color":"#064e3b","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\">Sous le capot</p><h3 class=\\"text-2xl font-bold text-white mb-4\\">Conçu pour la production à grande échelle</h3><p class=\\"text-sm text-slate-300 leading-relaxed mb-4\\">Commerce Pro a été pensé dès le début pour les boutiques actives. Chaque route API est mise en cache, et les requêtes SQL sont optimisées.</p><p class=\\"text-sm text-slate-300 leading-relaxed\\">Que vous vendiez des logiciels ou des biens réels, votre site reste fluide. Il absorbe les pics de trafic sans ralentissement.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"space-y-4\\"><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Gestion des commandes</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Suivez chaque commande du panier à la livraison. Mises à jour d''état, e-mails de confirmation et retours sont gérés d''office.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Catégories de produits</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Organisez vos articles avec des URLs claires, des photos et des filtres de langues. Pilotez tout depuis le tableau de bord du CMS.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Boutiques multilingues</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Traduisez vos articles et vos rayons dans toutes les langues. Les stocks sont partagés tout en gardant des métadonnées SEO propres.</p></div></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9908
|
+
updated_at = NOW()
|
|
9909
|
+
WHERE id = 42;
|
|
9910
|
+
|
|
9911
|
+
-- Block ID 43
|
|
9912
|
+
UPDATE public.blocks
|
|
9913
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#064e3b","position":0},{"color":"#022c22","position":100}],"direction":"135deg"}},"column_gap":"none","column_blocks":[[{"block_type":"heading","content":{"level":2,"textAlign":"center","textColor":"background","text_content":"Prêt à lancer votre boutique ?"}},{"block_type":"text","content":{"html_content":"<p class=\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\">Commencez à vendre dès aujourd''hui avec Commerce Pro. Multi-devises, conforme aux taxes et rapide dès l''installation.</p>"}},{"block_type":"button","content":{"url":"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license","size":"lg","text":"Acheter Commerce Pro","variant":"secondary","position":"center"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"vertical_alignment":"center"}'::jsonb,
|
|
9914
|
+
updated_at = NOW()
|
|
9915
|
+
WHERE id = 43;
|
|
9916
|
+
|
|
9917
|
+
-- Block ID 49
|
|
9918
|
+
UPDATE public.blocks
|
|
9919
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#1e1b4b","position":0},{"color":"#0f172a","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\">Couche d''Intelligence IA</p><h2 class=\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\">Boostez votre éditeur avec l''IA native.</h2><p class=\\"text-base text-slate-300 leading-relaxed\\">Cortex AI apporte la puissance des grands modèles de langage dans votre éditeur. Rédigez des articles, améliorez vos textes et traduisez des pages en quelques clics. Les blocs créés restent stables, rapides et fidèles à votre charte.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\"><h3 class=\\"text-lg font-bold text-white mb-4\\">OpenRouter et architecture BYOK</h3><ul class=\\"space-y-3 text-sm text-slate-300\\"><li class=\\"flex items-start gap-3\\"><span class=\\"text-violet-400 font-bold\\">✓</span><span><strong>Apportez votre clé API</strong> — Maîtrisez vos coûts avec vos propres clés OpenRouter. Aucun frais caché.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-violet-400 font-bold\\">✓</span><span><strong>Contexte structuré</strong> — L''IA comprend la mise en page et les colonnes pour des sorties toujours précises.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-violet-400 font-bold\\">✓</span><span><strong>Liberté de modèle</strong> — Choisissez librement entre Claude, GPT et les modèles open-source sans changer de code.</span></li></ul></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9920
|
+
updated_at = NOW()
|
|
9921
|
+
WHERE id = 49;
|
|
9922
|
+
|
|
9923
|
+
-- Block ID 51
|
|
9924
|
+
UPDATE public.blocks
|
|
9925
|
+
SET content = '{"padding":{"top":"lg","bottom":"lg"},"background":{"type":"solid","color":"transparent"},"column_gap":"lg","column_blocks":[[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\">✍️</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Rédaction en un clic</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Rédigez des guides, ajustez vos titres et créez des appels à l''action. Cortex AI adopte votre ton et écrit un texte clair.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\">🔄</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Refactorisation de structure</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Ajustez vos colonnes, ajoutez des grilles et réorganisez vos sections. Cortex AI génère des données valides sans effort.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\">🌐</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Traduction automatique</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Traduisez vos pages complètes entre le français et l''anglais. Cortex AI conserve vos mises en page et styles intacts.</p></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":2,"desktop":3},"vertical_alignment":"stretch"}'::jsonb,
|
|
9926
|
+
updated_at = NOW()
|
|
9927
|
+
WHERE id = 51;
|
|
9928
|
+
|
|
9929
|
+
-- Block ID 52
|
|
9930
|
+
UPDATE public.blocks
|
|
9931
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#0f172a","position":0},{"color":"#1e1b4b","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\">Comment ça marche</p><h3 class=\\"text-2xl font-bold text-white mb-4\\">Une IA qui comprend les blocs</h3><p class=\\"text-sm text-slate-300 leading-relaxed mb-4\\">La plupart des outils IA créent du texte brut sans structure. Cortex AI a été pensé pour l''éditeur de NextBlock. Il analyse vos blocs, vos colonnes et vos options visuelles.</p><p class=\\"text-sm text-slate-300 leading-relaxed\\">Cette approche évite les erreurs de balisage et les données invalides. Votre équipe obtient des rendus élégants sur mobile et ordinateur.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"space-y-4\\"><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Génération de contenu</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Produisez du contenu percutant pour vos pages et articles. L''assistant respecte votre style et délivre des textes fluides.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Optimisation SEO</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Rédigez des titres et méta descriptions de qualité. Cortex AI vérifie la lisibilité et les mots clés avant publication.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Conception axée sur la vie privée</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Vos requêtes vont directement à votre fournisseur d''IA via votre clé privée. Vos données ne sont jamais stockées ni revendues.</p></div></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9932
|
+
updated_at = NOW()
|
|
9933
|
+
WHERE id = 52;
|
|
9934
|
+
|
|
9935
|
+
-- Block ID 53
|
|
9936
|
+
UPDATE public.blocks
|
|
9937
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#312e81","position":0},{"color":"#1e1b4b","position":100}],"direction":"135deg"}},"column_gap":"none","column_blocks":[[{"block_type":"heading","content":{"level":2,"textAlign":"center","textColor":"background","text_content":"Prêt à ajouter l''IA à votre éditeur ?"}},{"block_type":"text","content":{"html_content":"<p class=\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\">Libérez la puissance de la création de contenu avec l''IA. Connectez votre clé, protégez vos données et rédigez vite.</p>"}},{"block_type":"button","content":{"url":"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license","size":"lg","text":"Acheter Cortex AI","variant":"secondary","position":"center"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"vertical_alignment":"center"}'::jsonb,
|
|
9938
|
+
updated_at = NOW()
|
|
9939
|
+
WHERE id = 53;
|
|
9940
|
+
|
|
9941
|
+
-- Block ID 65
|
|
9942
|
+
UPDATE public.blocks
|
|
9943
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#064e3b","position":0},{"color":"#0f172a","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\">Enterprise E-Commerce Engine</p><h2 class=\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\">Turn Next.js Into a Global Storefront.</h2><p class=\\"text-base text-slate-300 leading-relaxed\\">Commerce Pro is a fast store engine built for NextBlock. It helps teams sell physical goods and digital downloads with ease. Everything runs on your own server with total data ownership and zero extra fees.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\"><h3 class=\\"text-lg font-bold text-white mb-4\\">Why Teams Choose Commerce Pro</h3><ul class=\\"space-y-3 text-sm text-slate-300\\"><li class=\\"flex items-start gap-3\\"><span class=\\"text-emerald-400 font-bold\\">✓</span><span><strong>Multi-Currency Pricing</strong> — Live exchange rates, clear charm pricing, and automatic visitor country detection.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-emerald-400 font-bold\\">✓</span><span><strong>Simple Tax Rules</strong> — Native Stripe Tax support calculates local taxes automatically during checkout.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-emerald-400 font-bold\\">✓</span><span><strong>Hybrid Product Catalog</strong> — Sell physical goods with stock counts alongside digital software license keys.</span></li></ul></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9944
|
+
updated_at = NOW()
|
|
9945
|
+
WHERE id = 65;
|
|
9946
|
+
|
|
9947
|
+
-- Block ID 67
|
|
9948
|
+
UPDATE public.blocks
|
|
9949
|
+
SET content = '{"padding":{"top":"lg","bottom":"lg"},"background":{"type":"solid","color":"transparent"},"column_gap":"lg","column_blocks":[[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\">💳</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Headless Stripe Checkout</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Fast checkout flows for single or multiple items. Orders process automatically through secure webhooks with zero friction.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\">🔑</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Freemius Digital Licensing</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Deliver software files, issue license keys, and manage SaaS free trials easily. Everything connects directly to Freemius.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\">🛍️</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Visual Merchandising</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Design custom product pages, highlight special deals, and build shopping grids right inside the visual block editor.</p></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":2,"desktop":3},"vertical_alignment":"stretch"}'::jsonb,
|
|
9950
|
+
updated_at = NOW()
|
|
9951
|
+
WHERE id = 67;
|
|
9952
|
+
|
|
9953
|
+
-- Block ID 68
|
|
9954
|
+
UPDATE public.blocks
|
|
9955
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#0f172a","position":0},{"color":"#064e3b","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\">Under The Hood</p><h3 class=\\"text-2xl font-bold text-white mb-4\\">Built for Production Scale</h3><p class=\\"text-sm text-slate-300 leading-relaxed mb-4\\">Commerce Pro was built from day one for busy storefronts. Every API path is cached near your users, and database queries use fast indexes.</p><p class=\\"text-sm text-slate-300 leading-relaxed\\">Whether you sell digital downloads or retail goods, your storefront loads fast. It handles high traffic spikes without slow downs or crashes.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"space-y-4\\"><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Order Management</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Track customer orders from cart to shipping. Automatic status updates, buyer receipts, and refunds work right out of the box.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Product Categories</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Organize products with clean web addresses, category images, and full multi-language tags. Manage it all from the CMS dashboard.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Multi-Language Storefronts</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Translate products and category pages into any language. Each language shares inventory while keeping its own SEO metadata.</p></div></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9956
|
+
updated_at = NOW()
|
|
9957
|
+
WHERE id = 68;
|
|
9958
|
+
|
|
9959
|
+
-- Block ID 69
|
|
9960
|
+
UPDATE public.blocks
|
|
9961
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#064e3b","position":0},{"color":"#022c22","position":100}],"direction":"135deg"}},"column_gap":"none","column_blocks":[[{"block_type":"heading","content":{"level":2,"textAlign":"center","textColor":"background","text_content":"Ready to launch your storefront?"}},{"block_type":"text","content":{"html_content":"<p class=\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\">Start selling today with Commerce Pro. It is multi-currency, tax-compliant, and lightning-fast out of the box.</p>"}},{"block_type":"button","content":{"url":"#","size":"lg","text":"Purchase Commerce Pro","variant":"secondary","position":"center"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"vertical_alignment":"center"}'::jsonb,
|
|
9962
|
+
updated_at = NOW()
|
|
9963
|
+
WHERE id = 69;
|
|
9964
|
+
|
|
9965
|
+
-- Block ID 70
|
|
9966
|
+
UPDATE public.blocks
|
|
9967
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#1e1b4b","position":0},{"color":"#0f172a","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\">AI Intelligence Layer</p><h2 class=\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\">Supercharge Your Editor with AI.</h2><p class=\\"text-base text-slate-300 leading-relaxed\\">Cortex AI brings powerful language models directly into your editor. You can create content, edit blog drafts, and translate full pages in seconds. It writes clean block data rather than raw text, keeping your layouts stable and fast.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\"><h3 class=\\"text-lg font-bold text-white mb-4\\">OpenRouter & BYOK Architecture</h3><ul class=\\"space-y-3 text-sm text-slate-300\\"><li class=\\"flex items-start gap-3\\"><span class=\\"text-violet-400 font-bold\\">✓</span><span><strong>Bring Your Own Key</strong> — You control costs using your own OpenRouter tokens. There are no hidden fees or markups.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-violet-400 font-bold\\">✓</span><span><strong>Block-Aware Prompts</strong> — Models receive structured context. They see headings, columns, and layout rules for accurate outputs.</span></li><li class=\\"flex items-start gap-3\\"><span class=\\"text-violet-400 font-bold\\">✓</span><span><strong>Model Flexibility</strong> — Switch freely between Claude, GPT, and open-source models without changing code.</span></li></ul></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9968
|
+
updated_at = NOW()
|
|
9969
|
+
WHERE id = 70;
|
|
9970
|
+
|
|
9971
|
+
-- Block ID 72
|
|
9972
|
+
UPDATE public.blocks
|
|
9973
|
+
SET content = '{"padding":{"top":"lg","bottom":"lg"},"background":{"type":"solid","color":"transparent"},"column_gap":"lg","column_blocks":[[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\">✍️</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">One-Click Copywriting</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Write blog posts, polish headlines, and craft calls to action. Cortex AI matches your brand voice and writes clear prose with ease.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\">🔄</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Structure Refactoring</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Reorganize layout grids, split columns, and reorder sections. Cortex AI produces valid block schemas that render safely every time.</p></div>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\"><div class=\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\">🌐</div><h4 class=\\"text-lg font-bold text-foreground mb-2\\">Automated Translation</h4><p class=\\"text-sm text-muted-foreground leading-relaxed\\">Translate entire pages between languages with one click. Cortex AI keeps your styles, nested sections, and media links intact.</p></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":2,"desktop":3},"vertical_alignment":"stretch"}'::jsonb,
|
|
9974
|
+
updated_at = NOW()
|
|
9975
|
+
WHERE id = 72;
|
|
9976
|
+
|
|
9977
|
+
-- Block ID 73
|
|
9978
|
+
UPDATE public.blocks
|
|
9979
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#0f172a","position":0},{"color":"#1e1b4b","position":100}],"direction":"135deg"}},"column_gap":"xl","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class=\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\">How It Works</p><h3 class=\\"text-2xl font-bold text-white mb-4\\">AI That Understands Blocks</h3><p class=\\"text-sm text-slate-300 leading-relaxed mb-4\\">Most AI tools only generate flat text strings. Cortex AI is built specifically for NextBlock editors. It reads your full page structure, column arrays, and style options.</p><p class=\\"text-sm text-slate-300 leading-relaxed\\">This deep connection prevents broken markup and invalid data. Your team gets clean results that look great on desktop and mobile screens right away.</p>"}}],[{"block_type":"text","content":{"html_content":"<div class=\\"space-y-4\\"><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Content Generation</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Create engaging copy for marketing pages and help docs. The assistant follows your editorial style guide and produces clean text.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">SEO Optimization</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Draft concise meta titles and meta descriptions automatically. Cortex AI checks keyword density and reading ease before you publish.</p></div><div class=\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\"><h4 class=\\"text-sm font-bold text-white mb-1\\">Privacy-First Design</h4><p class=\\"text-xs text-slate-400 leading-relaxed\\">Your content goes directly to your chosen provider via private API keys. We never store, log, or train on your private data.</p></div></div>"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center"}'::jsonb,
|
|
9980
|
+
updated_at = NOW()
|
|
9981
|
+
WHERE id = 73;
|
|
9982
|
+
|
|
9983
|
+
-- Block ID 74
|
|
9984
|
+
UPDATE public.blocks
|
|
9985
|
+
SET content = '{"padding":{"top":"xl","bottom":"xl"},"background":{"type":"gradient","gradient":{"type":"linear","stops":[{"color":"#312e81","position":0},{"color":"#1e1b4b","position":100}],"direction":"135deg"}},"column_gap":"none","column_blocks":[[{"block_type":"heading","content":{"level":2,"textAlign":"center","textColor":"background","text_content":"Ready to add AI to your editor?"}},{"block_type":"text","content":{"html_content":"<p class=\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\">Unlock the full power of AI-driven content creation. Connect your key, protect your privacy, and edit faster today.</p>"}},{"block_type":"button","content":{"url":"#","size":"lg","text":"Purchase Cortex AI","variant":"secondary","position":"center"}}]],"container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"vertical_alignment":"center"}'::jsonb,
|
|
9986
|
+
updated_at = NOW()
|
|
9987
|
+
WHERE id = 74;
|
|
9988
|
+
|
|
9989
|
+
|
|
9990
|
+
|
|
9991
|
+
-- >>> FROM: 00000000000036_seed_posts_missing_seo_metadata.sql <<<
|
|
9992
|
+
-- Migration: 00000000000036_seed_posts_missing_seo_metadata.sql
|
|
9993
|
+
-- Purpose: Set meta_title and meta_description for remaining seeded blog posts
|
|
9994
|
+
-- (how-nextblock-works, comment-nextblock-fonctionne, nextblock-commerce-guide, guide-commerce-nextblock)
|
|
9995
|
+
-- so all 9 seeded posts achieve a 100/100 SEO score.
|
|
9996
|
+
-- Safety: Append-only, forward-only, content-guarded updates.
|
|
9997
|
+
|
|
9998
|
+
|
|
9999
|
+
UPDATE public.posts
|
|
10000
|
+
SET meta_title = 'How NextBlock™ Works: Architecture & Under the Hood',
|
|
10001
|
+
meta_description = 'Explore the architecture behind NextBlock: monorepo packages, typed block registry, Supabase database, and modern Next.js editor stack.',
|
|
10002
|
+
updated_at = NOW()
|
|
10003
|
+
WHERE slug = 'how-nextblock-works'
|
|
10004
|
+
AND meta_title IS NULL;
|
|
10005
|
+
|
|
10006
|
+
UPDATE public.posts
|
|
10007
|
+
SET meta_title = 'Comment fonctionne NextBlock™ : Architecture et Moteur',
|
|
10008
|
+
meta_description = 'Découvrez les fondations de NextBlock : monorepo, registre de blocs typés, base de données Supabase et moteur d''édition Next.js moderne.',
|
|
10009
|
+
updated_at = NOW()
|
|
10010
|
+
WHERE slug = 'comment-nextblock-fonctionne'
|
|
10011
|
+
AND meta_title IS NULL;
|
|
10012
|
+
|
|
10013
|
+
UPDATE public.posts
|
|
10014
|
+
SET meta_title = 'The Complete NextBlock™ Commerce Guide | Headless Store',
|
|
10015
|
+
meta_description = 'Build global e-commerce with NextBlock: manage products, configure multi-currency pricing, and accept payments with headless Stripe checkout.',
|
|
10016
|
+
updated_at = NOW()
|
|
10017
|
+
WHERE slug = 'nextblock-commerce-guide'
|
|
10018
|
+
AND meta_title IS NULL;
|
|
10019
|
+
|
|
10020
|
+
UPDATE public.posts
|
|
10021
|
+
SET meta_title = 'Guide Complet du Commerce avec NextBlock™ | E-Commerce',
|
|
10022
|
+
meta_description = 'Développez votre boutique avec NextBlock : gestion de produits, tarifs multi-devises et tunnel de paiement headless avec Stripe Checkout.',
|
|
10023
|
+
updated_at = NOW()
|
|
10024
|
+
WHERE slug = 'guide-commerce-nextblock'
|
|
10025
|
+
AND meta_title IS NULL;
|
|
10026
|
+
|
|
10027
|
+
|
|
10028
|
+
|
|
9856
10029
|
-- Step D: Record the applied migrations in history (truncated in Step B) so
|
|
9857
10030
|
-- \`npm run db:migrate:check\` reports up to date instead of listing every file as pending.
|
|
9858
10031
|
INSERT INTO supabase_migrations.schema_migrations (version, name) VALUES
|
|
@@ -9890,7 +10063,9 @@ END $$;
|
|
|
9890
10063
|
('00000000000031', 'seo_robots_settings_admin_only'),
|
|
9891
10064
|
('00000000000032', 'seed_seo_score_optimizations'),
|
|
9892
10065
|
('00000000000033', 'seed_seo_score_optimizations_p2'),
|
|
9893
|
-
('00000000000034', 'seed_seo_french_legal_readability')
|
|
10066
|
+
('00000000000034', 'seed_seo_french_legal_readability'),
|
|
10067
|
+
('00000000000035', 'digital_products_seo_optimizations'),
|
|
10068
|
+
('00000000000036', 'seed_posts_missing_seo_metadata')
|
|
9894
10069
|
ON CONFLICT (version) DO NOTHING;
|
|
9895
10070
|
|
|
9896
10071
|
-- Step E: Anchor preserved profiles
|
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
interface SeoScoreBadgeProps {
|
|
5
|
-
score: number;
|
|
6
|
-
className?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default function SeoScoreBadge({ score, className = "" }: SeoScoreBadgeProps) {
|
|
10
|
-
const normalizedScore = Math.max(0, Math.min(100, Math.round(score)));
|
|
11
|
-
|
|
12
|
-
let colorClass = "";
|
|
13
|
-
let dotClass = "";
|
|
14
|
-
let title = "";
|
|
15
|
-
|
|
16
|
-
if (normalizedScore >= 90) {
|
|
17
|
-
colorClass = "bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60";
|
|
18
|
-
dotClass = "bg-emerald-500";
|
|
19
|
-
title = `SEO Score: ${normalizedScore}% — Optimal`;
|
|
20
|
-
} else if (normalizedScore >= 70) {
|
|
21
|
-
colorClass = "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-800/60";
|
|
22
|
-
dotClass = "bg-amber-500";
|
|
23
|
-
title = `SEO Score: ${normalizedScore}% — Good (Minor warnings)`;
|
|
24
|
-
} else {
|
|
25
|
-
colorClass = "bg-rose-100 text-rose-800 border-rose-200 dark:bg-rose-950/50 dark:text-rose-300 dark:border-rose-800/60";
|
|
26
|
-
dotClass = "bg-rose-500";
|
|
27
|
-
title = `SEO Score: ${normalizedScore}% — Needs improvement`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<Badge
|
|
32
|
-
variant="outline"
|
|
33
|
-
title={title}
|
|
34
|
-
className={`inline-flex items-center px-2 py-0.5 text-xs font-semibold rounded-full border transition-colors ${colorClass} ${className}`}
|
|
35
|
-
>
|
|
36
|
-
<span className={`w-1.5 h-1.5 rounded-full mr-1.5 shrink-0 ${dotClass}`} />
|
|
37
|
-
{normalizedScore}%
|
|
38
|
-
</Badge>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
1
|
+
import { SeoScoreBadge } from "@nextblock-cms/ui";
|
|
2
|
+
export { SeoScoreBadge, type SeoScoreBadgeProps } from "@nextblock-cms/ui";
|
|
3
|
+
export default SeoScoreBadge;
|
|
@@ -4,6 +4,7 @@ import React from 'react';
|
|
|
4
4
|
import { ProductForm } from '@nextblock-cms/ecommerce';
|
|
5
5
|
import MediaPickerDialog from '../media/components/MediaPickerDialog';
|
|
6
6
|
import DraftStatusActions from '../components/DraftStatusActions';
|
|
7
|
+
import { usePageSeo } from '../../../lib/seo/page-audit-context';
|
|
7
8
|
|
|
8
9
|
type ProductFormProps = React.ComponentProps<typeof ProductForm>;
|
|
9
10
|
type ProductUpdateAction = NonNullable<ProductFormProps['updateAction']>;
|
|
@@ -24,6 +25,23 @@ export default function ProductFormClientShell({
|
|
|
24
25
|
...props
|
|
25
26
|
}: ProductFormClientShellProps) {
|
|
26
27
|
const [isMounted, setIsMounted] = React.useState(false);
|
|
28
|
+
const pageSeo = usePageSeo();
|
|
29
|
+
|
|
30
|
+
const handleSeoChange = React.useCallback(
|
|
31
|
+
(values: { title?: string | null; meta_title?: string | null; meta_description?: string | null }) => {
|
|
32
|
+
if (pageSeo) {
|
|
33
|
+
if (values.title !== undefined) {
|
|
34
|
+
pageSeo.setDocumentTitle(values.title || null);
|
|
35
|
+
}
|
|
36
|
+
pageSeo.setMeta({
|
|
37
|
+
metaTitle: values.meta_title || null,
|
|
38
|
+
metaDescription: values.meta_description || null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[pageSeo]
|
|
43
|
+
);
|
|
44
|
+
|
|
27
45
|
// The draft toolbar is gated on the server-computed `hasDraft`, but the form
|
|
28
46
|
// autosave writes a draft WITHOUT revalidating the route (revalidating would
|
|
29
47
|
// re-init the form and loop the autosave — see updateProductAction). Track
|
|
@@ -64,6 +82,7 @@ export default function ProductFormClientShell({
|
|
|
64
82
|
<ProductForm
|
|
65
83
|
{...props}
|
|
66
84
|
hasOpenDraft={hasDraft}
|
|
85
|
+
onSeoChange={handleSeoChange}
|
|
67
86
|
updateAction={updateAction ? wrappedUpdateAction : undefined}
|
|
68
87
|
mediaPickerNode={
|
|
69
88
|
<MediaPickerDialog
|
|
@@ -35,6 +35,8 @@ import BlockEditorArea from '../../../blocks/components/BlockEditorArea';
|
|
|
35
35
|
import VisibilityControl from '../../../components/VisibilityControl';
|
|
36
36
|
import RevisionHistoryButton from '../../../revisions/RevisionHistoryButton';
|
|
37
37
|
import { buildViewUrl } from '../../../../../lib/publishing/viewUrl';
|
|
38
|
+
import { PageSeoProvider } from '../../../../../lib/seo/page-audit-context';
|
|
39
|
+
import { PageSeoAuditSection } from '../../../../../components/seo/PageSeoAuditSection';
|
|
38
40
|
|
|
39
41
|
export default async function EditProductPage({
|
|
40
42
|
params,
|
|
@@ -211,8 +213,15 @@ export default async function EditProductPage({
|
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
return (
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
+
<PageSeoProvider
|
|
217
|
+
documentTitle={product.title}
|
|
218
|
+
documentType="product"
|
|
219
|
+
initialBlocks={descriptionBlocks}
|
|
220
|
+
initialMetaDescription={product.meta_description}
|
|
221
|
+
initialMetaTitle={product.meta_title}
|
|
222
|
+
>
|
|
223
|
+
<div className="space-y-8 w-full max-w-[1400px] mx-auto px-6 py-8">
|
|
224
|
+
<CortexAiPageContextRegistrar
|
|
216
225
|
context={{
|
|
217
226
|
contentType: 'product',
|
|
218
227
|
entityId: product.id,
|
|
@@ -356,6 +365,8 @@ export default async function EditProductPage({
|
|
|
356
365
|
availableCategoriesProp={allCategories}
|
|
357
366
|
/>
|
|
358
367
|
|
|
368
|
+
<PageSeoAuditSection className="my-6" />
|
|
369
|
+
|
|
359
370
|
<div className="border-t pt-8">
|
|
360
371
|
<h2 className="text-xl font-bold mb-4">Product Description Blocks</h2>
|
|
361
372
|
<BlockEditorArea
|
|
@@ -366,5 +377,6 @@ export default async function EditProductPage({
|
|
|
366
377
|
/>
|
|
367
378
|
</div>
|
|
368
379
|
</div>
|
|
380
|
+
</PageSeoProvider>
|
|
369
381
|
);
|
|
370
382
|
}
|
|
@@ -54,8 +54,8 @@ export interface PageSeoSnapshot {
|
|
|
54
54
|
blocks: unknown[];
|
|
55
55
|
/** Document title for articles / posts where the template renders an H1. */
|
|
56
56
|
documentTitle: string | null;
|
|
57
|
-
/** Whether this is a standalone 'page'
|
|
58
|
-
documentType: 'page' | 'post';
|
|
57
|
+
/** Whether this is a standalone 'page', an editorial 'post', or a store 'product'. */
|
|
58
|
+
documentType: 'page' | 'post' | 'product';
|
|
59
59
|
metaDescription: string | null;
|
|
60
60
|
metaTitle: string | null;
|
|
61
61
|
}
|
|
@@ -75,7 +75,7 @@ const PageSeoContext = createContext<PageSeoContextValue | null>(null);
|
|
|
75
75
|
export interface PageSeoProviderProps {
|
|
76
76
|
children: ReactNode;
|
|
77
77
|
documentTitle?: string | null;
|
|
78
|
-
documentType?: 'page' | 'post';
|
|
78
|
+
documentType?: 'page' | 'post' | 'product';
|
|
79
79
|
initialBlocks?: unknown[];
|
|
80
80
|
initialMetaDescription?: string | null;
|
|
81
81
|
initialMetaTitle?: string | null;
|
|
@@ -380,5 +380,39 @@ describe('buildPageSeoDocument feeding the page-level audit', () => {
|
|
|
380
380
|
expect(doc.headings).toEqual([{ level: 1, order: 0, text: 'Initial Draft Post' }]);
|
|
381
381
|
expect(doc.text).toBe('Initial Draft Post');
|
|
382
382
|
});
|
|
383
|
+
|
|
384
|
+
it('prepends product title as H1 when documentType is product', () => {
|
|
385
|
+
const blocks = [
|
|
386
|
+
{
|
|
387
|
+
block_type: 'section',
|
|
388
|
+
content: {
|
|
389
|
+
column_blocks: [
|
|
390
|
+
[
|
|
391
|
+
{
|
|
392
|
+
block_type: 'heading',
|
|
393
|
+
content: { level: 2, text_content: 'Features' },
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
const withProduct = buildPageSeoDocument(blocks, {
|
|
402
|
+
documentTitle: 'NextBlock Commerce Pro',
|
|
403
|
+
documentType: 'product',
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(withProduct.headings).toEqual([
|
|
407
|
+
{ level: 1, order: 0, text: 'NextBlock Commerce Pro' },
|
|
408
|
+
{ level: 2, order: 1, text: 'Features' },
|
|
409
|
+
]);
|
|
410
|
+
expect(withProduct.words).toContain('commerce');
|
|
411
|
+
expect(withProduct.words).toContain('pro');
|
|
412
|
+
|
|
413
|
+
const audit = auditSeo({ document: withProduct });
|
|
414
|
+
expect(audit.issues.map((i) => i.id)).not.toContain('headings-missing-h1');
|
|
415
|
+
});
|
|
383
416
|
});
|
|
384
417
|
|
|
418
|
+
|
|
@@ -1,463 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
tokenizeWords,
|
|
6
|
-
type SeoDocument,
|
|
7
|
-
type SeoHeading,
|
|
8
|
-
type SeoHeadingLevel,
|
|
9
|
-
type SeoImage,
|
|
10
|
-
type SeoLink,
|
|
11
|
-
} from '@nextblock-cms/utils/seo';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Flattens a page's blocks into the single `SeoDocument` the audit grades.
|
|
15
|
-
*
|
|
16
|
-
* The SEO engine in `@nextblock-cms/utils/seo` was written against one document
|
|
17
|
-
* that represents one page, but the CMS does not store a page as one document:
|
|
18
|
-
* it stores a list of block rows, each holding its own slice of content in its
|
|
19
|
-
* own differently-named field, with most of a real page's copy nested two
|
|
20
|
-
* levels down inside a `section` block's columns. Auditing those rows one at a
|
|
21
|
-
* time is what produced the finding this module exists to retire — "this page
|
|
22
|
-
* has no H1 heading" reported against a single paragraph, when the H1 was sitting
|
|
23
|
-
* in a `heading` block two rows above and the auditor was never shown it.
|
|
24
|
-
*
|
|
25
|
-
* So this walks the whole list in document order and merges everything into one
|
|
26
|
-
* document, which lets `auditSeo` answer the questions it was designed to
|
|
27
|
-
* answer — is there exactly one H1, is there enough copy, where does the
|
|
28
|
-
* keyphrase fall — about the page a visitor actually reads.
|
|
29
|
-
*
|
|
30
|
-
* Two properties are load-bearing and every change here has to preserve them:
|
|
31
|
-
*
|
|
32
|
-
* - **Nothing throws.** `content` is `Json` straight out of Postgres, and the
|
|
33
|
-
* editor hands us half-typed state on every keystroke, so a field can be
|
|
34
|
-
* null, a number, an array where an object was expected, or absent. Every
|
|
35
|
-
* read below tolerates that and skips rather than reporting. An unrecognised
|
|
36
|
-
* block — a custom block, whose content shape is whatever its author defined
|
|
37
|
-
* — is skipped for the same reason: guessing at arbitrary keys would put the
|
|
38
|
-
* author's internal configuration strings into the page's word count.
|
|
39
|
-
* - **It stays linear and allocation-light.** This runs behind the analysis
|
|
40
|
-
* panel's debounce, so it is re-run every time the author pauses typing. It
|
|
41
|
-
* makes one pass over the tree, reuses the image and link objects the HTML
|
|
42
|
-
* reader already built instead of copying them, and joins the text exactly
|
|
43
|
-
* once at the end.
|
|
44
|
-
*
|
|
45
|
-
* One known and accepted limitation: `buildSeoDocument` records where blocks
|
|
46
|
-
* divide the token stream so a multi-word keyphrase cannot be counted across a
|
|
47
|
-
* gap the reader can see, and that side table is keyed on the document object
|
|
48
|
-
* it built, so the merged document here does not carry one. The consequence is
|
|
49
|
-
* narrow — a phrase whose words happen to straddle the join between two blocks
|
|
50
|
-
* can be counted once too often — and it is the same fallback any hand-built
|
|
51
|
-
* document has always had. Correcting it needs a way to register boundaries for
|
|
52
|
-
* an assembled document, which is a change to the engine, not to this walker.
|
|
53
|
-
*/
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* How deep the walk will follow nested containers before giving up.
|
|
57
|
-
*
|
|
58
|
-
* Sections can hold sections, so the structure is genuinely recursive and has
|
|
59
|
-
* no schema-enforced ceiling. Eight levels is far past anything a human builds
|
|
60
|
-
* and cheap to enforce, and it means a corrupt row that somehow refers back
|
|
61
|
-
* into itself costs a bounded walk instead of a blown stack in the editor.
|
|
62
|
-
*/
|
|
63
|
-
const MAX_BLOCK_NESTING_DEPTH = 8;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The heading level used when a `heading` block's level is missing or corrupt.
|
|
67
|
-
*
|
|
68
|
-
* This mirrors `HeadingBlockRenderer`, which falls back to an H2 for anything
|
|
69
|
-
* outside 1-6. The audit has to grade the outline the visitor is served, and
|
|
70
|
-
* defaulting to 1 instead would invent an H1 that is nowhere on the page — and
|
|
71
|
-
* then report a second, entirely fictional H1 as a duplicate.
|
|
72
|
-
*/
|
|
73
|
-
const FALLBACK_HEADING_LEVEL: SeoHeadingLevel = 2;
|
|
74
|
-
|
|
75
|
-
/** The accumulating page document, before its parts are joined. */
|
|
76
|
-
interface PageDocumentDraft {
|
|
77
|
-
headings: SeoHeading[];
|
|
78
|
-
images: SeoImage[];
|
|
79
|
-
links: SeoLink[];
|
|
80
|
-
/**
|
|
81
|
-
* Each block's text, kept apart until the end and then joined with a single
|
|
82
|
-
* space. Concatenating without a separator would glue the last word of one
|
|
83
|
-
* block onto the first word of the next ("…our coffee" + "beans are…" reading
|
|
84
|
-
* as the single token "coffeebeans"), which corrupts the word count, the
|
|
85
|
-
* keyphrase density and the readability sample all at once.
|
|
86
|
-
*/
|
|
87
|
-
textParts: string[];
|
|
88
|
-
words: string[];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function createDraft(): PageDocumentDraft {
|
|
92
|
-
return { headings: [], images: [], links: [], textParts: [], words: [] };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Narrows to a plain object, which is the only shape a block or content has. */
|
|
96
|
-
function readRecord(value: unknown): Record<string, unknown> | null {
|
|
97
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
98
|
-
? (value as Record<string, unknown>)
|
|
99
|
-
: null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Reads a field that is supposed to hold display text.
|
|
104
|
-
*
|
|
105
|
-
* Whitespace is collapsed to single spaces to match `SeoDocument.text`, which
|
|
106
|
-
* the readability pass tokenises on the assumption that it already has been.
|
|
107
|
-
* Anything that is not a string reads as empty rather than being coerced,
|
|
108
|
-
* because `String(someObject)` would put "[object Object]" on the page.
|
|
109
|
-
*/
|
|
110
|
-
function readText(value: unknown): string {
|
|
111
|
-
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Clamps a stored heading level onto the six levels HTML actually has. */
|
|
115
|
-
function readHeadingLevel(value: unknown): SeoHeadingLevel {
|
|
116
|
-
const level = typeof value === 'number' ? Math.trunc(value) : Number.NaN;
|
|
117
|
-
|
|
118
|
-
return Number.isFinite(level) && level >= 1 && level <= 6
|
|
119
|
-
? (level as SeoHeadingLevel)
|
|
120
|
-
: FALLBACK_HEADING_LEVEL;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Adds a run of plain text, keeping the token stream in step with the text. */
|
|
124
|
-
function appendText(draft: PageDocumentDraft, text: string): void {
|
|
125
|
-
if (text === '') {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
draft.textParts.push(text);
|
|
130
|
-
for (const word of tokenizeWords(text)) {
|
|
131
|
-
draft.words.push(word);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Folds a document the engine already built — one rich-text block's HTML or
|
|
137
|
-
* Tiptap JSON — into the page draft.
|
|
138
|
-
*
|
|
139
|
-
* Headings are renumbered as they land so `order` counts positions on the page
|
|
140
|
-
* rather than positions within whichever block happened to contain them; the
|
|
141
|
-
* audit uses that number to say where the H1 sits, and a per-block number would
|
|
142
|
-
* point the author at the wrong heading.
|
|
143
|
-
*/
|
|
144
|
-
function appendDocument(draft: PageDocumentDraft, document: SeoDocument): void {
|
|
145
|
-
for (const heading of document.headings) {
|
|
146
|
-
draft.headings.push({ level: heading.level, order: draft.headings.length, text: heading.text });
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (const image of document.images) {
|
|
150
|
-
draft.images.push(image);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
for (const link of document.links) {
|
|
154
|
-
draft.links.push(link);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (document.text !== '') {
|
|
158
|
-
draft.textParts.push(document.text);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// `words` is taken from the sub-document rather than re-tokenised from its
|
|
162
|
-
// text: the engine guarantees the two agree, and re-tokenising would double
|
|
163
|
-
// the work on the largest blocks on the page for an identical answer.
|
|
164
|
-
for (const word of document.words) {
|
|
165
|
-
draft.words.push(word);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Adds one heading block, which is a standalone row rather than inline markup. */
|
|
170
|
-
function collectHeadingBlock(content: Record<string, unknown>, draft: PageDocumentDraft): void {
|
|
171
|
-
// Not run through an HTML stripper, unlike a rich-text block: the renderer
|
|
172
|
-
// prints `text_content` as a React child, so any markup in it is escaped and
|
|
173
|
-
// the visitor sees the angle brackets. Stripping here would analyse text the
|
|
174
|
-
// page never shows.
|
|
175
|
-
const text = readText(content['text_content']);
|
|
176
|
-
|
|
177
|
-
draft.headings.push({
|
|
178
|
-
level: readHeadingLevel(content['level']),
|
|
179
|
-
order: draft.headings.length,
|
|
180
|
-
text,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// A heading is also words on the page. Leaving it out of the text would make
|
|
184
|
-
// headings free of charge in the word count and invisible to the keyphrase
|
|
185
|
-
// density, even though they are the most heavily weighted copy on the page.
|
|
186
|
-
appendText(draft, text);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Adds one image block.
|
|
191
|
-
*
|
|
192
|
-
* The source is resolved the way `ImageBlockRenderer` resolves it — an external
|
|
193
|
-
* URL wins, otherwise the R2 object key — and a block with neither is not
|
|
194
|
-
* recorded as an image at all, because that block renders a "media not
|
|
195
|
-
* selected" placeholder. Reporting it would raise "an image is missing alt
|
|
196
|
-
* text" about something the visitor never sees as an image, which is precisely
|
|
197
|
-
* the class of false finding this module was written to remove.
|
|
198
|
-
*/
|
|
199
|
-
function collectImageBlock(content: Record<string, unknown>, draft: PageDocumentDraft): void {
|
|
200
|
-
const externalUrl = readText(content['external_url']);
|
|
201
|
-
const objectKey = readText(content['object_key']);
|
|
202
|
-
const source = externalUrl !== '' ? externalUrl : objectKey;
|
|
203
|
-
|
|
204
|
-
if (source === '') {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
draft.images.push({ alt: readText(content['alt_text']), src: source });
|
|
209
|
-
|
|
210
|
-
// The caption is rendered in a <figcaption> under the image, so it is prose a
|
|
211
|
-
// reader and a crawler both see, and it belongs in the page's text.
|
|
212
|
-
appendText(draft, readText(content['caption']));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Adds one button block: a link the visitor can follow, with a visible label. */
|
|
216
|
-
function collectButtonBlock(content: Record<string, unknown>, draft: PageDocumentDraft): void {
|
|
217
|
-
const text = readText(content['text']);
|
|
218
|
-
const href = readText(content['url']);
|
|
219
|
-
|
|
220
|
-
if (href !== '') {
|
|
221
|
-
draft.links.push({ external: isExternalHref(href), href, text });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
appendText(draft, text);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Adds a testimonial's visible quote and attribution. */
|
|
228
|
-
function collectTestimonialBlock(
|
|
229
|
-
content: Record<string, unknown>,
|
|
230
|
-
draft: PageDocumentDraft
|
|
231
|
-
): void {
|
|
232
|
-
appendText(draft, readText(content['quote']));
|
|
233
|
-
appendText(draft, readText(content['author_name']));
|
|
234
|
-
appendText(draft, readText(content['author_title']));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Walks `column_blocks`, an array of columns each holding an array of blocks.
|
|
239
|
-
*
|
|
240
|
-
* Column order then block order is the order the page reads in on a phone,
|
|
241
|
-
* where the columns stack, and it is the only total order that exists for this
|
|
242
|
-
* shape. Getting it wrong would not lose any content, but it would misreport
|
|
243
|
-
* which heading comes first and where in the copy the keyphrase falls.
|
|
244
|
-
*/
|
|
245
|
-
function collectColumnBlocks(value: unknown, draft: PageDocumentDraft, depth: number): void {
|
|
246
|
-
if (!Array.isArray(value)) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
for (const column of value) {
|
|
251
|
-
if (!Array.isArray(column)) {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
for (const block of column) {
|
|
256
|
-
collectBlock(block, draft, depth + 1);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Walks a section, which is where most of a real page's content lives.
|
|
263
|
-
*
|
|
264
|
-
* Its children are not rows in the blocks array — they are plain
|
|
265
|
-
* `{ block_type, content }` objects buried in `content.column_blocks` — so a
|
|
266
|
-
* walker that only looked at the top-level list would under-report almost every
|
|
267
|
-
* page ever built in this CMS.
|
|
268
|
-
*
|
|
269
|
-
* A section in slider mode renders its slides *instead of* its columns, and
|
|
270
|
-
* this mirrors that: walking both would count copy from a stale column set the
|
|
271
|
-
* visitor cannot reach, and inflate the word count with it.
|
|
272
|
-
*/
|
|
273
|
-
function collectSectionBlock(
|
|
274
|
-
content: Record<string, unknown>,
|
|
275
|
-
draft: PageDocumentDraft,
|
|
276
|
-
depth: number
|
|
277
|
-
): void {
|
|
278
|
-
const slides = content['slides'];
|
|
279
|
-
|
|
280
|
-
if (content['slider'] === true && Array.isArray(slides) && slides.length > 0) {
|
|
281
|
-
for (const slide of slides) {
|
|
282
|
-
const record = readRecord(slide);
|
|
283
|
-
if (record !== null) {
|
|
284
|
-
collectColumnBlocks(record['column_blocks'], draft, depth);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
collectColumnBlocks(content['column_blocks'], draft, depth);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** Dispatches one block row onto the reader that knows where its text lives. */
|
|
295
|
-
function collectBlock(value: unknown, draft: PageDocumentDraft, depth: number): void {
|
|
296
|
-
if (depth > MAX_BLOCK_NESTING_DEPTH) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const block = readRecord(value);
|
|
301
|
-
if (block === null) {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const blockType = typeof block['block_type'] === 'string' ? block['block_type'] : '';
|
|
306
|
-
const content = readRecord(block['content']);
|
|
307
|
-
if (content === null) {
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
switch (blockType) {
|
|
312
|
-
case 'text': {
|
|
313
|
-
// `html_content` holds either an HTML string or a JSON-stringified Tiptap
|
|
314
|
-
// document depending on when and where the block was authored.
|
|
315
|
-
// `buildSeoDocument` already distinguishes the two and reads both, so
|
|
316
|
-
// this delegates rather than growing a second parser that could disagree
|
|
317
|
-
// with the one the block-level panel uses. `text_content` is the shape
|
|
318
|
-
// some older rows still carry.
|
|
319
|
-
const source = content['html_content'] ?? content['text_content'];
|
|
320
|
-
appendDocument(draft, buildSeoDocument(source));
|
|
321
|
-
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
case 'heading':
|
|
326
|
-
collectHeadingBlock(content, draft);
|
|
327
|
-
|
|
328
|
-
return;
|
|
329
|
-
|
|
330
|
-
case 'image':
|
|
331
|
-
collectImageBlock(content, draft);
|
|
332
|
-
|
|
333
|
-
return;
|
|
334
|
-
|
|
335
|
-
case 'button':
|
|
336
|
-
collectButtonBlock(content, draft);
|
|
337
|
-
|
|
338
|
-
return;
|
|
339
|
-
|
|
340
|
-
case 'testimonial':
|
|
341
|
-
collectTestimonialBlock(content, draft);
|
|
342
|
-
|
|
343
|
-
return;
|
|
344
|
-
|
|
345
|
-
// The registry calls this `video_embed`; `video` is accepted alongside it
|
|
346
|
-
// because that is the name the block is known by in the UI and in prompts,
|
|
347
|
-
// and both carry the same optional `title`.
|
|
348
|
-
case 'video':
|
|
349
|
-
case 'video_embed':
|
|
350
|
-
appendText(draft, readText(content['title']));
|
|
351
|
-
|
|
352
|
-
return;
|
|
353
|
-
|
|
354
|
-
case 'section':
|
|
355
|
-
collectSectionBlock(content, draft, depth);
|
|
356
|
-
|
|
357
|
-
return;
|
|
358
|
-
|
|
359
|
-
// `hero` is not in the current registry, but rows created before sections
|
|
360
|
-
// absorbed it still exist in live databases and still render, so their copy
|
|
361
|
-
// still counts. They keep both shapes: slides and a plain column set.
|
|
362
|
-
case 'hero': {
|
|
363
|
-
const slides = content['slides'];
|
|
364
|
-
if (Array.isArray(slides)) {
|
|
365
|
-
for (const slide of slides) {
|
|
366
|
-
const record = readRecord(slide);
|
|
367
|
-
if (record !== null) {
|
|
368
|
-
collectColumnBlocks(record['column_blocks'], draft, depth);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
collectColumnBlocks(content['column_blocks'], draft, depth);
|
|
374
|
-
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
default:
|
|
379
|
-
// Every other block type — posts grids, forms, the commerce blocks, and
|
|
380
|
-
// any custom block whose content is defined by whoever built it — renders
|
|
381
|
-
// from data this walker cannot read, so it contributes nothing rather
|
|
382
|
-
// than contributing a guess.
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export interface BuildPageSeoDocumentOptions {
|
|
388
|
-
/**
|
|
389
|
-
* An explicit document title to treat as the page's top-level H1.
|
|
390
|
-
*
|
|
391
|
-
* Posts in NextBlock render their title in an editorial H1 wrapper on the public
|
|
392
|
-
* page (`PostClientContent.tsx`) rather than storing it as a heading block inside
|
|
393
|
-
* the content array. Supplying `documentTitle` with `documentType: 'post'` ensures
|
|
394
|
-
* the audit sees that H1 instead of falsely reporting `headings-missing-h1`.
|
|
395
|
-
*/
|
|
396
|
-
documentTitle?: string | null;
|
|
397
|
-
/**
|
|
398
|
-
* The kind of document being graded. When set to 'post' and a documentTitle is present,
|
|
399
|
-
* the title is treated as the primary H1 of the document.
|
|
400
|
-
*/
|
|
401
|
-
documentType?: 'page' | 'post';
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Builds one `SeoDocument` from a page's or post's block list.
|
|
406
|
-
*
|
|
407
|
-
* `blocks` is deliberately typed `unknown`: it comes from form state, from a
|
|
408
|
-
* draft row, or straight from Supabase as `Json`, and pretending at the
|
|
409
|
-
* signature that it is already an array of well-formed rows would only push the
|
|
410
|
-
* validation somewhere that has less context to do it in.
|
|
411
|
-
*/
|
|
412
|
-
export function buildPageSeoDocument(
|
|
413
|
-
blocks: unknown,
|
|
414
|
-
options?: BuildPageSeoDocumentOptions
|
|
415
|
-
): SeoDocument {
|
|
416
|
-
const isPostWithTitle =
|
|
417
|
-
options?.documentType === 'post' &&
|
|
418
|
-
typeof options?.documentTitle === 'string' &&
|
|
419
|
-
options.documentTitle.trim() !== '';
|
|
420
|
-
|
|
421
|
-
if (!Array.isArray(blocks) || blocks.length === 0) {
|
|
422
|
-
if (isPostWithTitle) {
|
|
423
|
-
const titleText = options!.documentTitle!.trim();
|
|
424
|
-
const titleWords = tokenizeWords(titleText);
|
|
425
|
-
return {
|
|
426
|
-
headings: [{ level: 1, order: 0, text: titleText }],
|
|
427
|
-
images: [],
|
|
428
|
-
links: [],
|
|
429
|
-
text: titleText,
|
|
430
|
-
words: titleWords,
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
return emptySeoDocument();
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const draft = createDraft();
|
|
437
|
-
for (const block of blocks) {
|
|
438
|
-
collectBlock(block, draft, 0);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (isPostWithTitle) {
|
|
442
|
-
const titleText = options!.documentTitle!.trim();
|
|
443
|
-
const titleWords = tokenizeWords(titleText);
|
|
444
|
-
draft.headings.unshift({
|
|
445
|
-
level: 1,
|
|
446
|
-
order: 0,
|
|
447
|
-
text: titleText,
|
|
448
|
-
});
|
|
449
|
-
for (let i = 1; i < draft.headings.length; i++) {
|
|
450
|
-
draft.headings[i].order = i;
|
|
451
|
-
}
|
|
452
|
-
draft.textParts.unshift(titleText);
|
|
453
|
-
draft.words.unshift(...titleWords);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
headings: draft.headings,
|
|
458
|
-
images: draft.images,
|
|
459
|
-
links: draft.links,
|
|
460
|
-
text: draft.textParts.join(' '),
|
|
461
|
-
words: draft.words,
|
|
462
|
-
};
|
|
463
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
buildPageSeoDocument,
|
|
3
|
+
type BuildPageSeoDocumentOptions,
|
|
4
|
+
} from '@nextblock-cms/utils/seo';
|
|
@@ -346,7 +346,7 @@ function toCustomRule(group: CustomRuleGroup): RobotsRule {
|
|
|
346
346
|
const values = group.other[key];
|
|
347
347
|
other[key] = values.length === 1 ? values[0] : values;
|
|
348
348
|
}
|
|
349
|
-
rule.other = other;
|
|
349
|
+
(rule as any).other = other;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
return rule;
|
|
@@ -476,9 +476,10 @@ export function renderRobotsMetadata(metadata: MetadataRoute.Robots): string {
|
|
|
476
476
|
content += `Crawl-delay: ${rule.crawlDelay}\n`;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
479
|
+
const ruleOther = (rule as any).other;
|
|
480
|
+
if (ruleOther) {
|
|
481
|
+
for (const key of Object.keys(ruleOther)) {
|
|
482
|
+
const value = ruleOther[key];
|
|
482
483
|
if (value === null || value === undefined) {
|
|
483
484
|
continue;
|
|
484
485
|
}
|
|
@@ -188,5 +188,15 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
|
|
|
188
188
|
"version": "00000000000034",
|
|
189
189
|
"name": "00000000000034_seed_seo_french_legal_readability.sql",
|
|
190
190
|
"sql": "-- Migration: 00000000000034_seed_seo_french_legal_readability.sql\n-- Description: Optimize French legal pages (politique de confidentialité & conditions d'utilisation) readability to achieve 100/100 Page SEO score.\n-- Safety: Forward-only, content-guarded updates that only apply to default seeded content.\n\nDO $$\nBEGIN\n -- Page 10: Politique de confidentialité FR (Block 92)\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Politique de confidentialité</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n<p>NextBlock™ CMS (« nous ») respecte votre vie privée. Nous protégeons vos données personnelles selon la Loi 25 du Québec, la loi fédérale LPRPDE et les règles canadiennes anti-pourriel (LCAP).</p>\r\n\r\n<h2>1. Responsable de la protection des données personnelles</h2>\r\n<p>Notre responsable veille au respect des règles de confidentialité. Vous pouvez lui écrire à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>2. Renseignements que nous recueillons</h2>\r\n<ul>\r\n <li><strong>Renseignements de compte</strong> — nom, adresse courriel et identifiants lors de l''inscription.</li>\r\n <li><strong>Données d''utilisation et d''appareil</strong> — recueillies uniquement avec votre consentement au moyen de outils web.</li>\r\n <li><strong>Communications</strong> — les messages que vous nous envoyez et vos préférences marketing.</li>\r\n</ul>\r\n\r\n<h2>3. Finalités et consentement</h2>\r\n<p>Nous recueillons vos données pour des motifs clairs : faire fonctionner nos services, sécuriser les accès et vous répondre. Les outils d''analyse et de suivi ne s''activent qu''avec votre accord clair. Selon la Loi 25, les témoins optionnels restent coupés tant que vous ne les acceptez pas. Vous pouvez retirer votre accord en tout temps.</p>\r\n\r\n<h2>4. Témoins et technologies de suivi</h2>\r\n<p>Les témoins essentiels assurent le bon usage du site. Ils ne demandent aucun accord préalable. Les témoins d''analyse se chargent seulement après votre choix sur notre bandeau. Nous gardons votre choix en mémoire pour le respecter.</p>\r\n\r\n<h2>5. Partage des données</h2>\r\n<p>Nous ne vendons pas vos données personnelles. Nous ne les communiquons qu''à des fournisseurs qui nous aident à exploiter la plateforme, sous obligation de confidentialité, ou lorsque la loi l''exige.</p>\r\n\r\n<h2>6. Conservation</h2>\r\n<p>Nous ne conservons les données personnelles que le temps nécessaire aux fins décrites ou exigé par la loi, après quoi ils sont détruits ou anonymisés sans risque.</p>\r\n\r\n<h2>7. Vos droits</h2>\r\n<p>Vous avez le droit de lire, de corriger et de faire effacer vos données. Vous pouvez aussi retirer votre accord ou demander une copie de vos données. Pour exercer vos droits, écrivez à notre responsable à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n\r\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\r\n<p>Nous n''envoyons des messages électroniques commerciaux qu''avec votre consentement. Chaque message nous identifie et comporte un mécanisme de désabonnement fonctionnel que nous respectons rapidement.</p>\r\n\r\n<h2>9. Mesures de sécurité</h2>\r\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées — dont le chiffrement en transit et le contrôle des accès — pour protéger vos données.</p>\r\n\r\n<h2>10. Logiciel libre</h2>\r\n<p>NextBlock™ CMS est un logiciel libre sous licence AGPLv3. Si vous hébergez NextBlock vous-même, vous gérez votre propre serveur. Vous êtes responsable des données sur votre instance, et ce texte est un modèle que vous pouvez adapter.</p>\r\n\r\n<h2>11. Modifications</h2>\r\n<p>Nous pouvons mettre à jour cette politique. Les changements importants seront communiqués sur le site et la date de mise à jour sera révisée.</p>\r\n\r\n<h2>12. Nous joindre</h2>\r\n<p>Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>. Vous pouvez aussi vous adresser à la Commission d''accès à l''information du Québec.</p>\r\n'::text)),\n updated_at = now()\n WHERE id = 92\n AND page_id = 10\n AND content->>'html_content' LIKE '%Politique de confidentialité%';\n\n -- Page 12: Conditions d'utilisation FR (Block 94)\n UPDATE public.blocks\n SET content = jsonb_set(content, '{html_content}', to_jsonb('\r\n<h1>Règles du service</h1>\r\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\r\n\r\n<h2>1. Acceptation des conditions</h2>\r\n<p>En utilisant le CMS NextBlock™ et les services associés (les « Services »), vous acceptez ces règles du service. Si vous refusez ces règles, veuillez ne pas utiliser les Services.</p>\r\n\r\n<h2>2. Logiciel libre et à code source ouvert</h2>\r\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou, à votre choix, toute version ultérieure. Vous êtes libre d''exécuter, d''étudier, de partager et de modifier le logiciel selon les termes de cette licence. Une copie de la licence est fournie avec le logiciel et est aussi disponible à <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\">gnu.org/licenses/agpl-3.0.html</a>.</p>\r\n<p>Droit d''auteur © 2025 NextBlock™ CMS.</p>\r\n\r\n<h2>3. Disponibilité du code source</h2>\r\n<p>Conformément à l''article 13 de l''AGPL-3.0, si vous exploitez une version modifiée de NextBlock™ CMS et la rendez accessible à des utilisateurs sur un réseau, vous devez offrir clairement à ces utilisateurs l''accès au code source correspondant de votre version modifiée, gratuitement, par un moyen usuel de copie de logiciels.</p>\r\n\r\n<h2>4. Marques de commerce</h2>\r\n<p>L''AGPL-3.0 accorde de larges droits sur le code source du logiciel, mais <strong>n''accorde aucun droit</strong> sur nos noms commerciaux, marques de commerce ou marques de service. « NextBlock™ », le nom NextBlock™ CMS et les logos associés demeurent notre propriété et ne peuvent être utilisés d''une manière laissant entendre une approbation ou une affiliation sans notre autorisation écrite préalable.</p>\r\n\r\n<h2>5. Comptes et utilisation acceptable</h2>\r\n<p>Si vous créez un compte, vous êtes responsable de la protection de vos identifiants et de toute activité effectuée à partir de votre compte, et vous vous engagez à nous aviser rapidement de toute utilisation non autorisée. Vous vous engagez à ne pas détourner les Services, notamment en tentant de les perturber, d''y accéder sans autorisation ou de les utiliser à des fins illégales.</p>\r\n\r\n<h2>6. Absence de garantie</h2>\r\n<p>Selon l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou tacite. Cela inclut les garanties de vente ou d''usage pour un besoin précis. Vous prenez sur vous les risques liés au bon emploi du logiciel.</p>\r\n\r\n<h2>7. Limitation de devoir</h2>\r\n<p>Comme l''énonce l''article 16 de l''AGPL-3.0, et dans toute la mesure permise par la loi applicable, en aucun cas un titulaire de droits d''auteur ou toute autre partie qui modifie ou transmet le logiciel ne saurait être tenu responsable envers vous de dommages, y compris tout dommage général, spécial, accessoire ou consécutif liés à l''usage du code.</p>\r\n\r\n<h2>8. Droit applicable</h2>\r\n<p>Ces conditions suivent les lois de la province de Québec et les lois du Canada applicables. Rien ici ne réduit vos droits stricts de consommateur selon ces lois.</p>\r\n\r\n<h2>9. Modifications</h2>\r\n<p>Nous pouvons réviser ces conditions de temps à autre. Les changements importants seront communiqués au moyen des Services, et l''utilisation continue des Services après leur entrée en vigueur vaut acceptation.</p>\r\n\r\n<h2>10. Nous joindre</h2>\r\n<p>Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\"mailto:{{privacy_email}}\">{{privacy_email}}</a>.</p>\r\n'::text)),\n updated_at = now()\n WHERE id = 94\n AND page_id = 12\n AND content->>'html_content' LIKE '%Conditions d''utilisation%';\n\nEND $$;\n"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"version": "00000000000035",
|
|
194
|
+
"name": "00000000000035_digital_products_seo_optimizations.sql",
|
|
195
|
+
"sql": "-- Migration: 00000000000035_digital_products_seo_optimizations.sql\n-- Purpose: Set meta_title and meta_description for the 2 real digital products (EN + FR)\n-- and optimize their block heading levels and copy readability to achieve 100/100 SEO scores.\n-- Safety: Append-only, forward-only, content-guarded updates.\n\nBEGIN;\n\n-- 1. Digital Products SEO Metadata (Titles & Descriptions)\n-- NextBlock Cortex AI - Cortex AI License (EN)\nUPDATE public.products\nSET meta_title = 'NextBlock Cortex AI License | Native AI Content Engine',\n meta_description = 'Add native AI copywriting, real-time block translation, and structure refactoring to your NextBlock editor with our BYOK Cortex AI license.',\n updated_at = NOW()\nWHERE id = 'd48a4bb4-5119-4576-9c1d-32c404dd84f8';\n\n-- Licence NextBlock™ Cortex AI (FR)\nUPDATE public.products\nSET meta_title = 'Licence NextBlock Cortex AI | Moteur IA pour Éditeur',\n meta_description = 'Intégrez la rédaction assistée par IA, la traduction de blocs et l''optimisation de structure dans NextBlock grâce à la licence Cortex AI.',\n updated_at = NOW()\nWHERE id = '69848224-a6d6-43c9-9748-f41a8e8e424b';\n\n-- NextBlock Commerce Pro - Commerce License (EN)\nUPDATE public.products\nSET meta_title = 'NextBlock Commerce Pro | Global Next.js Storefronts',\n meta_description = 'Launch high-performance digital and physical stores with headless Stripe checkout, Freemius licensing, and multi-currency pricing in NextBlock.',\n updated_at = NOW()\nWHERE id = 'ed86bd77-da61-4e6f-826c-956f9a55b7aa';\n\n-- Licence NextBlock™ Commerce Pro (FR)\nUPDATE public.products\nSET meta_title = 'Licence NextBlock Commerce Pro | E-Commerce Next.js',\n meta_description = 'Créez des boutiques Next.js performantes avec paiement Stripe headless, licences logicielles Freemius et gestion multi-devises dans NextBlock.',\n updated_at = NOW()\nWHERE id = '5c9e1b99-eeed-4fe0-adfa-31e81004c2f9';\n\n-- 2. Digital Products Block Content & Heading Optimizations\n-- Block ID 39\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Moteur E-Commerce d''Entreprise</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Faites de Next.js une Boutique Mondiale.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Commerce Pro est un moteur de boutique moderne conçu pour NextBlock. Il gère vos catalogues physiques et vos produits numériques avec rapidité. Tout tourne sur votre propre serveur avec un contrôle total de vos données.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">Pourquoi choisir Commerce Pro</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Multi-Devises</strong> — Taux de change en direct, règles d''arrondis et détection automatique du pays de l''acheteur.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Taxes automatiques</strong> — Prise en charge native de Stripe Tax pour respecter les règles fiscales locales.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Catalogue hybride</strong> — Vendez des produits physiques avec gestion de stock et des licences logicielles.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 39;\n\n-- Block ID 41\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">💳</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Checkout Stripe Headless</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Paiement direct et sécurisé pour un ou plusieurs articles. Les commandes se valident automatiquement grâce aux webhooks Stripe.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🔑</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Licences Numériques Freemius</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Distribuez vos logiciels, activez vos clés et gérez les périodes d''essai SaaS. Tout fonctionne directement avec Freemius sans souci.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🛍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Merchandising Visuel</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Concevez vos fiches produits, mettez en valeur vos promotions et créez vos rayons directement dans l''éditeur de blocs.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 41;\n\n-- Block ID 42\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#064e3b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Sous le capot</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Conçu pour la production à grande échelle</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Commerce Pro a été pensé dès le début pour les boutiques actives. Chaque route API est mise en cache, et les requêtes SQL sont optimisées.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Que vous vendiez des logiciels ou des biens réels, votre site reste fluide. Il absorbe les pics de trafic sans ralentissement.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Gestion des commandes</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Suivez chaque commande du panier à la livraison. Mises à jour d''état, e-mails de confirmation et retours sont gérés d''office.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Catégories de produits</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Organisez vos articles avec des URLs claires, des photos et des filtres de langues. Pilotez tout depuis le tableau de bord du CMS.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Boutiques multilingues</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Traduisez vos articles et vos rayons dans toutes les langues. Les stocks sont partagés tout en gardant des métadonnées SEO propres.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 42;\n\n-- Block ID 43\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#022c22\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Prêt à lancer votre boutique ?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\\">Commencez à vendre dès aujourd''hui avec Commerce Pro. Multi-devises, conforme aux taxes et rapide dès l''installation.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\",\"size\":\"lg\",\"text\":\"Acheter Commerce Pro\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 43;\n\n-- Block ID 49\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">Couche d''Intelligence IA</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Boostez votre éditeur avec l''IA native.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Cortex AI apporte la puissance des grands modèles de langage dans votre éditeur. Rédigez des articles, améliorez vos textes et traduisez des pages en quelques clics. Les blocs créés restent stables, rapides et fidèles à votre charte.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">OpenRouter et architecture BYOK</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Apportez votre clé API</strong> — Maîtrisez vos coûts avec vos propres clés OpenRouter. Aucun frais caché.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Contexte structuré</strong> — L''IA comprend la mise en page et les colonnes pour des sorties toujours précises.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Liberté de modèle</strong> — Choisissez librement entre Claude, GPT et les modèles open-source sans changer de code.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 49;\n\n-- Block ID 51\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">✍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Rédaction en un clic</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Rédigez des guides, ajustez vos titres et créez des appels à l''action. Cortex AI adopte votre ton et écrit un texte clair.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🔄</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Refactorisation de structure</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Ajustez vos colonnes, ajoutez des grilles et réorganisez vos sections. Cortex AI génère des données valides sans effort.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🌐</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Traduction automatique</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Traduisez vos pages complètes entre le français et l''anglais. Cortex AI conserve vos mises en page et styles intacts.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 51;\n\n-- Block ID 52\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">Comment ça marche</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Une IA qui comprend les blocs</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">La plupart des outils IA créent du texte brut sans structure. Cortex AI a été pensé pour l''éditeur de NextBlock. Il analyse vos blocs, vos colonnes et vos options visuelles.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Cette approche évite les erreurs de balisage et les données invalides. Votre équipe obtient des rendus élégants sur mobile et ordinateur.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Génération de contenu</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Produisez du contenu percutant pour vos pages et articles. L''assistant respecte votre style et délivre des textes fluides.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Optimisation SEO</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Rédigez des titres et méta descriptions de qualité. Cortex AI vérifie la lisibilité et les mots clés avant publication.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Conception axée sur la vie privée</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Vos requêtes vont directement à votre fournisseur d''IA via votre clé privée. Vos données ne sont jamais stockées ni revendues.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 52;\n\n-- Block ID 53\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Prêt à ajouter l''IA à votre éditeur ?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\\">Libérez la puissance de la création de contenu avec l''IA. Connectez votre clé, protégez vos données et rédigez vite.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\",\"size\":\"lg\",\"text\":\"Acheter Cortex AI\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 53;\n\n-- Block ID 65\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Enterprise E-Commerce Engine</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Turn Next.js Into a Global Storefront.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Commerce Pro is a fast store engine built for NextBlock. It helps teams sell physical goods and digital downloads with ease. Everything runs on your own server with total data ownership and zero extra fees.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-emerald-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">Why Teams Choose Commerce Pro</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Multi-Currency Pricing</strong> — Live exchange rates, clear charm pricing, and automatic visitor country detection.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Simple Tax Rules</strong> — Native Stripe Tax support calculates local taxes automatically during checkout.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-emerald-400 font-bold\\\">✓</span><span><strong>Hybrid Product Catalog</strong> — Sell physical goods with stock counts alongside digital software license keys.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 65;\n\n-- Block ID 67\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">💳</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Headless Stripe Checkout</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Fast checkout flows for single or multiple items. Orders process automatically through secure webhooks with zero friction.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🔑</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Freemius Digital Licensing</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Deliver software files, issue license keys, and manage SaaS free trials easily. Everything connects directly to Freemius.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-emerald-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 text-xl dark:bg-emerald-950\\\">🛍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Visual Merchandising</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Design custom product pages, highlight special deals, and build shopping grids right inside the visual block editor.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 67;\n\n-- Block ID 68\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#064e3b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-emerald-400 font-semibold mb-4\\\">Under The Hood</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">Built for Production Scale</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Commerce Pro was built from day one for busy storefronts. Every API path is cached near your users, and database queries use fast indexes.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">Whether you sell digital downloads or retail goods, your storefront loads fast. It handles high traffic spikes without slow downs or crashes.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Order Management</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Track customer orders from cart to shipping. Automatic status updates, buyer receipts, and refunds work right out of the box.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Product Categories</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Organize products with clean web addresses, category images, and full multi-language tags. Manage it all from the CMS dashboard.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Multi-Language Storefronts</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Translate products and category pages into any language. Each language shares inventory while keeping its own SEO metadata.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 68;\n\n-- Block ID 69\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#064e3b\",\"position\":0},{\"color\":\"#022c22\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Ready to launch your storefront?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-emerald-100 max-w-xl mx-auto mt-2 mb-6\\\">Start selling today with Commerce Pro. It is multi-currency, tax-compliant, and lightning-fast out of the box.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"#\",\"size\":\"lg\",\"text\":\"Purchase Commerce Pro\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 69;\n\n-- Block ID 70\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#1e1b4b\",\"position\":0},{\"color\":\"#0f172a\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">AI Intelligence Layer</p><h2 class=\\\"text-3xl sm:text-4xl font-extrabold text-white tracking-tight mb-4\\\">Supercharge Your Editor with AI.</h2><p class=\\\"text-base text-slate-300 leading-relaxed\\\">Cortex AI brings powerful language models directly into your editor. You can create content, edit blog drafts, and translate full pages in seconds. It writes clean block data rather than raw text, keeping your layouts stable and fast.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"rounded-2xl border border-violet-700 bg-slate-950 p-6 shadow-xl sm:p-8\\\"><h3 class=\\\"text-lg font-bold text-white mb-4\\\">OpenRouter & BYOK Architecture</h3><ul class=\\\"space-y-3 text-sm text-slate-300\\\"><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Bring Your Own Key</strong> — You control costs using your own OpenRouter tokens. There are no hidden fees or markups.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Block-Aware Prompts</strong> — Models receive structured context. They see headings, columns, and layout rules for accurate outputs.</span></li><li class=\\\"flex items-start gap-3\\\"><span class=\\\"text-violet-400 font-bold\\\">✓</span><span><strong>Model Flexibility</strong> — Switch freely between Claude, GPT, and open-source models without changing code.</span></li></ul></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 70;\n\n-- Block ID 72\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"lg\",\"bottom\":\"lg\"},\"background\":{\"type\":\"solid\",\"color\":\"transparent\"},\"column_gap\":\"lg\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">✍️</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">One-Click Copywriting</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Write blog posts, polish headlines, and craft calls to action. Cortex AI matches your brand voice and writes clear prose with ease.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🔄</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Structure Refactoring</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Reorganize layout grids, split columns, and reorder sections. Cortex AI produces valid block schemas that render safely every time.</p></div>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"h-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-colors hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900\\\"><div class=\\\"mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-xl dark:bg-violet-950\\\">🌐</div><h4 class=\\\"text-lg font-bold text-foreground mb-2\\\">Automated Translation</h4><p class=\\\"text-sm text-muted-foreground leading-relaxed\\\">Translate entire pages between languages with one click. Cortex AI keeps your styles, nested sections, and media links intact.</p></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":2,\"desktop\":3},\"vertical_alignment\":\"stretch\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 72;\n\n-- Block ID 73\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#0f172a\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"xl\",\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-xs uppercase tracking-[0.3em] text-violet-400 font-semibold mb-4\\\">How It Works</p><h3 class=\\\"text-2xl font-bold text-white mb-4\\\">AI That Understands Blocks</h3><p class=\\\"text-sm text-slate-300 leading-relaxed mb-4\\\">Most AI tools only generate flat text strings. Cortex AI is built specifically for NextBlock editors. It reads your full page structure, column arrays, and style options.</p><p class=\\\"text-sm text-slate-300 leading-relaxed\\\">This deep connection prevents broken markup and invalid data. Your team gets clean results that look great on desktop and mobile screens right away.</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class=\\\"space-y-4\\\"><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Content Generation</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Create engaging copy for marketing pages and help docs. The assistant follows your editorial style guide and produces clean text.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">SEO Optimization</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Draft concise meta titles and meta descriptions automatically. Cortex AI checks keyword density and reading ease before you publish.</p></div><div class=\\\"p-5 rounded-xl border border-slate-700 bg-slate-900\\\"><h4 class=\\\"text-sm font-bold text-white mb-1\\\">Privacy-First Design</h4><p class=\\\"text-xs text-slate-400 leading-relaxed\\\">Your content goes directly to your chosen provider via private API keys. We never store, log, or train on your private data.</p></div></div>\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 73;\n\n-- Block ID 74\nUPDATE public.blocks\nSET content = '{\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"stops\":[{\"color\":\"#312e81\",\"position\":0},{\"color\":\"#1e1b4b\",\"position\":100}],\"direction\":\"135deg\"}},\"column_gap\":\"none\",\"column_blocks\":[[{\"block_type\":\"heading\",\"content\":{\"level\":2,\"textAlign\":\"center\",\"textColor\":\"background\",\"text_content\":\"Ready to add AI to your editor?\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<p class=\\\"text-center text-violet-100 max-w-xl mx-auto mt-2 mb-6\\\">Unlock the full power of AI-driven content creation. Connect your key, protect your privacy, and edit faster today.</p>\"}},{\"block_type\":\"button\",\"content\":{\"url\":\"#\",\"size\":\"lg\",\"text\":\"Purchase Cortex AI\",\"variant\":\"secondary\",\"position\":\"center\"}}]],\"container_type\":\"container\",\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"vertical_alignment\":\"center\"}'::jsonb,\n updated_at = NOW()\nWHERE id = 74;\n\nCOMMIT;\n"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"version": "00000000000036",
|
|
199
|
+
"name": "00000000000036_seed_posts_missing_seo_metadata.sql",
|
|
200
|
+
"sql": "-- Migration: 00000000000036_seed_posts_missing_seo_metadata.sql\n-- Purpose: Set meta_title and meta_description for remaining seeded blog posts\n-- (how-nextblock-works, comment-nextblock-fonctionne, nextblock-commerce-guide, guide-commerce-nextblock)\n-- so all 9 seeded posts achieve a 100/100 SEO score.\n-- Safety: Append-only, forward-only, content-guarded updates.\n\nBEGIN;\n\nUPDATE public.posts\n SET meta_title = 'How NextBlock™ Works: Architecture & Under the Hood',\n meta_description = 'Explore the architecture behind NextBlock: monorepo packages, typed block registry, Supabase database, and modern Next.js editor stack.',\n updated_at = NOW()\n WHERE slug = 'how-nextblock-works'\n AND meta_title IS NULL;\n\nUPDATE public.posts\n SET meta_title = 'Comment fonctionne NextBlock™ : Architecture et Moteur',\n meta_description = 'Découvrez les fondations de NextBlock : monorepo, registre de blocs typés, base de données Supabase et moteur d''édition Next.js moderne.',\n updated_at = NOW()\n WHERE slug = 'comment-nextblock-fonctionne'\n AND meta_title IS NULL;\n\nUPDATE public.posts\n SET meta_title = 'The Complete NextBlock™ Commerce Guide | Headless Store',\n meta_description = 'Build global e-commerce with NextBlock: manage products, configure multi-currency pricing, and accept payments with headless Stripe checkout.',\n updated_at = NOW()\n WHERE slug = 'nextblock-commerce-guide'\n AND meta_title IS NULL;\n\nUPDATE public.posts\n SET meta_title = 'Guide Complet du Commerce avec NextBlock™ | E-Commerce',\n meta_description = 'Développez votre boutique avec NextBlock : gestion de produits, tarifs multi-devises et tunnel de paiement headless avec Stripe Checkout.',\n updated_at = NOW()\n WHERE slug = 'guide-commerce-nextblock'\n AND meta_title IS NULL;\n\nCOMMIT;\n"
|
|
191
201
|
}
|
|
192
202
|
];
|