create-nextblock 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/nextblock-template/app/actions/interactions.test.ts +301 -0
- package/templates/nextblock-template/app/actions/interactions.ts +372 -0
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +4 -4
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +2 -2
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +56 -57
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +1 -1
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +837 -0
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +6 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +4 -0
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +7 -2
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +4 -0
- package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -0
- package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +4 -3
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +3 -5
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +1 -1
- package/templates/nextblock-template/app/page.tsx +2 -2
- package/templates/nextblock-template/app/product/[slug]/page.tsx +2 -0
- package/templates/nextblock-template/components/AppShell.tsx +1 -1
- package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -0
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -0
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +2 -0
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +62 -19
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -19
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +4 -4
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +2 -0
- package/templates/nextblock-template/lib/setup/actions.ts +3 -1
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +30 -0
- package/templates/nextblock-template/lib/updates/check-upstream.ts +38 -4
- package/templates/nextblock-template/package.json +2 -1
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +2 -4
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +1 -1
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +1 -1
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +1 -1
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/lib/ai-block-generation.ts +0 -339
- package/templates/nextblock-template/lib/ai-client.ts +0 -247
- package/templates/nextblock-template/lib/ai-config.ts +0 -98
- package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +0 -125
- package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +0 -363
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +0 -405
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +0 -1228
- package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +0 -5
- package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +0 -223
- package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +0 -2183
- package/templates/nextblock-template/lib/ai-global-agent-tools.ts +0 -4807
- package/templates/nextblock-template/lib/ai-key-crypto.test.ts +0 -70
- package/templates/nextblock-template/lib/ai-key-crypto.ts +0 -132
- package/templates/nextblock-template/lib/ai-model-catalog.test.ts +0 -49
- package/templates/nextblock-template/lib/ai-model-catalog.ts +0 -41
- package/templates/nextblock-template/lib/ai-model-registry.test.ts +0 -231
- package/templates/nextblock-template/lib/ai-model-registry.ts +0 -522
- package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +0 -199
- package/templates/nextblock-template/lib/cortex-widget-registry.ts +0 -88
- package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +0 -237
- package/templates/nextblock-template/lib/cortex-widget-schema.ts +0 -393
|
@@ -57,7 +57,7 @@ Implemented:
|
|
|
57
57
|
- `fetch_ecommerce_stats`
|
|
58
58
|
- Multilingual navigation/footer tool arguments using either language codes or language names.
|
|
59
59
|
- Guardrails against OpenRouter free-model rate limits, raw tool-call leakage, and stuck loading streams.
|
|
60
|
-
- Custom-block "build widget" generation: `/api/ai/cortex/build-widget` and the custom-block agent tools (`
|
|
60
|
+
- Custom-block "build widget" generation: `/api/ai/cortex/build-widget` and the custom-block agent tools (`libs/cortex/src/lib/ai-global-agent-custom-block-tools.ts`) produce data-driven `custom_block_definitions` from a prompt. See [10-CUSTOM-BLOCKS.md](./10-CUSTOM-BLOCKS.md) for the block model.
|
|
61
61
|
|
|
62
62
|
Known incomplete or future work:
|
|
63
63
|
|
|
@@ -73,8 +73,8 @@ Known incomplete or future work:
|
|
|
73
73
|
| File | Purpose |
|
|
74
74
|
| --- | --- |
|
|
75
75
|
| `libs/utils/src/lib/nextblock-packages.ts` | Package registry. Contains `cortex-ai` metadata and Freemius product/plan ids. |
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
76
|
+
| `libs/cortex/src/lib/ai-config.ts` | Server-only Cortex AI constants and environment accessors. |
|
|
77
|
+
| `libs/cortex/src/lib/ai-key-crypto.ts` | AES-256-GCM encryption/decryption helpers for stored OpenRouter BYOK keys. |
|
|
78
78
|
| `.env.exemple` | Documents `FREEMIUS_AI_SANDBOX_KEY`, `OPENROUTER_API_KEY`, and `CORTEX_AI_ENCRYPTION_KEY`. |
|
|
79
79
|
| `libs/environment.d.ts` | Type declarations for Cortex AI environment variables. |
|
|
80
80
|
|
|
@@ -90,9 +90,9 @@ Known incomplete or future work:
|
|
|
90
90
|
|
|
91
91
|
| File | Purpose |
|
|
92
92
|
| --- | --- |
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
93
|
+
| `libs/cortex/src/lib/ai-client.ts` | Creates OpenRouter provider/client with credential resolution and text-generation helper. |
|
|
94
|
+
| `libs/cortex/src/lib/ai-model-catalog.ts` | Server-only OpenRouter model catalog fetcher. |
|
|
95
|
+
| `libs/cortex/src/lib/ai-model-registry.ts` | Free model registry, routing policy builder, model filtering/parsing helpers, rate-limit detection, fallback runner. |
|
|
96
96
|
| `apps/nextblock/scripts/verify-cortex-ai-routing.ts` | Manual verification script for OpenRouter routing. |
|
|
97
97
|
|
|
98
98
|
### Inline Editor Assistance
|
|
@@ -101,7 +101,7 @@ Known incomplete or future work:
|
|
|
101
101
|
| --- | --- |
|
|
102
102
|
| `libs/utils/src/lib/editor-blocks.ts` | Main Tiptap JSON Zod schemas, allowed node/mark types, JSON Schema extraction. Still used for product descriptions and agent validation. |
|
|
103
103
|
| `schemas/editor-blocks.ts` | Re-export shim for schema imports from app scripts/lib code. |
|
|
104
|
-
| `
|
|
104
|
+
| `libs/cortex/src/lib/ai-block-generation.ts` | Inline editor HTML-fragment generation using `generateText`, routing fallback, and lightweight output validation. |
|
|
105
105
|
| `apps/nextblock/app/api/ai/generate-blocks/route.ts` | Compatibility route for inline editor generation. Returns `{ html, credentialSource, modelId }`. |
|
|
106
106
|
| `libs/editor/src/lib/NotionEditor.tsx` | Editor prompt UI and HTML insertion behavior via normal Tiptap parsing. |
|
|
107
107
|
| `apps/nextblock/scripts/validate-editor-block-schema.ts` | Validates editor schema against sample content and emits diagnostics. |
|
|
@@ -111,11 +111,11 @@ Known incomplete or future work:
|
|
|
111
111
|
|
|
112
112
|
| File | Purpose |
|
|
113
113
|
| --- | --- |
|
|
114
|
-
| `
|
|
114
|
+
| `libs/cortex/src/lib/ai-global-agent-tools.ts` | Tool schemas and execution functions. |
|
|
115
115
|
| `apps/nextblock/app/api/ai/global-agent/route.ts` | Global agent route and SSE streaming orchestration. |
|
|
116
116
|
| `apps/nextblock/app/cms/components/CortexGlobalAgentChat.tsx` | Persistent dashboard chat UI with thread history. |
|
|
117
117
|
| `apps/nextblock/app/cms/components/CortexAiPageContext.tsx` | Client page-context provider/registrar used by CMS edit screens and the global chat. |
|
|
118
|
-
| `
|
|
118
|
+
| `libs/cortex/src/lib/ai-global-agent-tools.test.ts` | Unit tests for tool executors. |
|
|
119
119
|
| `apps/nextblock/scripts/verify-cortex-ai-global-tools.ts` | Focused verifier for global tools. |
|
|
120
120
|
|
|
121
121
|
### CMS Integration
|
|
@@ -211,7 +211,7 @@ Required only for saving/decrypting DB-stored BYOK keys.
|
|
|
211
211
|
|
|
212
212
|
Implementation detail:
|
|
213
213
|
|
|
214
|
-
- `
|
|
214
|
+
- `libs/cortex/src/lib/ai-key-crypto.ts` hashes this secret with SHA-256 to derive a 32-byte AES key.
|
|
215
215
|
- Stored keys use AES-256-GCM with a 12-byte random IV and auth tag.
|
|
216
216
|
- Changing this value invalidates previously encrypted stored keys.
|
|
217
217
|
|
|
@@ -284,7 +284,7 @@ Settings UI behavior:
|
|
|
284
284
|
|
|
285
285
|
## OpenRouter Client Architecture
|
|
286
286
|
|
|
287
|
-
The OpenRouter client is implemented in `
|
|
287
|
+
The OpenRouter client is implemented in `libs/cortex/src/lib/ai-client.ts`.
|
|
288
288
|
|
|
289
289
|
It uses:
|
|
290
290
|
|
|
@@ -328,7 +328,7 @@ This prevents accidental client-side exposure of secrets.
|
|
|
328
328
|
|
|
329
329
|
## Model Registry and Fallback
|
|
330
330
|
|
|
331
|
-
The model registry lives in `
|
|
331
|
+
The model registry lives in `libs/cortex/src/lib/ai-model-registry.ts`.
|
|
332
332
|
|
|
333
333
|
Default free router constant:
|
|
334
334
|
|
|
@@ -356,7 +356,7 @@ Both registries intentionally use the same model list. The inline editor no long
|
|
|
356
356
|
Paid model selection:
|
|
357
357
|
|
|
358
358
|
- `CORTEX_AI_REQUIRED_MODEL_PARAMETERS = ['tools', 'structured_outputs']`.
|
|
359
|
-
- `
|
|
359
|
+
- `libs/cortex/src/lib/ai-model-catalog.ts` fetches `https://openrouter.ai/api/v1/models?supported_parameters=tools,structured_outputs&output_modalities=text`.
|
|
360
360
|
- Catalog filtering keeps only non-expired text-output models that advertise all required parameters.
|
|
361
361
|
- `buildCortexAiRoutingPolicy` is the single policy entrypoint for inline editor generation, global agent routing, and shared text generation.
|
|
362
362
|
- Env-key routing always returns exactly the free fallback registry.
|
|
@@ -524,7 +524,7 @@ Response:
|
|
|
524
524
|
Generator:
|
|
525
525
|
|
|
526
526
|
```txt
|
|
527
|
-
|
|
527
|
+
libs/cortex/src/lib/ai-block-generation.ts
|
|
528
528
|
```
|
|
529
529
|
|
|
530
530
|
Prompt persona:
|
|
@@ -583,7 +583,7 @@ The global dashboard agent has two main pieces:
|
|
|
583
583
|
File:
|
|
584
584
|
|
|
585
585
|
```txt
|
|
586
|
-
|
|
586
|
+
libs/cortex/src/lib/ai-global-agent-tools.ts
|
|
587
587
|
```
|
|
588
588
|
|
|
589
589
|
Exported tool schemas:
|
|
@@ -1103,10 +1103,10 @@ npx nx build nextblock --skip-nx-cache
|
|
|
1103
1103
|
Vitest files:
|
|
1104
1104
|
|
|
1105
1105
|
```txt
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1106
|
+
libs/cortex/src/lib/ai-key-crypto.test.ts
|
|
1107
|
+
libs/cortex/src/lib/ai-model-catalog.test.ts
|
|
1108
|
+
libs/cortex/src/lib/ai-model-registry.test.ts
|
|
1109
|
+
libs/cortex/src/lib/ai-global-agent-tools.test.ts
|
|
1110
1110
|
```
|
|
1111
1111
|
|
|
1112
1112
|
Notes:
|
|
@@ -179,8 +179,8 @@ surfaced at `/cms/import-export` via `ContentTransferControls.tsx`.
|
|
|
179
179
|
Cortex AI can generate a custom block definition from a prompt:
|
|
180
180
|
|
|
181
181
|
- Route: `apps/nextblock/app/api/ai/cortex/build-widget/route.ts`.
|
|
182
|
-
- Helpers: `
|
|
183
|
-
agent tools in `
|
|
182
|
+
- Helpers: `libs/cortex/src/lib/cortex-widget-registry.ts` and the custom-block
|
|
183
|
+
agent tools in `libs/cortex/src/lib/ai-global-agent-custom-block-tools.ts`.
|
|
184
184
|
- After a Cortex-driven change, the front end dispatches a
|
|
185
185
|
`nextblock:cortex-data-changed` event; the custom-blocks list listens for it
|
|
186
186
|
(and for window focus) and refetches so the library stays in sync without a
|
|
@@ -205,8 +205,8 @@ npm run verify:cortex-ai-build-widget
|
|
|
205
205
|
Relevant Vitest files:
|
|
206
206
|
|
|
207
207
|
- `apps/nextblock/components/renderers/DynamicLayoutEngine.test.tsx`
|
|
208
|
-
- `
|
|
209
|
-
- `
|
|
208
|
+
- `libs/cortex/src/lib/cortex-widget-registry.test.ts`
|
|
209
|
+
- `libs/cortex/src/lib/cortex-widget-schema.test.tsx`
|
|
210
210
|
|
|
211
211
|
## Notes for Contributors
|
|
212
212
|
|
|
@@ -445,7 +445,9 @@ export async function completeSetup(input: CompleteSetupInput): Promise<ActionRe
|
|
|
445
445
|
.eq('id', created.user.id)
|
|
446
446
|
.maybeSingle();
|
|
447
447
|
if (createdProfile?.role !== 'ADMIN') {
|
|
448
|
-
await admin.auth.admin.deleteUser(created.user.id).catch(() => {
|
|
448
|
+
await admin.auth.admin.deleteUser(created.user.id).catch((err) => {
|
|
449
|
+
console.warn('Failed to clean up stale admin user after race condition:', err);
|
|
450
|
+
});
|
|
449
451
|
return {
|
|
450
452
|
ok: false,
|
|
451
453
|
error: 'Setup was just completed by another session. Please sign in instead.',
|
|
@@ -198,5 +198,35 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
|
|
|
198
198
|
"version": "00000000000036",
|
|
199
199
|
"name": "00000000000036_setup_system_alerts.sql",
|
|
200
200
|
"sql": "-- System notification layer for the automated upstream-update architecture.\n--\n-- `system_alerts` is the single sink that every update track writes into:\n-- * Track A (the .github/workflows/nextblock-sync.yml GitHub Action) inserts a\n-- 'merge_conflict' row via the Supabase REST API when an upstream merge can't be\n-- auto-resolved, so the CMS dashboard can point an operator at GitHub to sort it.\n-- * Track B (the runtime update engine, app/api/cms/check-updates) inserts a\n-- 'runtime_update_available' row for non-git installs (npm create / local / Docker)\n-- with a download link to the latest verified release tarball.\n-- The dashboard banner (cms/layout.tsx -> SystemAlertsBanner) renders unresolved rows.\n--\n-- Writers always use the service-role key (REST API / getServiceRoleSupabaseClient),\n-- which bypasses RLS; RLS below only governs who can READ/RESOLVE from the dashboard.\n\ncreate table if not exists public.system_alerts (\n id uuid primary key default gen_random_uuid(),\n -- The notification kind. Constrained to the two tracks this system emits; widen the\n -- CHECK in a later migration if new alert kinds are added.\n alert_type text not null check (alert_type in ('merge_conflict', 'runtime_update_available')),\n title text not null,\n message text not null,\n -- Structured context for deep-linking the banner CTA, e.g.\n -- merge_conflict -> { \"repo\": \"owner/name\", \"branch\": \"...\", \"action_url\": \"https://github.com/owner/name/branches\" }\n -- runtime_update_available -> { \"latest_version\": \"0.11.0\", \"download_url\": \"https://github.com/.../v0.11.0.tar.gz\" }\n metadata jsonb not null default '{}'::jsonb,\n is_resolved boolean not null default false,\n resolved_at timestamptz,\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n);\n\ncomment on table public.system_alerts is 'System notifications for the automated upstream-update architecture (merge conflicts, runtime updates available). Written by service-role; read by ADMINs.';\ncomment on column public.system_alerts.alert_type is 'One of: merge_conflict, runtime_update_available.';\ncomment on column public.system_alerts.metadata is 'Structured deep-link context for the dashboard banner CTA (repo/branch/action_url or latest_version/download_url).';\ncomment on column public.system_alerts.is_resolved is 'When true the alert is hidden from the dashboard banner.';\n\n-- Banner query: unresolved alerts, newest first.\ncreate index if not exists idx_system_alerts_unresolved\n on public.system_alerts (is_resolved, created_at desc);\n\n-- Keep updated_at fresh on every mutation (same helper used across the schema).\ndrop trigger if exists trg_system_alerts_updated_at on public.system_alerts;\ncreate trigger trg_system_alerts_updated_at\n before update on public.system_alerts\n for each row\n execute function public.set_current_timestamp_updated_at();\n\nalter table public.system_alerts enable row level security;\n\n-- Only authenticated ADMINs may view alerts in the dashboard. WRITERs and the public\n-- anon role get zero rows (RLS default-deny: no policy applies to them).\ndrop policy if exists system_alerts_select_admin on public.system_alerts;\ncreate policy system_alerts_select_admin\n on public.system_alerts\n for select\n to authenticated\n using ((select public.get_current_user_role()) = 'ADMIN');\n\n-- ADMINs may resolve (dismiss) alerts from the dashboard. Inserts are service-role only\n-- (RLS-bypassing), so there is intentionally no INSERT policy.\ndrop policy if exists system_alerts_update_admin on public.system_alerts;\ncreate policy system_alerts_update_admin\n on public.system_alerts\n for update\n to authenticated\n using ((select public.get_current_user_role()) = 'ADMIN')\n with check ((select public.get_current_user_role()) = 'ADMIN');\n\n-- Base table privileges (RLS still filters rows on top of these). Mirrors the grant\n-- model the rest of the schema uses; service_role keeps full access for the writers.\ngrant select, update on public.system_alerts to authenticated;\ngrant all on public.system_alerts to service_role;\n"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"version": "00000000000037",
|
|
204
|
+
"name": "00000000000037_refresh_setup_article_install_paths.sql",
|
|
205
|
+
"sql": "-- Rewrite the seeded \"How to Setup NextBlock\" tutorial (EN + FR) around the four\n-- current installation paths:\n-- 1. One-click Deploy on Vercel (Supabase Marketplace integration, zero env vars),\n-- 2. npm create nextblock (standalone app, browser /setup wizard on :3000),\n-- 3. git clone -> npm install -> npx nx serve nextblock (browser /setup wizard on :4200),\n-- 4. git clone -> npm install -> npm run docker:setup (fully local, non-interactive).\n--\n-- Also sets the posts' meta_title / meta_description (previously unset — SEO title fell\n-- back to posts.title and the description to posts.subtitle) and refreshes\n-- title / subtitle / excerpt for both languages. Slugs are intentionally unchanged:\n-- 'how-to-setup-nextblock' and 'comment-configurer-nextblock' are public URLs and are\n-- also mapped to the tutorial presentation (post-article--tutorial) by slug.\n--\n-- Forward-only and idempotent: it updates the two posts rows and replaces their single\n-- text block (same pattern as 00000000000029). Safe to re-run; a no-op if the posts\n-- do not exist.\n\n-- EN post metadata\nUPDATE public.posts\nSET\n title = 'How to Install NextBlock: Every Setup Option Explained',\n subtitle = 'Four ways to launch NextBlock: a one-click Vercel deploy, npm create nextblock, git clone with the browser setup wizard, or a fully local Docker stack.',\n excerpt = 'Every way to install NextBlock — one-click cloud deploy, CLI scaffold, git clone, or self-hosted Docker — with copy-paste steps for each.',\n meta_title = 'How to Install NextBlock CMS — Vercel, CLI, Git or Docker',\n meta_description = 'Install NextBlock in minutes — one-click Vercel deploy, npm create nextblock, git clone, or self-hosted Docker. No config files, no manual SQL.'\nWHERE slug = 'how-to-setup-nextblock';\n\n-- FR post metadata\nUPDATE public.posts\nSET\n title = $meta$Installer NextBlock : toutes les options expliquées$meta$,\n subtitle = $meta$Quatre façons de lancer NextBlock : déploiement Vercel en un clic, npm create nextblock, git clone avec l'assistant dans le navigateur, ou une pile Docker 100 % locale.$meta$,\n excerpt = $meta$Toutes les façons d'installer NextBlock — cloud en un clic, CLI, git clone ou Docker auto-hébergé — avec les étapes à copier-coller.$meta$,\n meta_title = $meta$Installer NextBlock — Vercel, CLI, Git ou Docker$meta$,\n meta_description = $meta$Installez NextBlock en quelques minutes : déploiement Vercel en un clic, npm create nextblock, git clone ou Docker auto-hébergé. Sans config ni SQL.$meta$\nWHERE slug = 'comment-configurer-nextblock';\n\nWITH target_posts AS (\n SELECT id, language_id, slug\n FROM public.posts\n WHERE slug IN ('how-to-setup-nextblock', 'comment-configurer-nextblock')\n),\npurged AS (\n DELETE FROM public.blocks\n WHERE post_id IN (SELECT id FROM target_posts)\n)\nINSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n\n-- EN: How to Install NextBlock\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock is an open-source, AI-native Next.js CMS built on Supabase — and installing it no longer involves config files, terminal wizards, or manual SQL. There are four ways to get running, and they all end in the same place: a browser <strong>setup wizard</strong> that connects your database, configures media storage, and creates your admin account for you. Pick the path that fits, follow the steps, and you will be publishing in minutes.</p>\n\n<div class='grid gap-5 md:grid-cols-2 my-10'>\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Fastest · about 3 minutes</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>One-click deploy on Vercel</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A live production site with a managed database. No terminal, no environment variables, nothing to copy.</p>\n </a>\n <a href='#npm-create' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Recommended for new projects</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Scaffold a standalone Next.js app with NextBlock built in, then finish setup in your browser.</p>\n </a>\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Full source code</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Clone the repository</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Run the complete monorepo — the CMS, every package, and the docs. Made for contributors and platform teams.</p>\n </a>\n <a href='#docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>100% local · no accounts</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Self-hosted with Docker</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>One command boots the entire stack on your machine — database, auth, storage, and the CMS. No cloud services at all.</p>\n </a>\n</div>\n\n<p class='text-sm text-slate-500 dark:text-slate-400'>Not sure which to pick? If you want a live website with the least effort, choose <a href='#one-click-vercel'>Vercel</a>. If you want a local project to build on, choose <a href='#npm-create'>npm create nextblock</a>.</p>\n\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\n <img src='/images/included.webp' alt='NextBlock CMS platform overview showing the block editor, CMS dashboard, and integrations included with every installation' class='w-full h-auto object-cover' />\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Whichever path you choose, you get the same block editor, the same CMS, and the same database schema.</figcaption>\n</figure>\n\n<h2 id='one-click-vercel'>Option 1: One-Click Deploy on Vercel</h2>\n<p>The fastest way to get a production NextBlock site. One button creates your own copy of NextBlock on GitHub, provisions a managed Supabase database, and deploys the site — you never open a terminal or copy a single key.</p>\n<ol class='space-y-2'>\n <li><strong>Click Deploy to Vercel</strong> and sign in — Vercel clones NextBlock into a new repository you own.</li>\n <li><strong>Create the Supabase database</strong> when prompted: pick a name and a region. Vercel connects it to the project and injects the keys before the first build.</li>\n <li><strong>Open your new site</strong> once the build finishes. Every fresh instance takes you straight to the setup wizard.</li>\n <li><strong>Create your administrator account.</strong> It is confirmed instantly — no verification email — and you land in the CMS dashboard.</li>\n</ol>\n<div class='my-8'>\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy to Vercel →</a>\n</div>\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zero configuration</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>There are no environment variables to fill in. Media storage automatically uses your connected Supabase project, security secrets are derived for you, and database migrations run automatically on every production build. Adding a custom domain later? Set <code>NEXT_PUBLIC_URL</code> in your Vercel project and redeploy.</p>\n</div>\n<p>Your site also keeps itself current: the dashboard checklist includes a one-click <strong>Connect GitHub</strong> step that installs a daily workflow syncing your copy with the latest NextBlock release.</p>\n\n<h2 id='npm-create'>Option 2: Scaffold a Project with npm create nextblock</h2>\n<p>The best starting point for building your own site. The CLI scaffolds a standalone Next.js application with NextBlock already wired in — no monorepo, no workspace tooling — and hands everything else to the browser wizard.</p>\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a> (it includes npm), then create a free project at <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a> (or pick the CLI's Docker mode during creation and skip cloud accounts entirely).</p>\n<pre><code>npm create nextblock@latest my-site\ncd my-site\nnpm run dev</code></pre>\n<p>Open <code>http://localhost:3000/setup</code> and let the wizard take over:</p>\n<ol class='space-y-2'>\n <li><strong>Connect Supabase</strong> — paste your project URL, publishable (anon) key, secret (service role) key, and a personal access token so the wizard can apply the database schema for you.</li>\n <li><strong>Choose media storage</strong> — plug in a Cloudflare R2 bucket for images and files, or leave it blank to use your connected Supabase project's storage.</li>\n <li><strong>Create your administrator</strong> — the wizard applies every migration, generates the app secrets, writes <code>.env.local</code>, creates your confirmed admin account, and signs you in. Restart <code>npm run dev</code> once afterwards so the fresh environment is baked into the app.</li>\n</ol>\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Premium modules</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Need a store? One command adds products, checkout, orders, and coupons — license-gated and ready when you are: <code>npx create-nextblock activate ecommerce</code></p>\n</div>\n\n<h2 id='git-clone'>Option 3: Clone the Repository</h2>\n<p>Run the full Nx monorepo: the CMS application, every shared package, the CLI source, and the documentation. This is the path for contributors, plugin authors, and teams that customize the platform itself.</p>\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\ncd nextblock\nnpm install\nnpx nx serve nextblock</code></pre>\n<p>Open <code>http://localhost:4200</code> — a fresh install redirects every page to <code>/setup</code>, where the same three-step wizard connects Supabase, configures storage, and creates your admin. It validates your keys, writes <code>.env.local</code> with generated secrets, and applies all migrations over the Supabase Management API — no Supabase CLI required.</p>\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>No terminal setup</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Configuration moved entirely to the browser — there is no interactive terminal step anymore. When the wizard finishes you are signed in as the administrator; restart the dev server once afterwards so the fresh environment is baked into the app bundle.</p>\n</div>\n\n<h2 id='docker'>Option 4: Self-Hosted with Docker</h2>\n<p>Everything runs on your machine: Supabase's Postgres and auth engines, a PostgREST API behind a Kong gateway, S3-compatible MinIO storage, and the CMS itself. No Supabase account, no Vercel, no email service — ideal for evaluations, air-gapped environments, and anyone who wants a fully self-hosted CMS with complete data ownership.</p>\n<p>With <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a> installed and running:</p>\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\ncd nextblock\nnpm install\nnpm run docker:setup</code></pre>\n<p>The command asks nothing: it generates secure keys, builds the stack, applies every migration, and starts the services. When it finishes, open <code>http://localhost:3000</code> — the setup wizard already has the database and MinIO storage wired up, so the only step left is creating your administrator (confirmed instantly, no email required).</p>\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Day-2 commands</p>\n <pre class='mt-4 mb-0'><code># rebuild and restart the stack\nnpm run docker:up\n\n# stop the stack (your data persists in Docker volumes)\nnpm run docker:down\n\n# follow the application logs\nnpm run docker:logs</code></pre>\n</div>\n\n<h2 id='after-install'>After You Install: Your First 10 Minutes</h2>\n<p>Every path drops you at <code>/cms/dashboard</code>, signed in as the first administrator. A built-in onboarding checklist walks you through the rest:</p>\n<ul class='space-y-2'>\n <li><strong>Add your branding</strong> — upload your logo and set the site title.</li>\n <li><strong>Set your footer</strong> — copyright line and footer navigation.</li>\n <li><strong>Configure email (SMTP)</strong> — under Settings, so password resets and invitations can send.</li>\n <li><strong>Optional extras</strong> — connect analytics, enable bot protection, and (on Vercel) turn on automatic updates.</li>\n</ul>\n<p>From there, see how the platform fits together in <a href='/article/how-nextblock-works'>How NextBlock Works</a>, add a storefront with the <a href='/article/nextblock-commerce-guide'>Commerce guide</a>, or meet your AI copilot in the <a href='/article/nextblock-cortex-ai-guide'>Cortex AI guide</a>.</p>\n\n<h2 id='faq'>Installation FAQ</h2>\n<h3>What do I need installed?</h3>\n<p>Nothing for the Vercel path — it runs entirely in the browser. For <code>npm create nextblock</code>: <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> (which includes npm). For the cloned repository: Node.js 20+ and git. For Docker: those plus Docker Desktop.</p>\n<h3>Is NextBlock free?</h3>\n<p>Yes — the core of NextBlock is a 100% free, open-source CMS (AGPL). Premium packages such as e-commerce and Cortex AI are optional and activate with a license key. Both Vercel and Supabase offer free tiers, so a starter site can run at no cost.</p>\n<h3>Do I need a Supabase account?</h3>\n<p>On Vercel, the database is created for you during the deploy. For <code>npm create nextblock</code> and the cloned repository you bring a free Supabase project. With Docker you need no cloud accounts at all.</p>\n<h3>Do I have to run migrations or SQL by hand?</h3>\n<p>No. The setup wizard, the Vercel build, and the Docker stack all apply the database schema automatically — and re-running is always safe.</p>\n<h3>Can I switch paths later?</h3>\n<p>Yes. Every path runs the same application and the same database schema, so you can prototype locally with Docker today and deploy to Vercel tomorrow. NextBlock deploys like any standard Next.js app.</p>\n<h3>How do I update NextBlock?</h3>\n<p>On Vercel, the Connect GitHub onboarding step enables a daily automatic sync with upstream. On a cloned repository, <code>git pull</code>, run <code>npm run db:migrate</code>, then restart (on production builds, pending migrations apply automatically). With Docker, pull the latest code and run <code>npm run docker:up</code>.</p>\n\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Ready to launch?</p>\n <p class='text-sm text-slate-600 dark:text-slate-300'>Pick your path above, or jump straight to the fastest one.</p>\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy on Vercel</a>\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\n </div>\n</div>$$\n), 0 FROM target_posts tp WHERE tp.slug = 'how-to-setup-nextblock'\n\nUNION ALL\n\n-- FR: Installer NextBlock\nSELECT tp.id, tp.language_id, 'text', jsonb_build_object('html_content',\n$$<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Il existe quatre façons de démarrer, et elles aboutissent toutes au même endroit : un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur. Choisissez le chemin qui vous convient, suivez les étapes, et vous publierez en quelques minutes.</p>\n\n<div class='grid gap-5 md:grid-cols-2 my-10'>\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Le plus rapide · environ 3 minutes</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Déploiement Vercel en un clic</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un site de production en ligne avec une base de données gérée. Pas de terminal, pas de variables d'environnement, rien à copier.</p>\n </a>\n <a href='#npm-create' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Recommandé pour les nouveaux projets</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Générez une app Next.js autonome avec NextBlock intégré, puis terminez la configuration dans votre navigateur.</p>\n </a>\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Code source complet</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Cloner le dépôt</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Conçu pour les contributeurs et les équipes plateforme.</p>\n </a>\n <a href='#docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>100 % local · aucun compte</p>\n <h3 class='mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Auto-hébergé avec Docker</h3>\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Une seule commande démarre toute la pile sur votre machine — base de données, auth, stockage et le CMS. Aucun service cloud.</p>\n </a>\n</div>\n\n<p class='text-sm text-slate-500 dark:text-slate-400'>Vous hésitez ? Pour un site en ligne avec le minimum d'effort, choisissez <a href='#one-click-vercel'>Vercel</a>. Pour un projet local à personnaliser, choisissez <a href='#npm-create'>npm create nextblock</a>.</p>\n\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\n <img src='/images/included.webp' alt='Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation' class='w-full h-auto object-cover' />\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\n</figure>\n\n<h2 id='one-click-vercel'>Option 1 : Déploiement Vercel en un clic</h2>\n<p>Le moyen le plus rapide d'obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\n<ol class='space-y-2'>\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l'assistant de configuration.</li>\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\n</ol>\n<div class='my-8'>\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel →</a>\n</div>\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zéro configuration</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Aucune variable d'environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s'exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\n</div>\n<p>Par ailleurs, votre site reste à jour tout seul : la checklist du tableau de bord inclut une étape <strong>Connect GitHub</strong> en un clic qui installe un workflow quotidien synchronisant votre copie avec la dernière version de NextBlock.</p>\n\n<h2 id='npm-create'>Option 2 : Créer un projet avec npm create nextblock</h2>\n<p>Le meilleur point de départ pour construire votre propre site. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — et confie tout le reste à l'assistant dans le navigateur.</p>\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a> (npm inclus), puis créez un projet gratuit sur <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a> (ou choisissez le mode Docker du CLI pendant la création et passez-vous entièrement de comptes cloud).</p>\n<pre><code>npm create nextblock@latest mon-site\ncd mon-site\nnpm run dev</code></pre>\n<p>Ouvrez <code>http://localhost:3000/setup</code> et laissez l'assistant faire le travail :</p>\n<ol class='space-y-2'>\n <li><strong>Connectez Supabase</strong> — collez l'URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d'accès personnel pour que l'assistant applique le schéma de base de données à votre place.</li>\n <li><strong>Choisissez le stockage des médias</strong> — branchez un bucket Cloudflare R2 pour les images et les fichiers, ou laissez les champs vides pour utiliser le stockage de votre projet Supabase.</li>\n <li><strong>Créez votre administrateur</strong> — l'assistant applique toutes les migrations, génère les secrets de l'application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l'application.</li>\n</ol>\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Modules premium</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Besoin d'une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l'êtes : <code>npx create-nextblock activate ecommerce</code></p>\n</div>\n\n<h2 id='git-clone'>Option 3 : Cloner le dépôt</h2>\n<p>Faites tourner le monorepo Nx complet : l'application CMS, tous les packages partagés, le code du CLI et la documentation. C'est le chemin des contributeurs, des auteurs de plugins et des équipes qui personnalisent la plateforme elle-même.</p>\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\ncd nextblock\nnpm install\nnpx nx serve nextblock</code></pre>\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l'API de management Supabase — sans CLI Supabase.</p>\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Zéro configuration dans le terminal</p>\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>La configuration se fait entièrement dans le navigateur — il n'y a plus d'étape interactive en ligne de commande. Quand l'assistant termine, vous êtes connecté en administrateur ; redémarrez ensuite le serveur de développement une fois pour que le nouvel environnement soit intégré au bundle de l'application.</p>\n</div>\n\n<h2 id='docker'>Option 4 : Auto-hébergé avec Docker</h2>\n<p>Tout tourne sur votre machine : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même. Pas de compte Supabase, pas de Vercel, pas de service d'email — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\n<p>Avec <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a> installé et démarré :</p>\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\ncd nextblock\nnpm install\nnpm run docker:setup</code></pre>\n<p>La commande ne pose aucune question : elle génère des clés sécurisées, construit la pile, applique toutes les migrations et démarre les services. Quand elle se termine, ouvrez <code>http://localhost:3000</code> — la base de données et le stockage MinIO sont déjà connectés dans l'assistant de configuration, il ne reste qu'à créer votre administrateur (confirmé instantanément, sans email).</p>\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Commandes du quotidien</p>\n <pre class='mt-4 mb-0'><code># reconstruire et redémarrer la pile\nnpm run docker:up\n\n# arrêter la pile (vos données persistent dans les volumes Docker)\nnpm run docker:down\n\n# suivre les logs de l'application\nnpm run docker:logs</code></pre>\n</div>\n\n<h2 id='after-install'>Après l'installation : vos 10 premières minutes</h2>\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\n<ul class='space-y-2'>\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\n <li><strong>Configurez l'email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\n <li><strong>Extras optionnels</strong> — connectez vos outils d'analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\n</ul>\n<p>Ensuite, découvrez comment la plateforme s'articule dans <a href='/article/comment-nextblock-fonctionne'>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href='/article/guide-commerce-nextblock'>guide Commerce</a>.</p>\n\n<h2 id='faq'>FAQ d'installation</h2>\n<h3>Que dois-je installer ?</h3>\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> : <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> (npm inclus). Pour le dépôt cloné : Node.js 20+ et git. Pour Docker : ajoutez Docker Desktop.</p>\n<h3>NextBlock est-il gratuit ?</h3>\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l'e-commerce et Cortex AI sont optionnels et s'activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\n<h3>Ai-je besoin d'un compte Supabase ?</h3>\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker, aucun compte cloud n'est nécessaire.</p>\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\n<p>Non. L'assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l'opération est toujours sans risque.</p>\n<h3>Puis-je changer de chemin plus tard ?</h3>\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd'hui et déployer sur Vercel demain. NextBlock se déploie comme n'importe quelle app Next.js.</p>\n<h3>Comment mettre à jour NextBlock ?</h3>\n<p>Sur Vercel, l'étape Connect GitHub de la checklist active une synchronisation quotidienne automatique. Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s'appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\n\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Prêt à vous lancer ?</p>\n <p class='text-sm text-slate-600 dark:text-slate-300'>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel</a>\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\n </div>\n</div>$$\n), 0 FROM target_posts tp WHERE tp.slug = 'comment-configurer-nextblock';\n"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"version": "00000000000038",
|
|
209
|
+
"name": "00000000000038_seed_consent_banner_translations.sql",
|
|
210
|
+
"sql": "-- 00000000000038_seed_consent_banner_translations.sql\n-- Consent banner UI copy (EN + FR). Forward-only and idempotent.\n\nINSERT INTO public.translations (key, translations)\nVALUES\n (\n 'privacy.consent.aria_label',\n jsonb_build_object(\n 'en', 'Privacy consent',\n 'fr', 'Consentement à la confidentialité'\n )\n ),\n (\n 'privacy.consent.title',\n jsonb_build_object(\n 'en', 'We value your privacy',\n 'fr', 'Nous respectons votre vie privée'\n )\n ),\n (\n 'privacy.consent.description_before_policy_link',\n jsonb_build_object(\n 'en', 'We use only essential cookies by default. With your consent we also use analytics to improve the site. See our',\n 'fr', 'Nous utilisons seulement les témoins essentiels par défaut. Avec votre consentement, nous utilisons aussi l''analytique pour améliorer le site. Consultez notre'\n )\n ),\n (\n 'privacy.consent.privacy_policy_link',\n jsonb_build_object(\n 'en', 'Privacy Policy',\n 'fr', 'politique de confidentialité'\n )\n ),\n (\n 'privacy.consent.description_after_policy_link',\n jsonb_build_object(\n 'en', '.',\n 'fr', '.'\n )\n ),\n (\n 'privacy.consent.privacy_policy_href',\n jsonb_build_object(\n 'en', '/privacy-policy',\n 'fr', '/politique-de-confidentialite'\n )\n ),\n (\n 'privacy.consent.necessary_label',\n jsonb_build_object(\n 'en', 'Necessary',\n 'fr', 'Nécessaires'\n )\n ),\n (\n 'privacy.consent.necessary_help',\n jsonb_build_object(\n 'en', 'Always on',\n 'fr', 'Toujours actifs'\n )\n ),\n (\n 'privacy.consent.analytics_label',\n jsonb_build_object(\n 'en', 'Analytics',\n 'fr', 'Analytique'\n )\n ),\n (\n 'privacy.consent.analytics_help',\n jsonb_build_object(\n 'en', 'Usage insights',\n 'fr', 'Statistiques d''utilisation'\n )\n ),\n (\n 'privacy.consent.marketing_label',\n jsonb_build_object(\n 'en', 'Marketing',\n 'fr', 'Marketing'\n )\n ),\n (\n 'privacy.consent.marketing_help',\n jsonb_build_object(\n 'en', 'Personalized content',\n 'fr', 'Contenu personnalisé'\n )\n ),\n (\n 'privacy.consent.save_choices',\n jsonb_build_object(\n 'en', 'Save choices',\n 'fr', 'Enregistrer mes choix'\n )\n ),\n (\n 'privacy.consent.accept_all',\n jsonb_build_object(\n 'en', 'Accept all',\n 'fr', 'Tout accepter'\n )\n ),\n (\n 'privacy.consent.reject_all',\n jsonb_build_object(\n 'en', 'Reject all',\n 'fr', 'Tout refuser'\n )\n ),\n (\n 'privacy.consent.hide_options',\n jsonb_build_object(\n 'en', 'Hide options',\n 'fr', 'Masquer les options'\n )\n ),\n (\n 'privacy.consent.manage_options',\n jsonb_build_object(\n 'en', 'Manage options',\n 'fr', 'Gérer les options'\n )\n )\nON CONFLICT (key) DO UPDATE\nSET translations = public.translations.translations || EXCLUDED.translations;\n"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"version": "00000000000039",
|
|
214
|
+
"name": "00000000000039_setup_interactions.sql",
|
|
215
|
+
"sql": "-- 00000000000039_setup_interactions.sql\n-- Migration establishing Product Reviews and Post Comments schema.\n\n-- 1. Create Enums\nCREATE TYPE public.interaction_type AS ENUM ('review', 'comment');\nCREATE TYPE public.approval_status AS ENUM ('pending', 'approved', 'denied');\n\n-- 2. Alter Products Table to support aggregations\nALTER TABLE public.products \n ADD COLUMN average_rating numeric(3,2) NOT NULL DEFAULT 0.00,\n ADD COLUMN total_reviews integer NOT NULL DEFAULT 0;\n\nCOMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rating of approved reviews.';\nCOMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';\n\n-- 3. Create Interactions Table\nCREATE TABLE public.cms_interactions (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n type public.interaction_type NOT NULL,\n status public.approval_status NOT NULL DEFAULT 'pending',\n content text NOT NULL,\n rating integer,\n user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,\n product_id uuid REFERENCES public.products(id) ON DELETE CASCADE,\n post_id bigint REFERENCES public.posts(id) ON DELETE CASCADE,\n reactions jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n\n -- Ensure polymorphism is respected (either product or post)\n CONSTRAINT check_product_or_post CHECK (\n (product_id IS NOT NULL AND post_id IS NULL)\n OR (post_id IS NOT NULL AND product_id IS NULL)\n ),\n\n -- Ensure rating matches business rules\n CONSTRAINT check_rating_only_for_review CHECK (\n (type = 'review' AND rating IS NOT NULL AND rating >= 1 AND rating <= 5)\n OR (type = 'comment' AND rating IS NULL)\n )\n);\n\nCOMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';\nCOMMENT ON COLUMN public.cms_interactions.rating IS 'Star rating 1-5, only populated for product reviews.';\nCOMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';\n\n-- 4. Create rating aggregation trigger function\nCREATE OR REPLACE FUNCTION public.update_product_ratings()\nRETURNS TRIGGER AS $$\nDECLARE\n v_product_id uuid;\n v_avg numeric(3,2);\n v_count integer;\nBEGIN\n -- Determine which product_id we need to update\n IF TG_OP = 'DELETE' THEN\n v_product_id := OLD.product_id;\n ELSE\n v_product_id := NEW.product_id;\n END IF;\n\n IF v_product_id IS NOT NULL THEN\n -- Calculate average and count of approved reviews for this product\n SELECT COALESCE(avg(rating), 0.00), count(*)\n INTO v_avg, v_count\n FROM public.cms_interactions\n WHERE product_id = v_product_id\n AND type = 'review'\n AND status = 'approved';\n\n -- Update products table\n UPDATE public.products\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\n total_reviews = v_count\n WHERE id = v_product_id;\n END IF;\n\n -- Handle old product_id if it changed on UPDATE (e.g. transfer of reviews)\n IF TG_OP = 'UPDATE' AND OLD.product_id IS NOT NULL AND OLD.product_id <> NEW.product_id THEN\n SELECT COALESCE(avg(rating), 0.00), count(*)\n INTO v_avg, v_count\n FROM public.cms_interactions\n WHERE product_id = OLD.product_id\n AND type = 'review'\n AND status = 'approved';\n\n UPDATE public.products\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\n total_reviews = v_count\n WHERE id = OLD.product_id;\n END IF;\n\n RETURN NULL;\nEND;\n$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;\n\n-- 5. Attach trigger to interactions table\nCREATE TRIGGER trigger_update_product_ratings\nAFTER INSERT OR UPDATE OR DELETE\nON public.cms_interactions\nFOR EACH ROW\nEXECUTE FUNCTION public.update_product_ratings();\n\n-- 6. Attach update_at timestamp trigger\nCREATE TRIGGER set_updated_at\n BEFORE UPDATE ON public.cms_interactions\n FOR EACH ROW\n EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\n-- 7. Enable RLS and define policies\nALTER TABLE public.cms_interactions ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY cms_interactions_read_policy\n ON public.cms_interactions\n FOR SELECT\n TO public\n USING (\n status = 'approved'\n OR ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n );\n\nCREATE POLICY cms_interactions_insert_policy\n ON public.cms_interactions\n FOR INSERT\n TO authenticated\n WITH CHECK (\n auth.uid() = user_id\n AND (status = 'pending' OR (SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n );\n\nCREATE POLICY cms_interactions_update_policy\n ON public.cms_interactions\n FOR UPDATE\n TO authenticated\n USING (\n ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n )\n WITH CHECK (\n ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n );\n\nCREATE POLICY cms_interactions_delete_policy\n ON public.cms_interactions\n FOR DELETE\n TO authenticated\n USING (\n ((SELECT public.get_current_user_role()) IN ('ADMIN', 'WRITER'))\n );\n\n-- 8. Grant Table Permissions\nGRANT SELECT ON public.cms_interactions TO anon;\nGRANT ALL ON public.cms_interactions TO authenticated;\nGRANT ALL ON public.cms_interactions TO service_role;\n"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"version": "00000000000040",
|
|
219
|
+
"name": "00000000000040_add_profiles_fk_to_interactions.sql",
|
|
220
|
+
"sql": "-- 00000000000040_add_profiles_fk_to_interactions.sql\n-- Alter cms_interactions.user_id to reference public.profiles directly for PostgREST joins.\n\nALTER TABLE public.cms_interactions\n DROP CONSTRAINT IF EXISTS cms_interactions_user_id_fkey,\n ADD CONSTRAINT cms_interactions_user_id_profiles_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\n\nCOMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles.id';\n"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"version": "00000000000041",
|
|
224
|
+
"name": "00000000000041_seed_interactions_translations.sql",
|
|
225
|
+
"sql": "-- 00000000000041_seed_interactions_translations.sql\n-- Adds translation strings for Product Reviews and Post Comments storefront modules.\n\nBEGIN;\n\nINSERT INTO public.translations (key, translations)\nVALUES\n (\n 'reviews.customer_reviews',\n '{\"en\": \"Customer Reviews\", \"fr\": \"Avis clients\"}'::jsonb\n ),\n (\n 'reviews.share_thoughts',\n '{\"en\": \"Share your thoughts and experience with this product.\", \"fr\": \"Partagez vos impressions et votre expérience avec ce produit.\"}'::jsonb\n ),\n (\n 'reviews.write_review',\n '{\"en\": \"Write a Review\", \"fr\": \"Rédiger un avis\"}'::jsonb\n ),\n (\n 'reviews.cancel_review',\n '{\"en\": \"Cancel Review\", \"fr\": \"Annuler l''avis\"}'::jsonb\n ),\n (\n 'reviews.login_to_write',\n '{\"en\": \"Please log in to write a review.\", \"fr\": \"Veuillez vous connecter pour rédiger un avis.\"}'::jsonb\n ),\n (\n 'reviews.write_your_review',\n '{\"en\": \"Write your review\", \"fr\": \"Rédigez votre avis\"}'::jsonb\n ),\n (\n 'reviews.rating',\n '{\"en\": \"Rating\", \"fr\": \"Note\"}'::jsonb\n ),\n (\n 'reviews.description',\n '{\"en\": \"Review Description\", \"fr\": \"Description de l''avis\"}'::jsonb\n ),\n (\n 'reviews.description_placeholder',\n '{\"en\": \"What did you like or dislike? How does it perform?\", \"fr\": \"Qu''avez-vous aimé ou déploré ? Comment se comporte-t-il ?\"}'::jsonb\n ),\n (\n 'reviews.submit_review',\n '{\"en\": \"Submit Review\", \"fr\": \"Soumettre l''avis\"}'::jsonb\n ),\n (\n 'reviews.submitting',\n '{\"en\": \"Submitting...\", \"fr\": \"Envoi en cours...\"}'::jsonb\n ),\n (\n 'reviews.success_pending',\n '{\"en\": \"Your review has been submitted successfully and is pending moderation.\", \"fr\": \"Votre avis a été soumis avec succès et est en attente de modération.\"}'::jsonb\n ),\n (\n 'reviews.cancel',\n '{\"en\": \"Cancel\", \"fr\": \"Annuler\"}'::jsonb\n ),\n (\n 'reviews.helpful',\n '{\"en\": \"Helpful\", \"fr\": \"Utile\"}'::jsonb\n ),\n (\n 'reviews.no_reviews',\n '{\"en\": \"No reviews yet\", \"fr\": \"Aucun avis pour le moment\"}'::jsonb\n ),\n (\n 'reviews.be_the_first',\n '{\"en\": \"There are no reviews for this product yet. Be the first to write one!\", \"fr\": \"Il n''y a pas encore d''avis sur ce produit. Soyez le premier à en rédiger un !\"}'::jsonb\n ),\n (\n 'reviews.load_more',\n '{\"en\": \"Load More Reviews\", \"fr\": \"Charger plus d''avis\"}'::jsonb\n ),\n (\n 'reviews.review_count_one',\n '{\"en\": \"{count} review\", \"fr\": \"{count} avis\"}'::jsonb\n ),\n (\n 'reviews.review_count_other',\n '{\"en\": \"{count} reviews\", \"fr\": \"{count} avis\"}'::jsonb\n ),\n (\n 'comments.discussion',\n '{\"en\": \"Discussion & Comments\", \"fr\": \"Discussion & Commentaires\"}'::jsonb\n ),\n (\n 'comments.join_conversation',\n '{\"en\": \"Join the conversation and express your thoughts.\", \"fr\": \"Rejoignez la conversation et exprimez vos pensées.\"}'::jsonb\n ),\n (\n 'comments.write_comment',\n '{\"en\": \"Write a Comment\", \"fr\": \"Écrire un commentaire\"}'::jsonb\n ),\n (\n 'comments.cancel_comment',\n '{\"en\": \"Cancel Comment\", \"fr\": \"Annuler le commentaire\"}'::jsonb\n ),\n (\n 'comments.login_to_write',\n '{\"en\": \"Please log in to write a comment.\", \"fr\": \"Veuillez vous connecter pour écrire un commentaire.\"}'::jsonb\n ),\n (\n 'comments.join_discussion',\n '{\"en\": \"Join the Discussion\", \"fr\": \"Rejoindre la discussion\"}'::jsonb\n ),\n (\n 'comments.your_message',\n '{\"en\": \"Your Message\", \"fr\": \"Votre message\"}'::jsonb\n ),\n (\n 'comments.message_placeholder',\n '{\"en\": \"What are your thoughts on this article?\", \"fr\": \"Quelles sont vos pensées sur cet article ?\"}'::jsonb\n ),\n (\n 'comments.post_comment',\n '{\"en\": \"Post Comment\", \"fr\": \"Publier le commentaire\"}'::jsonb\n ),\n (\n 'comments.success_pending',\n '{\"en\": \"Your comment has been submitted successfully and is pending moderation.\", \"fr\": \"Votre commentaire a été soumis avec succès et est en attente de modération.\"}'::jsonb\n ),\n (\n 'comments.like',\n '{\"en\": \"Like\", \"fr\": \"J''aime\"}'::jsonb\n ),\n (\n 'comments.no_comments',\n '{\"en\": \"No comments yet\", \"fr\": \"Aucun commentaire pour le moment\"}'::jsonb\n ),\n (\n 'comments.be_the_first',\n '{\"en\": \"Be the first to share your thoughts!\", \"fr\": \"Soyez le premier à partager vos pensées !\"}'::jsonb\n ),\n (\n 'comments.load_more',\n '{\"en\": \"Load More Comments\", \"fr\": \"Charger plus de commentaires\"}'::jsonb\n )\nON CONFLICT (key) DO UPDATE\nSET \n translations = EXCLUDED.translations,\n updated_at = now();\n\nCOMMIT;\n"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"version": "00000000000042",
|
|
229
|
+
"name": "00000000000042_seed_deploy_to_vercel_ctas.sql",
|
|
230
|
+
"sql": "-- Promote the one-click \"Deploy to Vercel\" button as a CTA in the seeded homepage.\n--\n-- Two placements per language, both rendered through the normal 'button' block\n-- (ButtonBlockRenderer), so external URLs open in a new tab with rel=noopener:\n-- 1. Home HERO — replace the secondary \"View on GitHub\" / \"Voir sur GitHub\"\n-- outline button with a \"Deploy on Vercel\" / \"Déployer sur Vercel\" button\n-- (same variant/size/position; the primary \"Get Started\" button is untouched,\n-- and GitHub is still linked from the hero social row + the community section).\n-- 2. Home CLOSING CTA band (\"Have Questions?\" / \"Des questions ?\") — append a\n-- secondary (outline) \"Deploy on Vercel\" button after the contact button so\n-- the page also ends on the deploy action, styled to match the hero's Deploy\n-- button and keep the filled \"Get in Touch\" button as the band's primary.\n--\n-- The canonical deploy URL is the exact href from the README's Deploy button\n-- (native Supabase Marketplace `stores` flow). It is stored as a raw button `url`\n-- value (literal '&', not '&') because the renderer uses it directly as an href.\n--\n-- Forward-only and idempotent:\n-- * The hero swap matches the old button label, so a second run is a no-op\n-- (the label no longer exists) and it silently skips any install where the\n-- button was already customized or removed.\n-- * The closing-band append is guarded by NOT LIKE '%Deploy on Vercel%' /\n-- '%Déployer sur Vercel%', so it never appends twice.\n-- Scoping is by unique block text, so exactly the home hero / closing band match.\n\n-- ---------------------------------------------------------------------------\n-- 1a. EN home hero: \"View on GitHub\" -> \"Deploy on Vercel\"\n-- ---------------------------------------------------------------------------\nUPDATE public.blocks\nSET content = replace(\n replace(content::text, '\"View on GitHub\"', '\"Deploy on Vercel\"'),\n '\"https://github.com/nextblock-cms/nextblock\"',\n '\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\"'\n )::jsonb\nWHERE content::text LIKE '%\"View on GitHub\"%';\n\n-- ---------------------------------------------------------------------------\n-- 1b. FR home hero: \"Voir sur GitHub\" -> \"Déployer sur Vercel\"\n-- ---------------------------------------------------------------------------\nUPDATE public.blocks\nSET content = replace(\n replace(content::text, '\"Voir sur GitHub\"', '\"Déployer sur Vercel\"'),\n '\"https://github.com/nextblock-cms/nextblock\"',\n '\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\"'\n )::jsonb\nWHERE content::text LIKE '%\"Voir sur GitHub\"%';\n\n-- ---------------------------------------------------------------------------\n-- 2a. EN home closing band (\"Have Questions?\"): append a Deploy on Vercel button\n-- ---------------------------------------------------------------------------\nUPDATE public.blocks\nSET content = jsonb_set(\n content,\n '{column_blocks,0}',\n (content #> '{column_blocks,0}') ||\n '[{\"block_type\":\"button\",\"content\":{\"text\":\"Deploy on Vercel\",\"url\":\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\",\"variant\":\"outline\",\"size\":\"lg\",\"position\":\"center\"}}]'::jsonb\n )\nWHERE content::text LIKE '%Have Questions?%'\n AND content::text NOT LIKE '%Deploy on Vercel%';\n\n-- ---------------------------------------------------------------------------\n-- 2b. FR home closing band (\"Des questions ?\"): append a Déployer sur Vercel button\n-- ---------------------------------------------------------------------------\nUPDATE public.blocks\nSET content = jsonb_set(\n content,\n '{column_blocks,0}',\n (content #> '{column_blocks,0}') ||\n '[{\"block_type\":\"button\",\"content\":{\"text\":\"Déployer sur Vercel\",\"url\":\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\",\"variant\":\"outline\",\"size\":\"lg\",\"position\":\"center\"}}]'::jsonb\n )\nWHERE content::text LIKE '%Des questions ?%'\n AND content::text NOT LIKE '%Déployer sur Vercel%';\n"
|
|
201
231
|
}
|
|
202
232
|
];
|
|
@@ -23,7 +23,9 @@ import pkg from '../../package.json';
|
|
|
23
23
|
|
|
24
24
|
const UPSTREAM_REPO = 'nextblock-cms/nextblock';
|
|
25
25
|
const RELEASES_API = `https://api.github.com/repos/${UPSTREAM_REPO}/releases/latest`;
|
|
26
|
-
|
|
26
|
+
// The sync workflow tags its conflict issues with this hidden body marker. We match on it
|
|
27
|
+
// (not on a label) so a label that failed to create on GitHub can't hide a real conflict.
|
|
28
|
+
const CONFLICT_MARKER = '<!-- nextblock-sync-conflict -->';
|
|
27
29
|
const REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; // throttle the background poll to every 6h
|
|
28
30
|
|
|
29
31
|
export type UpdateTrack = 'git' | 'standalone';
|
|
@@ -240,11 +242,12 @@ export async function checkForSyncConflicts(): Promise<SyncConflictResult> {
|
|
|
240
242
|
// Only an AUTHORITATIVE res.ok read may drive the resolve loop below: a 404 (private
|
|
241
243
|
// fork without NEXTBLOCK_GITHUB_TOKEN, or a transient error) must NOT be read as "zero
|
|
242
244
|
// open conflicts", or live conflict alerts would be wrongly auto-resolved and hidden.
|
|
243
|
-
let issues: Array<{ number: number; html_url: string; pull_request?: unknown }> =
|
|
245
|
+
let issues: Array<{ number: number; html_url: string; body?: string | null; pull_request?: unknown }> =
|
|
246
|
+
[];
|
|
244
247
|
let issuesFetched = false;
|
|
245
248
|
let fetchError: string | undefined;
|
|
246
249
|
try {
|
|
247
|
-
const url = `https://api.github.com/repos/${self.owner}/${self.repo}/issues?
|
|
250
|
+
const url = `https://api.github.com/repos/${self.owner}/${self.repo}/issues?state=open&per_page=50`;
|
|
248
251
|
const res = await fetch(url, {
|
|
249
252
|
headers: githubHeaders(),
|
|
250
253
|
signal: AbortSignal.timeout(15_000),
|
|
@@ -253,7 +256,10 @@ export async function checkForSyncConflicts(): Promise<SyncConflictResult> {
|
|
|
253
256
|
if (res.ok) {
|
|
254
257
|
issuesFetched = true;
|
|
255
258
|
const data = await res.json();
|
|
256
|
-
|
|
259
|
+
// Exclude PRs (the /issues endpoint returns them too) and keep only our marked issues.
|
|
260
|
+
issues = (Array.isArray(data) ? data : [])
|
|
261
|
+
.filter((i) => !i.pull_request)
|
|
262
|
+
.filter((i) => typeof i.body === 'string' && i.body.includes(CONFLICT_MARKER));
|
|
257
263
|
} else if (res.status === 404) {
|
|
258
264
|
fetchError =
|
|
259
265
|
'Could not read repository issues (HTTP 404). For a private fork, set NEXTBLOCK_GITHUB_TOKEN.';
|
|
@@ -393,6 +399,34 @@ export async function refreshUpstreamStatus(): Promise<{
|
|
|
393
399
|
return { update, conflicts, snapshot };
|
|
394
400
|
}
|
|
395
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Mark the sync workflow as installed/active immediately after a successful Connect
|
|
404
|
+
* install — so the onboarding step flips to done right away rather than waiting for the
|
|
405
|
+
* next throttled poll (and GitHub's brief workflow-registration lag). Never throws.
|
|
406
|
+
*/
|
|
407
|
+
export async function markSyncWorkflowInstalled(): Promise<void> {
|
|
408
|
+
try {
|
|
409
|
+
const config = await getSystemConfiguration();
|
|
410
|
+
const prev = config.settings?.['upstream_status'] as Partial<UpstreamStatusSnapshot> | undefined;
|
|
411
|
+
const self = resolveSelfRepo();
|
|
412
|
+
const snapshot: UpstreamStatusSnapshot = {
|
|
413
|
+
checked_at: new Date().toISOString(),
|
|
414
|
+
current_version: prev?.current_version ?? pkg.version,
|
|
415
|
+
latest_version: prev?.latest_version ?? null,
|
|
416
|
+
update_available: prev?.update_available ?? false,
|
|
417
|
+
track: prev?.track ?? 'git',
|
|
418
|
+
repo: self ? `${self.owner}/${self.repo}` : prev?.repo ?? null,
|
|
419
|
+
open_conflicts: prev?.open_conflicts ?? 0,
|
|
420
|
+
actions_active: true,
|
|
421
|
+
};
|
|
422
|
+
await setSystemConfigurationServiceRole({
|
|
423
|
+
settings: { ...config.settings, upstream_status: snapshot },
|
|
424
|
+
});
|
|
425
|
+
} catch {
|
|
426
|
+
/* best-effort */
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
396
430
|
/** Throttled background refresh for the CMS layout's after() hook. Never throws. */
|
|
397
431
|
export async function maybeRefreshUpstreamStatus(): Promise<void> {
|
|
398
432
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextblock-cms/template",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "next dev",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@nextblock-cms/sdk": "workspace:*",
|
|
31
31
|
"@nextblock-cms/ui": "workspace:*",
|
|
32
32
|
"@nextblock-cms/utils": "workspace:*",
|
|
33
|
+
"@nextblock/cortex": "workspace:*",
|
|
33
34
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
34
35
|
"@supabase/ssr": "^0.10.2",
|
|
35
36
|
"@supabase/supabase-js": "^2.105.1",
|
|
@@ -4,12 +4,10 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
|
|
4
4
|
|
|
5
5
|
import { DynamicLayoutEngine } from '../components/renderers/DynamicLayoutEngine';
|
|
6
6
|
import {
|
|
7
|
+
buildCortexWidgetDefinitionInsertPayload,
|
|
7
8
|
buildCortexProfileCardVerificationDefinition,
|
|
8
9
|
validateCortexWidgetDefinitionOutput,
|
|
9
|
-
} from '
|
|
10
|
-
import {
|
|
11
|
-
buildCortexWidgetDefinitionInsertPayload,
|
|
12
|
-
} from '../lib/cortex-widget-registry';
|
|
10
|
+
} from '@nextblock/cortex';
|
|
13
11
|
|
|
14
12
|
dotenv.config({ path: '.env.local' });
|
|
15
13
|
|