create-nextblock 0.15.0 → 0.15.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/bin/create-nextblock.js +23 -2
- package/docker-template/.dockerignore +2 -1
- package/package.json +1 -1
- package/scripts/sync-template.js +97 -0
- package/templates/nextblock-template/.dockerignore +2 -1
- package/templates/nextblock-template/README.md +57 -34
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +525 -1
- package/templates/nextblock-template/app/cms/settings/site-scripts/components/SiteScriptManager.tsx +13 -5
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +2 -1
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +26 -11
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +9 -0
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +8 -0
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -151
- package/templates/nextblock-template/docs/README.md +2 -0
- package/templates/nextblock-template/gitignore +3 -0
- package/templates/nextblock-template/lib/onboarding/status.ts +11 -5
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +35 -0
- package/templates/nextblock-template/lib/updates/check-upstream.ts +167 -46
- package/templates/nextblock-template/package.json +6 -1
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -209
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -0
- package/templates/nextblock-template/tools/update.mjs +1285 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +0 -1
|
@@ -103,5 +103,40 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
|
|
|
103
103
|
"version": "00000000000017",
|
|
104
104
|
"name": "00000000000017_cortex_ai_mcp_server.sql",
|
|
105
105
|
"sql": "-- Cortex AI MCP (Model Context Protocol) server access.\n--\n-- Adds the bearer-token store that gates /api/mcp, the endpoint that exposes the\n-- Cortex AI tool registry to external MCP clients (Claude Code, Claude Desktop,\n-- Cursor, VS Code). Two pieces:\n--\n-- 1. public.mcp_access_tokens — one row per issued token. We store ONLY the\n-- SHA-256 hash of the token, never the token itself: the plaintext is shown\n-- to the admin exactly once at mint time and is unrecoverable afterwards, so\n-- a database leak cannot be replayed against the MCP endpoint. `token_prefix`\n-- is the non-secret leading fragment kept purely so the UI can tell two tokens\n-- apart in a list.\n--\n-- 2. cortex_ai_mcp_settings — a non-secret JSON site_settings row holding the\n-- server on/off switch and the localhost-trust flag. It is added to all four\n-- site_settings policies so only authenticated ADMINs can read or write it;\n-- the MCP route itself reads it through the service-role client, which\n-- bypasses RLS.\n--\n-- Forward-only. Recreates the four site_settings policies idempotently, preserving\n-- every key already in each policy's sensitive array (note that\n-- language_detection_settings stays anon-READABLE and so is absent from the SELECT\n-- policy, exactly as migration 00000000000012 left it).\n\nCREATE TABLE IF NOT EXISTS public.mcp_access_tokens (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n name text NOT NULL,\n -- Lowercase hex SHA-256 of the plaintext token. Unique so a lookup is a single\n -- indexed equality probe and duplicate mints are impossible.\n token_hash text NOT NULL UNIQUE,\n -- Non-secret display fragment, e.g. \"nbmcp_a1b2c3d4\". Never enough to authenticate.\n token_prefix text NOT NULL,\n -- 'read' grants the read-only tools; 'write' additionally grants the mutating ones.\n scopes text[] NOT NULL DEFAULT ARRAY['read', 'write']::text[],\n created_by uuid REFERENCES auth.users (id) ON DELETE SET NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n last_used_at timestamptz,\n expires_at timestamptz,\n revoked_at timestamptz\n);\n\nCOMMENT ON TABLE public.mcp_access_tokens IS\n 'Bearer tokens for the Cortex AI MCP server at /api/mcp. Stores SHA-256 hashes only; plaintext is displayed once at mint time.';\n\nCREATE INDEX IF NOT EXISTS mcp_access_tokens_token_hash_idx\n ON public.mcp_access_tokens (token_hash);\n\n-- Orders the admin token list newest-first without a sort.\nCREATE INDEX IF NOT EXISTS mcp_access_tokens_created_at_idx\n ON public.mcp_access_tokens (created_at DESC);\n\nALTER TABLE public.mcp_access_tokens ENABLE ROW LEVEL SECURITY;\n\n-- Tokens are credentials: admin-only, with no anon or WRITER access at all. The\n-- MCP route verifies them with the service-role client, which bypasses RLS.\nDROP POLICY IF EXISTS mcp_access_tokens_admin_select ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_select ON public.mcp_access_tokens\n FOR SELECT TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_insert ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_insert ON public.mcp_access_tokens\n FOR INSERT TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_update ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_update ON public.mcp_access_tokens\n FOR UPDATE TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_delete ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_delete ON public.mcp_access_tokens\n FOR DELETE TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.mcp_access_tokens TO authenticated;\nGRANT ALL ON public.mcp_access_tokens TO service_role;\n\n-- Add cortex_ai_mcp_settings to the admin-only site_settings group (all four policies).\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"version": "00000000000018",
|
|
109
|
+
"name": "00000000000018_site_scripts.sql",
|
|
110
|
+
"sql": "-- Site scripts: admin-authored JavaScript injected into every page of the public site.\n--\n-- Rich-text blocks can already carry an inline <script>, but that script belongs to\n-- one block on one page. This table is for behaviour that spans the site: chat\n-- widgets, third-party embeds, and the scroll/animation helpers that page classes\n-- rely on. Each row gets a name, an on/off switch, and a defined injection point.\n--\n-- NOT the same thing as `site_settings.privacy_settings -> custom_scripts`, which is\n-- a single consent-gated blob for marketing tags and only fires once a visitor\n-- accepts cookies. Rows here are functional site code and run unconditionally, so\n-- anything requiring consent belongs in that setting instead, not this table.\n--\n-- Scripts are emitted with the request's CSP nonce by the root layout, so they run\n-- under the site's existing Content-Security-Policy rather than forcing it open.\n--\n-- Security posture: this is arbitrary JavaScript on every page, so writes are\n-- ADMIN-only (WRITER is deliberately excluded, unlike most content tables) and the\n-- public may read only rows that are switched on, so a half-written draft is never\n-- served to a visitor.\n\nCREATE TABLE IF NOT EXISTS public.site_scripts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n name text NOT NULL,\n description text,\n -- Raw JavaScript, stored WITHOUT the surrounding <script> tag. The layout adds\n -- the tag so the nonce and attributes are always applied by us, never by the\n -- author. Ignored when `src` is set.\n code text DEFAULT ''::text NOT NULL,\n -- When set, an external script is loaded from this URL and `code` is ignored.\n src text,\n -- Where the tag is emitted. 'head' runs before first paint (blocking, use\n -- sparingly); 'body_end' runs once the markup exists and is the right default\n -- for anything that queries the DOM.\n placement text DEFAULT 'body_end'::text NOT NULL,\n -- Applies to external `src` scripts; inline code ignores it.\n load_strategy text DEFAULT 'default'::text NOT NULL,\n is_active boolean DEFAULT false NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_scripts_pkey PRIMARY KEY (id),\n CONSTRAINT site_scripts_placement_check\n CHECK ((placement = ANY (ARRAY['head'::text, 'body_start'::text, 'body_end'::text]))),\n CONSTRAINT site_scripts_load_strategy_check\n CHECK ((load_strategy = ANY (ARRAY['default'::text, 'defer'::text, 'async'::text]))),\n -- An external script must be https so it cannot be downgraded in transit.\n CONSTRAINT site_scripts_src_scheme_check\n CHECK ((src IS NULL OR src ~ '^https://')),\n -- A row has to actually do something: inline code or an external src.\n CONSTRAINT site_scripts_has_payload_check\n CHECK ((src IS NOT NULL OR length(btrim(code)) > 0))\n);\n\nCOMMENT ON TABLE public.site_scripts IS\n 'Admin-authored JavaScript injected into the public site by the root layout, with the request CSP nonce applied. Only is_active rows are publicly readable; only ADMIN may write. Distinct from privacy_settings.custom_scripts, which is consent-gated marketing tags.';\n\nCREATE INDEX IF NOT EXISTS site_scripts_active_placement_sort_idx\n ON public.site_scripts USING btree (is_active, placement, sort_order);\n\nDROP TRIGGER IF EXISTS set_site_scripts_updated_at ON public.site_scripts;\nCREATE TRIGGER set_site_scripts_updated_at\n BEFORE UPDATE ON public.site_scripts\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.site_scripts ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON TABLE public.site_scripts TO anon;\nGRANT ALL ON TABLE public.site_scripts TO authenticated;\nGRANT ALL ON TABLE public.site_scripts TO service_role;\n\n-- Anonymous visitors need the active scripts to render the page. Inactive rows stay\n-- private so a half-written script is never exposed before it is switched on.\nDROP POLICY IF EXISTS \"Public read active site scripts\" ON public.site_scripts;\nCREATE POLICY \"Public read active site scripts\" ON public.site_scripts\n FOR SELECT TO authenticated, anon USING (is_active);\n\nDROP POLICY IF EXISTS \"Admins read all site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins read all site scripts\" ON public.site_scripts\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins insert site scripts\" ON public.site_scripts\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins update site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins update site scripts\" ON public.site_scripts\n FOR UPDATE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins delete site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins delete site scripts\" ON public.site_scripts\n FOR DELETE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"version": "00000000000019",
|
|
114
|
+
"name": "00000000000019_site_script_revisions.sql",
|
|
115
|
+
"sql": "-- Audit trail and undo for site scripts.\n--\n-- `site_scripts` ships arbitrary JavaScript to every visitor, which makes it the\n-- highest-privilege write in the CMS: a bad or malicious snippet can read cookies,\n-- watch checkout forms, or phone home. Content has Revision History for exactly this\n-- reason; code needs it more, not less. Every create/update/delete writes one row\n-- here, and every row is a complete, restorable snapshot — so this table is both the\n-- log (\"who shipped what, when, from where\") and the undo.\n--\n-- APPEND-ONLY BY CONSTRUCTION. There are no UPDATE or DELETE policies, and the\n-- trigger below rejects both even for the service role, which otherwise bypasses\n-- RLS. An audit trail that the compromised credential can rewrite is not an audit\n-- trail. Reverting therefore writes a NEW 'revert' row rather than removing history.\n--\n-- `script_id` and `actor_user_id` are deliberately PLAIN uuids with no foreign keys:\n-- an FK with ON DELETE SET NULL would have to UPDATE this table when a script or a\n-- profile is deleted, which the append-only trigger forbids. `script_name` is\n-- denormalised so a deleted script is still identifiable in the log.\n\nCREATE TABLE IF NOT EXISTS public.site_script_revisions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n script_id uuid,\n script_name text NOT NULL,\n revision_type text NOT NULL,\n -- Null when the actor could not be resolved (e.g. a localhost dev connection).\n actor_user_id uuid,\n -- Which surface made the change, so an unexpected edit can be traced back to\n -- the dashboard or to an MCP token.\n source text DEFAULT 'cms'::text NOT NULL,\n summary text,\n -- Full restorable state of the script at this revision. For 'delete' it is the\n -- state immediately BEFORE removal, so restoring it brings the script back.\n snapshot jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_script_revisions_pkey PRIMARY KEY (id),\n CONSTRAINT site_script_revisions_type_check\n CHECK ((revision_type = ANY (ARRAY['create'::text, 'update'::text, 'delete'::text, 'revert'::text]))),\n CONSTRAINT site_script_revisions_source_check\n CHECK ((source = ANY (ARRAY['cms'::text, 'mcp'::text]))),\n CONSTRAINT site_script_revisions_snapshot_is_object_check\n CHECK ((jsonb_typeof(snapshot) = 'object'))\n);\n\nCOMMENT ON TABLE public.site_script_revisions IS\n 'Append-only audit log and undo history for site_scripts. Each row is a restorable snapshot. UPDATE and DELETE are blocked by trigger, including for the service role.';\n\nCREATE INDEX IF NOT EXISTS site_script_revisions_script_created_idx\n ON public.site_script_revisions USING btree (script_id, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS site_script_revisions_created_idx\n ON public.site_script_revisions USING btree (created_at DESC);\n\n-- Enforced in the database rather than the application so it holds for every\n-- caller, including the service-role client the MCP server uses.\nCREATE OR REPLACE FUNCTION public.prevent_site_script_revision_rewrite() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path = ''\n AS $$\nBEGIN\n RAISE EXCEPTION 'site_script_revisions is append-only; % is not permitted', TG_OP\n USING ERRCODE = 'restrict_violation';\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_site_script_revisions_append_only ON public.site_script_revisions;\nCREATE TRIGGER trg_site_script_revisions_append_only\n BEFORE UPDATE OR DELETE ON public.site_script_revisions\n FOR EACH ROW EXECUTE FUNCTION public.prevent_site_script_revision_rewrite();\n\nALTER TABLE public.site_script_revisions ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, INSERT ON TABLE public.site_script_revisions TO authenticated;\nGRANT ALL ON TABLE public.site_script_revisions TO service_role;\n\n-- Read is ADMIN-only: snapshots contain the full source of scripts that may not be\n-- active yet, and the log itself reveals operational history.\nDROP POLICY IF EXISTS \"Admins read site script revisions\" ON public.site_script_revisions;\nCREATE POLICY \"Admins read site script revisions\" ON public.site_script_revisions\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert site script revisions\" ON public.site_script_revisions;\nCREATE POLICY \"Admins insert site script revisions\" ON public.site_script_revisions\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"version": "00000000000020",
|
|
119
|
+
"name": "00000000000020_updating_article.sql",
|
|
120
|
+
"sql": "-- 00000000000020_updating_article.sql\n-- Seeds the \"How Updating NextBlock Works\" guide as an EN/FR twin pair, the companion\n-- to the install guide seeded in the baseline (00000000000003, slugs 'how-to-setup-nextblock'\n-- / 'comment-configurer-nextblock'). It documents the single `npm run update` command\n-- across all four install paths — one-click Vercel, npm create → Docker, npm create →\n-- managed cloud, and the cloned monorepo.\n--\n-- Forward-only and idempotent by construction:\n-- * posts carries UNIQUE (language_id, slug), so the inserts use ON CONFLICT DO NOTHING;\n-- * blocks has no natural unique key, so the body insert is guarded on NOT EXISTS;\n-- * ids are never hardcoded — posts.id and blocks.id are identity columns and a live\n-- database has real editor-created rows occupying the low ids the baseline used.\n-- * the feature image is looked up rather than asserted, so a site that deleted the\n-- seeded media row still gets the article (with no cover) instead of a failed migration.\n--\n-- Because the sandbox reset payload and the /setup wizard's embedded bundle are both\n-- generated FROM this directory, the article reaches a fresh install and every hourly\n-- sandbox reset with no extra wiring — regenerate them with `npm run generate:sandbox`\n-- and `npm run generate:migrations-bundle`.\n--\n-- blocks.content is JSONB written with PostgreSQL dollar-quoting (same style as 006/008)\n-- so the HTML can use ordinary single-quoted class attributes without quote doubling.\n\nDO $body$\nDECLARE\n v_group uuid := 'c0d3f1a2-8b47-4e19-9a52-7f6b1d4e8c30';\n v_image uuid;\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_image\n FROM public.media\n WHERE id = '641ddf75-5c90-41df-8b83-e7c298f30a6a'::uuid;\n\n ---------------------------------------------------------------------------\n -- English\n ---------------------------------------------------------------------------\n INSERT INTO public.posts (\n language_id, author_id, title, slug, label, excerpt, subtitle, status,\n published_at, meta_title, meta_description, feature_image_id, version,\n translation_group_id\n )\n SELECT\n 1, NULL,\n 'How Updating NextBlock Works: One Command for Every Install',\n 'how-updating-works',\n 'Maintenance',\n 'However you installed NextBlock — one-click Vercel, the CLI, Docker or a git clone — a single npm run update brings the code, the dependencies and the database schema forward together.',\n 'Automatic upstream syncing on Vercel, and one command everywhere else: what npm run update does, what it never touches, and how to roll it back.',\n 'published', now(),\n 'How to Update NextBlock — One Command for Every Install',\n 'Update NextBlock in one step. npm run update pulls new code, installs dependencies and applies pending database migrations on Vercel, Docker, CLI and git-clone installs alike.',\n v_image, 1, v_group\n WHERE NOT EXISTS (\n SELECT 1 FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n );\n\n SELECT id INTO v_en_post\n FROM public.posts\n WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id\n LIMIT 1;\n\n IF v_en_post IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks WHERE post_id = v_en_post\n ) THEN\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n VALUES (v_en_post, 1, 'text', $nben$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock ships improvements continuously — new blocks, editor fixes, security patches, and occasionally a database change that the new code depends on. Keeping up with all of that used to mean knowing which of the four install paths you were on. It no longer does. Every NextBlock project, however it was created, understands one command:</p>\\n\\n<div class='my-10 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'>\\n <div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'>\\n <span class='h-3 w-3 rounded-full bg-red-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-yellow-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-green-400/70'></span>\\n <span class='ml-3 text-xs font-mono text-slate-400'>your project</span>\\n </div>\\n <div class='px-6 py-8 text-center'>\\n <p class='mt-0 mb-2 font-mono text-2xl sm:text-3xl font-semibold text-emerald-300'>npm run update</p>\\n <p class='mb-0 text-sm text-slate-400'>Code · dependencies · database schema — in that order, in one step.</p>\\n </div>\\n</div>\\n\\n<p>It figures out which kind of install it is running inside, picks the right source for new code, installs the matching dependencies, and then applies any database migrations the new version needs. If you would rather look before you leap, <code>npm run update -- --check</code> reports exactly what would change and touches nothing.</p>\\n\\n<h2 id='the-four-paths'>The four install paths, and how each one gets updates</h2>\\n<p class='text-slate-600 dark:text-slate-400'>These map one-to-one onto the four options in <a href='/article/how-to-setup-nextblock'>the install guide</a>. The command is the same everywhere; what differs is where the new code comes from.</p>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-8'>\\n <a href='#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 <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Fully automatic</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>One-click Vercel & GitHub forks</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A daily workflow merges upstream into your repository and Vercel redeploys. You do nothing.</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 <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock → Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Update, then rebuild the local stack. Your Postgres and media volumes survive untouched.</p>\\n </a>\\n <a href='#cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock → Supabase</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>New framework files come from npm; your own pages, routes and content are left alone.</p>\\n </a>\\n <a href='#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 <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>git clone the monorepo</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A guarded git pull or upstream merge, then dependencies and migrations. No manual steps.</p>\\n </a>\\n</div>\\n\\n<h2 id='vercel'>1. One-click Vercel and GitHub forks — hands-off</h2>\\n<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard’s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository’s <strong>Actions</strong> tab.</p>\\n<ol class='space-y-2'>\\n <li>The workflow merges the latest upstream NextBlock into your deploy branch.</li>\\n <li>A clean merge is pushed to your branch, which triggers an ordinary Vercel deployment.</li>\\n <li>During that production build, NextBlock applies any pending database migrations <em>before</em> the app is built — so new code never runs against an old schema.</li>\\n <li>If the merge conflicts, nothing is pushed. The workflow opens a GitHub issue instead, and your CMS dashboard shows an amber banner linking straight to it. Resolve it, close the issue, and the banner clears itself.</li>\\n</ol>\\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'>Make the repository public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>A public repository is completely zero-config. On a private one, add a <code>NEXTBLOCK_GITHUB_TOKEN</code> environment variable with read access to issues so the conflict banner still works — and note that Vercel’s free Hobby plan refuses to auto-deploy automated commits on private repositories, so the merge would land without deploying.</p>\\n</div>\\n<p>Working on a local clone of that fork? <code>npm run update</code> does the same merge on your machine, adding an <code>upstream</code> remote if it is missing, then installs dependencies and applies migrations.</p>\\n\\n<h2 id='docker'>2. npm create nextblock → Docker — update, then rebuild</h2>\\n<p>From your project directory:</p>\\n<pre><code>npm run update\\nnpm run docker:up</code></pre>\\n<p>The first command refreshes the application, its dependencies and the schema; the second rebuilds and restarts the containers. Your database and media live in Docker volumes and are never touched by either step — <code>docker:up</code> rebuilds images, not data.</p>\\n\\n<h2 id='cloud'>3. npm create nextblock → managed Supabase — one command</h2>\\n<pre><code>npm run update\\nnpm run build\\nnpm start</code></pre>\\n<p>Your project is a standalone Next.js app, so new framework code is fetched from the published <code>create-nextblock</code> package on npm — the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock refreshes the files it owns, merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and then applies migrations.</p>\\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'>Deploying to Vercel from this project</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Run <code>npm run update</code> locally, commit the result, and push. Your production build applies any pending migrations on the way up, exactly as it does for one-click installs.</p>\\n</div>\\n\\n<h2 id='clone'>4. The cloned monorepo — one command</h2>\\n<pre><code>npm run update</code></pre>\\n<p>In a clone of the NextBlock repository this fast-forwards your checkout, reinstalls workspace dependencies and applies pending migrations. It refuses to run over uncommitted changes and tells you how to stash them first, so an update can never silently eat work in progress. If you have local commits, it stops and points you at <code>git pull --rebase</code> rather than guessing.</p>\\n\\n<h2 id='what-it-does'>What <code>npm run update</code> actually does</h2>\\n<ol class='space-y-2'>\\n <li><strong>Identifies the install.</strong> Monorepo or standalone app; git-backed or npm-backed; Docker or not.</li>\\n <li><strong>Updates the code</strong> from the right source — an upstream git merge, a fast-forward pull, or the published <code>create-nextblock</code> package.</li>\\n <li><strong>Installs dependencies</strong> with <code>npm install</code>, so the code and the packages it imports move together.</li>\\n <li><strong>Refreshes the migration files</strong> shipped inside <code>@nextblock-cms/db</code>, so the newest schema changes are on disk before anything is applied.</li>\\n <li><strong>Applies pending migrations</strong>, listing them first and asking before it writes.</li>\\n <li><strong>Clears the dashboard’s update banner</strong> once the new version is really in place.</li>\\n</ol>\\n\\n<h3>Options</h3>\\n<div class='overflow-x-auto my-6'>\\n<table class='w-full text-left text-sm'>\\n <thead><tr class='border-b border-slate-200 dark:border-white/10'><th class='py-3 pr-4 font-semibold'>Command</th><th class='py-3 font-semibold'>What it does</th></tr></thead>\\n <tbody class='align-top'>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update</code></td><td class='py-3'>Code, dependencies and schema.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --check</code></td><td class='py-3'>Report what would change. Writes nothing.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --yes</code></td><td class='py-3'>Skip the confirmation prompts. Useful in CI.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --db-only</code></td><td class='py-3'>Apply pending migrations and nothing else.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --skip-db</code></td><td class='py-3'>Update code and dependencies, leave the database alone.</td></tr>\\n <tr><td class='py-3 pr-4'><code>npm run update -- --force</code></td><td class='py-3'>Run even when you are already on the latest version.</td></tr>\\n </tbody>\\n</table>\\n</div>\\n\\n<h2 id='database'>What happens to your database</h2>\\n<p>Schema changes are <strong>forward-only</strong>. NextBlock never rewrites or replays a migration that has already run: each one is applied and recorded in the same transaction, so a failure rolls back cleanly and leaves the database exactly as it was. Already-applied migrations are skipped by version, which makes re-running an update completely safe.</p>\\n<p>Migrations change <em>structure</em> — tables, columns, indexes, permissions. Your pages, posts, products, media and users are yours; the update never deletes or rewrites them.</p>\\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'>Belt and braces</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Before a big jump on a production site, take a database snapshot — Supabase does daily backups on paid plans, and you can trigger one on demand from the Supabase dashboard. Then run <code>npm run update -- --check</code> to see the pending list before you commit to it.</p>\\n</div>\\n\\n<h2 id='safety'>If something goes wrong</h2>\\n<ul class='space-y-2'>\\n <li><strong>Standalone projects:</strong> every framework file the update replaces is copied first into a timestamped folder under <code>.nextblock-backup/</code> in your project. Nothing is deleted, so files you added yourself are never removed.</li>\\n <li><strong>Git-backed installs:</strong> the update is an ordinary commit. <code>git log</code> shows it and <code>git revert</code> undoes it.</li>\\n <li><strong>A conflicted merge</strong> is aborted automatically — your working tree is left exactly as it was, with instructions printed for resolving it by hand.</li>\\n <li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>\\n</ul>\\n<p>If you have customised a file that NextBlock owns — something under <code>app/</code>, <code>components/</code> or <code>lib/</code> — the update will replace it and back up your version. Diff the backup afterwards to bring your change forward. Customisations that live in your own new files, in the CMS, or in <code>.env</code> are never affected.</p>\\n\\n<h2 id='knowing'>Knowing when there is something to update</h2>\\n<p>You do not have to poll. NextBlock checks in the background while you use the CMS and raises a dashboard banner when a newer version is published, telling you which version you are on and what is available. Administrators can also just run <code>npm run update -- --check</code> at any time.</p>\\n\\n<h2 id='faq'>Update FAQ</h2>\\n<h3>Will updating overwrite my content or settings?</h3>\\n<p>No. Content, media, users and settings live in your database; site configuration lives in your environment variables. The update touches application code, dependencies and schema structure only.</p>\\n<h3>Do I have to update every release?</h3>\\n<p>No, though staying close to the latest release keeps you on security fixes and makes each jump smaller. Updates apply in sequence, so skipping several versions still lands correctly.</p>\\n<h3>Can I run it in CI?</h3>\\n<p>Yes — <code>npm run update -- --yes</code> never prompts, and it exits non-zero if the schema step fails so a pipeline can catch it.</p>\\n<h3>What if my project has no database connection configured?</h3>\\n<p>Code and dependencies still update; the schema step is skipped with a warning telling you which environment variable to set. Re-run <code>npm run update -- --db-only</code> once it is configured.</p>\\n<h3>I am on the one-click Vercel deploy — do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</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'>One command, every install.</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>New to NextBlock? Start with the install guide — then never think about upgrades again.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='/article/how-to-setup-nextblock' 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'>Read the install guide</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\\n </div>\\n</div>\"}\n$nben$::jsonb, 0);\n END IF;\n\n ---------------------------------------------------------------------------\n -- French\n ---------------------------------------------------------------------------\n INSERT INTO public.posts (\n language_id, author_id, title, slug, label, excerpt, subtitle, status,\n published_at, meta_title, meta_description, feature_image_id, version,\n translation_group_id\n )\n SELECT\n 2, NULL,\n 'Les mises à jour de NextBlock : une seule commande, quelle que soit l''installation',\n 'comment-fonctionnent-les-mises-a-jour',\n 'Maintenance',\n 'Quelle que soit votre installation — Vercel en un clic, le CLI, Docker ou un git clone — une seule commande npm run update fait avancer ensemble le code, les dépendances et le schéma de base de données.',\n 'Synchronisation automatique sur Vercel, et une seule commande partout ailleurs : ce que fait npm run update, ce qu''il ne touche jamais, et comment revenir en arrière.',\n 'published', now(),\n 'Mettre à jour NextBlock — une commande pour toutes les installations',\n 'Mettez NextBlock à jour en une étape. npm run update récupère le nouveau code, installe les dépendances et applique les migrations en attente, sur Vercel, Docker, CLI et git clone.',\n v_image, 1, v_group\n WHERE NOT EXISTS (\n SELECT 1 FROM public.posts\n WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n );\n\n SELECT id INTO v_fr_post\n FROM public.posts\n WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id\n LIMIT 1;\n\n IF v_fr_post IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks WHERE post_id = v_fr_post\n ) THEN\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n VALUES (v_fr_post, 2, 'text', $nbfr$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock évolue en continu — nouveaux blocs, corrections de l'éditeur, correctifs de sécurité, et parfois une modification de la base de données dont le nouveau code dépend. Suivre tout cela supposait autrefois de savoir laquelle des quatre méthodes d'installation vous aviez utilisée. Ce n'est plus le cas. Tout projet NextBlock, quelle que soit sa création, comprend une seule commande :</p>\\n\\n<div class='my-10 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'>\\n <div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'>\\n <span class='h-3 w-3 rounded-full bg-red-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-yellow-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-green-400/70'></span>\\n <span class='ml-3 text-xs font-mono text-slate-400'>votre projet</span>\\n </div>\\n <div class='px-6 py-8 text-center'>\\n <p class='mt-0 mb-2 font-mono text-2xl sm:text-3xl font-semibold text-emerald-300'>npm run update</p>\\n <p class='mb-0 text-sm text-slate-400'>Code · dépendances · schéma de base de données — dans cet ordre, en une seule étape.</p>\\n </div>\\n</div>\\n\\n<p>La commande détermine dans quel type d'installation elle s'exécute, choisit la bonne source pour le nouveau code, installe les dépendances correspondantes, puis applique les migrations dont la nouvelle version a besoin. Si vous préférez regarder avant de sauter, <code>npm run update -- --check</code> indique exactement ce qui changerait sans rien modifier.</p>\\n\\n<h2 id='the-four-paths'>Les quatre installations et leurs mises à jour</h2>\\n<p class='text-slate-600 dark:text-slate-400'>Elles correspondent une à une aux quatre options du <a href='/article/comment-configurer-nextblock'>guide d'installation</a>. La commande est la même partout ; seule la provenance du nouveau code change.</p>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-8'>\\n <a href='#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 <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Entièrement automatique</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Vercel en un clic et forks GitHub</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un workflow quotidien fusionne les nouveautés dans votre dépôt et Vercel redéploie. Vous n'avez rien à faire.</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 <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock → Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Mettez à jour, puis reconstruisez la pile locale. Vos volumes Postgres et médias restent intacts.</p>\\n </a>\\n <a href='#cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock → Supabase</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Les nouveaux fichiers viennent de npm ; vos pages, routes et contenus ne sont pas touchés.</p>\\n </a>\\n <a href='#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 <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>git clone du monorepo</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un pull ou une fusion protégée, puis les dépendances et les migrations. Aucune étape manuelle.</p>\\n </a>\\n</div>\\n\\n<h2 id='vercel'>1. Vercel en un clic et forks GitHub — sans intervention</h2>\\n<p>Ce chemin se met à jour tout seul. Lors du déploiement, NextBlock a créé un dépôt qui vous appartient ; l'étape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s'exécute <strong>chaque jour à minuit UTC</strong> et peut aussi être lancé à la demande depuis l'onglet <strong>Actions</strong> de votre dépôt.</p>\\n<ol class='space-y-2'>\\n <li>Le workflow fusionne la dernière version de NextBlock dans votre branche de déploiement.</li>\\n <li>Une fusion propre est poussée sur votre branche, ce qui déclenche un déploiement Vercel normal.</li>\\n <li>Pendant ce build de production, NextBlock applique les migrations en attente <em>avant</em> de construire l'application — le nouveau code ne tourne donc jamais sur un ancien schéma.</li>\\n <li>En cas de conflit, rien n'est poussé. Le workflow ouvre une issue GitHub et votre tableau de bord affiche une bannière ambre qui pointe dessus. Résolvez, fermez l'issue, et la bannière disparaît d'elle-même.</li>\\n</ol>\\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'>Rendez le dépôt public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Un dépôt public ne demande aucune configuration. Sur un dépôt privé, ajoutez une variable d'environnement <code>NEXTBLOCK_GITHUB_TOKEN</code> avec un accès en lecture aux issues pour que la bannière de conflit fonctionne — et sachez que l'offre gratuite Hobby de Vercel refuse de déployer automatiquement les commits automatisés sur un dépôt privé.</p>\\n</div>\\n<p>Vous travaillez sur un clone local de ce fork ? <code>npm run update</code> effectue la même fusion sur votre machine, en ajoutant le dépôt <code>upstream</code> s'il manque, puis installe les dépendances et applique les migrations.</p>\\n\\n<h2 id='docker'>2. npm create nextblock → Docker — mettre à jour puis reconstruire</h2>\\n<p>Depuis le dossier de votre projet :</p>\\n<pre><code>npm run update\\nnpm run docker:up</code></pre>\\n<p>La première commande met à jour l'application, ses dépendances et le schéma ; la seconde reconstruit et redémarre les conteneurs. Votre base de données et vos médias vivent dans des volumes Docker et ne sont touchés ni par l'une ni par l'autre — <code>docker:up</code> reconstruit des images, pas des données.</p>\\n\\n<h2 id='cloud'>3. npm create nextblock → Supabase géré — une commande</h2>\\n<pre><code>npm run update\\nnpm run build\\nnpm start</code></pre>\\n<p>Votre projet est une application Next.js autonome : le nouveau code provient donc du paquet <code>create-nextblock</code> publié sur npm — exactement l'artefact à partir duquel votre projet a été généré, versionné en phase avec la release. NextBlock rafraîchit les fichiers qui lui appartiennent, fusionne les nouvelles versions de dépendances dans votre <code>package.json</code>, lance <code>npm install</code>, puis applique les migrations.</p>\\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'>Vous déployez ce projet sur Vercel ?</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Lancez <code>npm run update</code> en local, validez le résultat et poussez. Votre build de production applique les migrations en attente au passage, exactement comme pour les installations en un clic.</p>\\n</div>\\n\\n<h2 id='clone'>4. Le monorepo cloné — une commande</h2>\\n<pre><code>npm run update</code></pre>\\n<p>Dans un clone du dépôt NextBlock, la commande met votre copie à jour, réinstalle les dépendances du workspace et applique les migrations en attente. Elle refuse de s'exécuter par-dessus des modifications non validées et vous explique comment les mettre de côté : une mise à jour ne peut donc jamais faire disparaître du travail en cours. Si vous avez des commits locaux, elle s'arrête et vous oriente vers <code>git pull --rebase</code> plutôt que de deviner.</p>\\n\\n<h2 id='what-it-does'>Ce que fait réellement <code>npm run update</code></h2>\\n<ol class='space-y-2'>\\n <li><strong>Identifie l'installation.</strong> Monorepo ou application autonome ; basée sur git ou sur npm ; Docker ou non.</li>\\n <li><strong>Met à jour le code</strong> depuis la bonne source — fusion git, pull en avance rapide, ou le paquet <code>create-nextblock</code> publié.</li>\\n <li><strong>Installe les dépendances</strong> avec <code>npm install</code>, pour que le code et les paquets qu'il importe avancent ensemble.</li>\\n <li><strong>Rafraîchit les fichiers de migration</strong> livrés dans <code>@nextblock-cms/db</code>, afin que les dernières évolutions du schéma soient sur le disque avant toute application.</li>\\n <li><strong>Applique les migrations en attente</strong>, en les listant d'abord et en demandant confirmation.</li>\\n <li><strong>Efface la bannière de mise à jour</strong> du tableau de bord une fois la nouvelle version réellement en place.</li>\\n</ol>\\n\\n<h3>Options</h3>\\n<div class='overflow-x-auto my-6'>\\n<table class='w-full text-left text-sm'>\\n <thead><tr class='border-b border-slate-200 dark:border-white/10'><th class='py-3 pr-4 font-semibold'>Commande</th><th class='py-3 font-semibold'>Effet</th></tr></thead>\\n <tbody class='align-top'>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update</code></td><td class='py-3'>Code, dépendances et schéma.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --check</code></td><td class='py-3'>Indique ce qui changerait. N'écrit rien.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --yes</code></td><td class='py-3'>Sans confirmation. Pratique en CI.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --db-only</code></td><td class='py-3'>Applique uniquement les migrations en attente.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --skip-db</code></td><td class='py-3'>Met à jour le code et les dépendances, sans toucher à la base.</td></tr>\\n <tr><td class='py-3 pr-4'><code>npm run update -- --force</code></td><td class='py-3'>S'exécute même si vous êtes déjà à jour.</td></tr>\\n </tbody>\\n</table>\\n</div>\\n\\n<h2 id='database'>Ce qui arrive à votre base de données</h2>\\n<p>Les évolutions du schéma sont <strong>uniquement additives</strong>. NextBlock ne réécrit ni ne rejoue jamais une migration déjà appliquée : chacune est appliquée et enregistrée dans la même transaction, si bien qu'un échec est annulé proprement et laisse la base exactement dans son état initial. Les migrations déjà appliquées sont ignorées par numéro de version, ce qui rend une nouvelle exécution totalement sûre.</p>\\n<p>Les migrations modifient la <em>structure</em> — tables, colonnes, index, permissions. Vos pages, articles, produits, médias et utilisateurs vous appartiennent : la mise à jour ne les supprime ni ne les réécrit.</p>\\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'>Par précaution</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Avant un grand saut sur un site en production, prenez une sauvegarde de la base — Supabase en réalise quotidiennement sur les offres payantes, et vous pouvez en déclencher une à la demande depuis son tableau de bord. Lancez ensuite <code>npm run update -- --check</code> pour voir la liste des migrations en attente.</p>\\n</div>\\n\\n<h2 id='safety'>En cas de problème</h2>\\n<ul class='space-y-2'>\\n <li><strong>Projets autonomes :</strong> chaque fichier remplacé est d'abord copié dans un dossier horodaté sous <code>.nextblock-backup/</code> dans votre projet. Rien n'est supprimé : les fichiers que vous avez ajoutés ne disparaissent jamais.</li>\\n <li><strong>Installations basées sur git :</strong> la mise à jour est un commit ordinaire. <code>git log</code> l'affiche et <code>git revert</code> l'annule.</li>\\n <li><strong>Une fusion en conflit</strong> est annulée automatiquement — votre copie de travail reste intacte, avec les instructions pour résoudre à la main.</li>\\n <li><strong>Une migration en échec</strong> est annulée. Corrigez la cause et relancez : rien ne reste à moitié appliqué.</li>\\n</ul>\\n<p>Si vous avez personnalisé un fichier appartenant à NextBlock — sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> — la mise à jour le remplacera en sauvegardant votre version. Comparez ensuite la sauvegarde pour reporter votre modification. Les personnalisations qui vivent dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais affectées.</p>\\n\\n<h2 id='knowing'>Savoir qu'une mise à jour est disponible</h2>\\n<p>Inutile de surveiller. NextBlock vérifie en arrière-plan pendant que vous utilisez le CMS et affiche une bannière sur le tableau de bord dès qu'une version plus récente est publiée, en indiquant votre version actuelle et celle disponible. Les administrateurs peuvent aussi lancer <code>npm run update -- --check</code> à tout moment.</p>\\n\\n<h2 id='faq'>FAQ des mises à jour</h2>\\n<h3>La mise à jour va-t-elle écraser mon contenu ou mes réglages ?</h3>\\n<p>Non. Contenus, médias, utilisateurs et réglages vivent dans votre base de données ; la configuration du site vit dans vos variables d'environnement. La mise à jour ne touche que le code, les dépendances et la structure du schéma.</p>\\n<h3>Dois-je installer chaque version ?</h3>\\n<p>Non, mais rester proche de la dernière version vous garantit les correctifs de sécurité et rend chaque saut plus petit. Les migrations s'appliquent dans l'ordre : sauter plusieurs versions fonctionne malgré tout.</p>\\n<h3>Puis-je l'exécuter en CI ?</h3>\\n<p>Oui — <code>npm run update -- --yes</code> ne pose aucune question et renvoie un code d'erreur si l'étape schéma échoue, pour qu'un pipeline puisse le détecter.</p>\\n<h3>Et si aucune connexion à la base n'est configurée ?</h3>\\n<p>Le code et les dépendances sont tout de même mis à jour ; l'étape schéma est ignorée avec un avertissement indiquant la variable d'environnement à définir. Relancez ensuite <code>npm run update -- --db-only</code>.</p>\\n<h3>Je suis sur le déploiement Vercel en un clic — dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est entièrement automatique. La commande existe pour mettre à jour <em>tout de suite</em> plutôt qu'à minuit, ou lorsque vous travaillez sur un clone local.</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'>Une commande, toutes les installations.</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Vous débutez avec NextBlock ? Commencez par le guide d'installation — puis oubliez les mises à jour.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='/article/comment-configurer-nextblock' 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'>Lire le guide d'installation</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\\n </div>\\n</div>\"}\n$nbfr$::jsonb, 0);\n END IF;\nEND\n$body$;\n\n-- Revision baseline for the two new posts.\n--\n-- 00000000000016 back-fills a 'snapshot' revision for every post that existed WHEN IT\n-- RAN. On a fresh install migrations run in order, so 016 executes before this file and\n-- these two posts would be the only ones in the CMS without a restore point. This is the\n-- same projection as 016 section 3b, scoped to the two slugs.\nINSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)\nSELECT\n po.id,\n NULL::uuid,\n po.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', po.title,\n 'slug', po.slug,\n 'language_id', po.language_id,\n 'status', po.status,\n 'meta_title', po.meta_title,\n 'meta_description', po.meta_description,\n 'custom_canonical', po.custom_canonical,\n 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', po.feature_image_id,\n 'label', po.label,\n 'excerpt', po.excerpt,\n 'subtitle', po.subtitle\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.post_id = po.id\n ), '[]'::jsonb)\n )\n FROM public.posts po\n WHERE po.slug IN ('how-updating-works', 'comment-fonctionnent-les-mises-a-jour')\n AND NOT EXISTS (\n SELECT 1 FROM public.post_revisions r\n WHERE r.post_id = po.id\n AND r.revision_type = 'snapshot'\n AND r.version <= po.version\n )\nON CONFLICT (post_id, version) DO NOTHING;\n"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"version": "00000000000021",
|
|
124
|
+
"name": "00000000000021_updating_article_git_merge.sql",
|
|
125
|
+
"sql": "-- 00000000000021_updating_article_git_merge.sql\n-- Corrects the \"How Updating NextBlock Works\" article seeded in 00000000000020.\n--\n-- That version described the standalone update as \"replace the files and keep a backup\n-- under .nextblock-backup/\". The updater now performs a real git 3-way merge instead:\n-- both template versions are committed into the project's own object database under\n-- refs/nextblock/{base,head}, and the diff between them is applied with `git apply --3way`.\n-- A developer's edits to framework files are preserved and only genuine overlaps conflict.\n-- The copy-and-back-up path survives only as the fallback for a project with no git repo,\n-- no commits, or a dirty tree.\n--\n-- 020 is already applied everywhere and never replays, so the copy is corrected here as\n-- targeted, idempotent string replacements rather than a full re-write of the body: each\n-- replace() is a no-op once the old sentence is gone, so re-running changes nothing.\n-- Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n ---------------------------------------------------------------------------\n -- English\n ---------------------------------------------------------------------------\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 1. the \"if something goes wrong\" bullet\n '<li><strong>Standalone projects:</strong> every framework file the update replaces is copied first into a timestamped folder under <code>.nextblock-backup/</code> in your project. Nothing is deleted, so files you added yourself are never removed.</li>',\n '<li><strong>Standalone projects:</strong> the update is applied as a <strong>git 3-way merge</strong> into your working tree — nothing is committed for you. Review it with <code>git diff</code>, and undo the whole thing with <code>git reset --hard HEAD</code>. Nothing is ever deleted, so files you added yourself are never removed.</li>'\n ),\n -- 2. the \"you customised a framework file\" paragraph\n '<p>If you have customised a file that NextBlock owns — something under <code>app/</code>, <code>components/</code> or <code>lib/</code> — the update will replace it and back up your version. Diff the backup afterwards to bring your change forward. Customisations that live in your own new files, in the CMS, or in <code>.env</code> are never affected.</p>',\n '<p>If you have customised a file that NextBlock owns — something under <code>app/</code>, <code>components/</code> or <code>lib/</code> — <strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts, with ordinary <code><<<<<<< ours</code> / <code>>>>>>>> theirs</code> markers. List them with <code>git diff --name-only --diff-filter=U</code> and resolve with <code>git checkout --theirs</code> or <code>--ours</code>. Customisations in your own files, in the CMS, or in <code>.env</code> are never touched at all.</p>'\n ),\n -- 3. the managed-cloud section\n '<p>Your project is a standalone Next.js app, so new framework code is fetched from the published <code>create-nextblock</code> package on npm — the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock refreshes the files it owns, merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and then applies migrations.</p>',\n '<p>Your project is a standalone Next.js app with no upstream to pull from, so new framework code comes from the published <code>create-nextblock</code> package on npm — the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock fetches both your current version and the new one, and applies the difference between them as a <strong>git 3-way merge</strong>, so the update behaves exactly like a <code>git pull</code>: files you never touched update silently, files you customised keep your changes. It then merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and applies migrations.</p><p>This needs a git repository with at least one commit and a clean working tree — commit your work before updating. Without that there is nothing to merge against, so the files are copied instead and anything replaced is kept under <code>.nextblock-backup/</code>.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n ---------------------------------------------------------------------------\n -- French\n ---------------------------------------------------------------------------\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<li><strong>Projets autonomes :</strong> chaque fichier remplacé est d''abord copié dans un dossier horodaté sous <code>.nextblock-backup/</code> dans votre projet. Rien n''est supprimé : les fichiers que vous avez ajoutés ne disparaissent jamais.</li>',\n '<li><strong>Projets autonomes :</strong> la mise à jour est appliquée comme une <strong>fusion git à trois voies</strong> dans votre copie de travail — rien n''est validé à votre place. Examinez-la avec <code>git diff</code>, et annulez tout avec <code>git reset --hard HEAD</code>. Rien n''est jamais supprimé : les fichiers que vous avez ajoutés ne disparaissent jamais.</li>'\n ),\n '<p>Si vous avez personnalisé un fichier appartenant à NextBlock — sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> — la mise à jour le remplacera en sauvegardant votre version. Comparez ensuite la sauvegarde pour reporter votre modification. Les personnalisations qui vivent dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais affectées.</p>',\n '<p>Si vous avez personnalisé un fichier appartenant à NextBlock — sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> — <strong>votre modification est conservée</strong>. La mise à jour fusionne le changement amont dans votre version, et seul un changement qui chevauche réellement le vôtre entre en conflit, avec les marqueurs habituels <code><<<<<<< ours</code> / <code>>>>>>>> theirs</code>. Listez-les avec <code>git diff --name-only --diff-filter=U</code> et résolvez avec <code>git checkout --theirs</code> ou <code>--ours</code>. Les personnalisations dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais touchées.</p>'\n ),\n '<p>Votre projet est une application Next.js autonome : le nouveau code provient donc du paquet <code>create-nextblock</code> publié sur npm — exactement l''artefact à partir duquel votre projet a été généré, versionné en phase avec la release. NextBlock rafraîchit les fichiers qui lui appartiennent, fusionne les nouvelles versions de dépendances dans votre <code>package.json</code>, lance <code>npm install</code>, puis applique les migrations.</p>',\n '<p>Votre projet est une application Next.js autonome sans dépôt amont à tirer : le nouveau code provient donc du paquet <code>create-nextblock</code> publié sur npm — exactement l''artefact à partir duquel votre projet a été généré, versionné en phase avec la release. NextBlock récupère votre version actuelle et la nouvelle, puis applique la différence entre les deux comme une <strong>fusion git à trois voies</strong> : la mise à jour se comporte donc exactement comme un <code>git pull</code> — les fichiers que vous n''avez jamais touchés se mettent à jour silencieusement, ceux que vous avez personnalisés conservent vos changements. Elle fusionne ensuite les nouvelles versions de dépendances dans votre <code>package.json</code>, lance <code>npm install</code> et applique les migrations.</p><p>Cela nécessite un dépôt git avec au moins un commit et une copie de travail propre — validez votre travail avant de mettre à jour. Sans cela il n''y a rien contre quoi fusionner : les fichiers sont alors copiés et tout ce qui est remplacé est conservé sous <code>.nextblock-backup/</code>.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"version": "00000000000022",
|
|
129
|
+
"name": "00000000000022_updating_article_accuracy.sql",
|
|
130
|
+
"sql": "-- 00000000000022_updating_article_accuracy.sql\n-- Three accuracy fixes to the \"How Updating NextBlock Works\" article\n-- (seeded in 00000000000020, first corrected in 00000000000021).\n--\n-- 0. The merge is performed with `git merge-file`, not `git apply --3way`. The latter\n-- implies --index: it stages its result (so `git diff` shows the developer nothing),\n-- requires every path to be tracked (one gitignored framework path aborted the whole\n-- update), and requires the worktree to match the index. merge-file touches no git\n-- state at all. Consequently the resolution commands 021 shipped are wrong: there are\n-- no index stages, so `git checkout --theirs/--ours` does not apply. The conflict\n-- markers are ordinary text; `git checkout -- <file>` discards one file's merge.\n--\n-- 1. \"A conflicted merge is aborted automatically\" was true of only ONE path. The\n-- monorepo/fork path does abort and restore the tree, because the merge belongs to\n-- upstream. The standalone path deliberately LEAVES the conflict in the working tree,\n-- because it is the developer's own repository and resolving it is the whole point.\n-- The bullet now states both.\n-- 2. The Docker section claimed `npm run update` refreshes the schema. It does not, by\n-- design: the self-hosted stack ships its own migration runner (the `migrate` service\n-- in docker-compose.yml, which tracks applied versions in a different table), so the\n-- updater stages the SQL and hands off to `npm run docker:up` rather than applying it\n-- twice through two different trackers.\n--\n-- Same targeted, idempotent replace() approach as 021 — a no-op once the old sentence is\n-- gone. Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 021 shipped index-based resolution commands; the merge no longer\n -- uses the git index, so they do not apply.\n '<strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts, with ordinary <code><<<<<<< ours</code> / <code>>>>>>>> theirs</code> markers. List them with <code>git diff --name-only --diff-filter=U</code> and resolve with <code>git checkout --theirs</code> or <code>--ours</code>.',\n '<strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts — the updater lists those files, and each one carries ordinary <code><<<<<<< your version</code> / <code>>>>>>>> NextBlock</code> markers. Edit them as you would any conflict, or run <code>git checkout -- <file></code> to discard the merge for that one file.'\n ),\n '<li><strong>A conflicted merge</strong> is aborted automatically — your working tree is left exactly as it was, with instructions printed for resolving it by hand.</li>',\n '<li><strong>A conflict</strong> behaves differently by install, on purpose. On a fork or clone the upstream merge is <em>aborted</em> and your working tree is left exactly as it was. On a standalone project the conflict is <em>left in place</em> for you to resolve — it is your own repository, and that is the point — and <code>git reset --hard HEAD</code> backs the whole update out.</li>'\n ),\n '<p>The first command refreshes the application, its dependencies and the schema; the second rebuilds and restarts the containers. Your database and media live in Docker volumes and are never touched by either step — <code>docker:up</code> rebuilds images, not data.</p>',\n '<p>The first command updates the application and its dependencies and stages the new migrations; the second rebuilds the containers <em>and applies those migrations</em>. The self-hosted stack runs its own migration service, so the updater hands the schema step to it rather than applying the same SQL through two different trackers. Your database and media live in Docker volumes and are never touched by either command — <code>docker:up</code> rebuilds images, not data.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<strong>votre modification est conservée</strong>. La mise à jour fusionne le changement amont dans votre version, et seul un changement qui chevauche réellement le vôtre entre en conflit, avec les marqueurs habituels <code><<<<<<< ours</code> / <code>>>>>>>> theirs</code>. Listez-les avec <code>git diff --name-only --diff-filter=U</code> et résolvez avec <code>git checkout --theirs</code> ou <code>--ours</code>.',\n '<strong>votre modification est conservée</strong>. La mise à jour fusionne le changement amont dans votre version, et seul un changement qui chevauche réellement le vôtre entre en conflit — la commande liste ces fichiers, et chacun porte les marqueurs habituels <code><<<<<<< your version</code> / <code>>>>>>>> NextBlock</code>. Modifiez-les comme n''importe quel conflit, ou lancez <code>git checkout -- <fichier></code> pour abandonner la fusion sur ce seul fichier.'\n ),\n '<li><strong>Une fusion en conflit</strong> est annulée automatiquement — votre copie de travail reste intacte, avec les instructions pour résoudre à la main.</li>',\n '<li><strong>Un conflit</strong> se comporte différemment selon l''installation, volontairement. Sur un fork ou un clone, la fusion amont est <em>annulée</em> et votre copie de travail reste intacte. Sur un projet autonome, le conflit est <em>laissé en place</em> pour que vous le résolviez — c''est votre dépôt, et c''est tout l''intérêt — et <code>git reset --hard HEAD</code> annule toute la mise à jour.</li>'\n ),\n '<p>La première commande met à jour l''application, ses dépendances et le schéma ; la seconde reconstruit et redémarre les conteneurs. Votre base de données et vos médias vivent dans des volumes Docker et ne sont touchés ni par l''une ni par l''autre — <code>docker:up</code> reconstruit des images, pas des données.</p>',\n '<p>La première commande met à jour l''application et ses dépendances et prépare les nouvelles migrations ; la seconde reconstruit les conteneurs <em>et applique ces migrations</em>. La pile auto-hébergée dispose de son propre service de migration : la mise à jour lui confie donc l''étape schéma plutôt que d''appliquer le même SQL via deux suivis différents. Votre base de données et vos médias vivent dans des volumes Docker et ne sont touchés par aucune des deux commandes — <code>docker:up</code> reconstruit des images, pas des données.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"version": "00000000000023",
|
|
134
|
+
"name": "00000000000023_updating_article_layout_not_host.sql",
|
|
135
|
+
"sql": "-- 00000000000023_updating_article_layout_not_host.sql\n-- Corrects the most misleading claim in the \"How Updating NextBlock Works\" article\n-- (seeded 00000000000020, corrected in 021 and 022): that the automatic GitHub Action is\n-- about being deployed on Vercel.\n--\n-- It is not. The upstream-sync Action merges the NextBlock MONOREPO into the repository,\n-- so it only works where the repository IS the monorepo — a Vercel 1-click deploy, a\n-- GitHub fork, or a clone. A project scaffolded by `npm create nextblock` is the flattened\n-- standalone app (app/, components/, lib/ at the root); merging apps/ + libs/ + nx.json\n-- into it would wreck it. Pushing that project to GitHub and deploying it on Vercel does\n-- not change its layout, so it is still a `npm run update` install. Docker is orthogonal:\n-- it is how you RUN a project, not what shape the repository is.\n--\n-- Also documents that a merge conflict now holds the migration step back until the\n-- conflict is resolved, so the schema never moves ahead of undecided code.\n--\n-- Targeted, idempotent replace() as in 021/022. Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 1. The section intro: say what actually qualifies.\n '<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard’s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository’s <strong>Actions</strong> tab.</p>',\n '<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard’s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository’s <strong>Actions</strong> tab.</p>\\n<div class=''rounded-3xl border border-slate-200 bg-slate-50 p-6 my-8 dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300''>What qualifies — it is the repository, not the host</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>The workflow merges the <strong>NextBlock monorepo</strong> into your repository, so it only works where your repository <em>is</em> that monorepo: a one-click deploy, a GitHub fork, or a clone. A project created with <code>npm create nextblock</code> is the flattened standalone app — <code>app/</code>, <code>components/</code> and <code>lib/</code> at the root — and merging <code>apps/</code>, <code>libs/</code> and <code>nx.json</code> into it would wreck it. Pushing that project to GitHub and deploying it on Vercel does not change its shape: it is still an <code>npm run update</code> install, and NextBlock will not offer it this workflow. Docker is a separate question entirely — that is how you <em>run</em> a project, not what shape its repository is.</p>\\n</div>'\n ),\n -- 2. The FAQ answer, which asked exactly the question this clarifies.\n '<h3>I am on the one-click Vercel deploy — do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>',\n '<h3>I am on the one-click Vercel deploy — do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>\\n<h3>I deployed to Vercel, but from <code>npm create nextblock</code>. Is that automatic too?</h3>\\n<p>No — and this is the distinction that catches people out. Automatic updates depend on your repository being the NextBlock <strong>monorepo</strong>, not on where the site is hosted. A project scaffolded by the CLI is the flattened standalone app whatever you deploy it to, so it updates with <code>npm run update</code>. You will not see the <strong>Connect GitHub</strong> step on that kind of install, because the workflow it installs would merge a completely different source tree into yours.</p>'\n ),\n -- 3. Conflicts hold the schema step.\n '<li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>',\n '<li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>\\n <li><strong>Unresolved conflicts hold the database back.</strong> If a merge left conflicts, the update finishes the code and dependency work but <em>stops before migrating</em> — your schema never moves ahead of code you have not finished deciding on. Resolve them and run <code>npm run update</code> again to apply the migrations, or walk away with <code>git reset --hard HEAD</code>; either way the database was never touched.</li>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<p>Ce chemin se met à jour tout seul. Lors du déploiement, NextBlock a créé un dépôt qui vous appartient ; l''étape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s''exécute <strong>chaque jour à minuit UTC</strong> et peut aussi être lancé à la demande depuis l''onglet <strong>Actions</strong> de votre dépôt.</p>',\n '<p>Ce chemin se met à jour tout seul. Lors du déploiement, NextBlock a créé un dépôt qui vous appartient ; l''étape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s''exécute <strong>chaque jour à minuit UTC</strong> et peut aussi être lancé à la demande depuis l''onglet <strong>Actions</strong> de votre dépôt.</p>\\n<div class=''rounded-3xl border border-slate-200 bg-slate-50 p-6 my-8 dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300''>Ce qui compte : le dépôt, pas l''hébergeur</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Le workflow fusionne le <strong>monorepo NextBlock</strong> dans votre dépôt : il ne fonctionne donc que si votre dépôt <em>est</em> ce monorepo — déploiement en un clic, fork GitHub ou clone. Un projet créé avec <code>npm create nextblock</code> est l''application autonome aplatie — <code>app/</code>, <code>components/</code> et <code>lib/</code> à la racine — et y fusionner <code>apps/</code>, <code>libs/</code> et <code>nx.json</code> le casserait. Pousser ce projet sur GitHub et le déployer sur Vercel ne change pas sa forme : il se met toujours à jour avec <code>npm run update</code>, et NextBlock ne lui proposera pas ce workflow. Docker est une tout autre question — c''est la façon d''<em>exécuter</em> un projet, pas la forme de son dépôt.</p>\\n</div>'\n ),\n '<h3>Je suis sur le déploiement Vercel en un clic — dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est entièrement automatique. La commande existe pour mettre à jour <em>tout de suite</em> plutôt qu''à minuit, ou lorsque vous travaillez sur un clone local.</p>',\n '<h3>Je suis sur le déploiement Vercel en un clic — dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est entièrement automatique. La commande existe pour mettre à jour <em>tout de suite</em> plutôt qu''à minuit, ou lorsque vous travaillez sur un clone local.</p>\\n<h3>J''ai déployé sur Vercel, mais depuis <code>npm create nextblock</code>. Est-ce automatique aussi ?</h3>\\n<p>Non — et c''est la distinction qui piège le plus. Les mises à jour automatiques dépendent du fait que votre dépôt soit le <strong>monorepo</strong> NextBlock, pas de l''endroit où le site est hébergé. Un projet généré par le CLI reste l''application autonome aplatie, quel que soit l''hébergeur : il se met à jour avec <code>npm run update</code>. L''étape <strong>Connect GitHub</strong> ne s''affiche pas sur ce type d''installation, car le workflow qu''elle installe fusionnerait une arborescence totalement différente dans la vôtre.</p>'\n ),\n '<li><strong>Une migration en échec</strong> est annulée. Corrigez la cause et relancez : rien ne reste à moitié appliqué.</li>',\n '<li><strong>Une migration en échec</strong> est annulée. Corrigez la cause et relancez : rien ne reste à moitié appliqué.</li>\\n <li><strong>Les conflits non résolus bloquent la base.</strong> Si une fusion a laissé des conflits, la mise à jour termine le code et les dépendances mais <em>s''arrête avant les migrations</em> — votre schéma ne prend jamais de l''avance sur un code que vous n''avez pas fini d''arbitrer. Résolvez-les puis relancez <code>npm run update</code> pour appliquer les migrations, ou abandonnez avec <code>git reset --hard HEAD</code> : dans les deux cas la base n''a jamais été touchée.</li>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"version": "00000000000024",
|
|
139
|
+
"name": "00000000000024_updating_article_newline_fix.sql",
|
|
140
|
+
"sql": "-- 00000000000024_updating_article_newline_fix.sql\n-- Repairs two mistakes made by 00000000000023 in the \"How Updating NextBlock Works\"\n-- article, and lands the FAQ entry that migration failed to insert.\n--\n-- 1. RENDERING BUG. 023 wrote `\\n` inside ordinary single-quoted SQL literals. With\n-- standard_conforming_strings on (the default), that is a literal backslash followed\n-- by 'n' — not a newline — so five visible \"\\n\" sequences were stored in the article\n-- body. Replaced here with real newlines via chr(10). Idempotent: once none remain,\n-- replace() is a no-op.\n--\n-- 2. SILENT NO-MATCH. 023's FAQ replacement targeted a string spanning `</h3>\\n<p>`, and\n-- for the same reason the literal never matched the real newline in the stored HTML, so\n-- the replacement quietly did nothing. Redone here by anchoring on the single-line <h3>\n-- alone and prepending the new entry — no newline in either the search or the\n-- replacement, which is the rule this file establishes for editing the article.\n--\n-- Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n -- (1) literal backslash-n -> real newline\n replace(content->>'html_content', E'\\\\n', chr(10)),\n -- (2) the FAQ entry 023 failed to insert\n '<h3>I am on the one-click Vercel deploy — do I need to run anything?</h3>',\n '<h3>I deployed to Vercel, but from <code>npm create nextblock</code>. Is that automatic too?</h3><p>No — and this is the distinction that catches people out. Automatic updates depend on your repository being the NextBlock <strong>monorepo</strong>, not on where the site is hosted. A project scaffolded by the CLI is the flattened standalone app whatever you deploy it to, so it updates with <code>npm run update</code>. You will not see the <strong>Connect GitHub</strong> step on that kind of install, because the workflow it installs would merge a completely different source tree into yours.</p><h3>I am on the one-click Vercel deploy — do I need to run anything?</h3>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(content->>'html_content', E'\\\\n', chr(10)),\n '<h3>Je suis sur le déploiement Vercel en un clic — dois-je lancer quelque chose ?</h3>',\n '<h3>J''ai déployé sur Vercel, mais depuis <code>npm create nextblock</code>. Est-ce automatique aussi ?</h3><p>Non — et c''est la distinction qui piège le plus. Les mises à jour automatiques dépendent du fait que votre dépôt soit le <strong>monorepo</strong> NextBlock, pas de l''endroit où le site est hébergé. Un projet généré par le CLI reste l''application autonome aplatie, quel que soit l''hébergeur : il se met à jour avec <code>npm run update</code>. L''étape <strong>Connect GitHub</strong> ne s''affiche pas sur ce type d''installation, car le workflow qu''elle installe fusionnerait une arborescence totalement différente dans la vôtre.</p><h3>Je suis sur le déploiement Vercel en un clic — dois-je lancer quelque chose ?</h3>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
|
|
106
141
|
}
|
|
107
142
|
];
|
|
@@ -23,6 +23,12 @@ 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
|
+
// The npm registry is the authoritative version signal for standalone installs: the
|
|
27
|
+
// `create-nextblock` package ships the exact standalone template they were scaffolded
|
|
28
|
+
// from and is published in lockstep with the app, whereas GitHub release tags are cut by
|
|
29
|
+
// hand and lag (they have, in practice, sat many minor versions behind the real version —
|
|
30
|
+
// which silently disabled this whole check). Releases stay as the fallback.
|
|
31
|
+
const NPM_REGISTRY_API = 'https://registry.npmjs.org/create-nextblock/latest';
|
|
26
32
|
// The sync workflow tags its conflict issues with this hidden body marker. We match on it
|
|
27
33
|
// (not on a label) so a label that failed to create on GitHub can't hide a real conflict.
|
|
28
34
|
const CONFLICT_MARKER = '<!-- nextblock-sync-conflict -->';
|
|
@@ -87,18 +93,90 @@ function compareSemver(a: string, b: string): number {
|
|
|
87
93
|
return 0;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The NextBlock version this install is running.
|
|
98
|
+
*
|
|
99
|
+
* `package.json.nextblock.version` is the authoritative stamp, written by the scaffolder
|
|
100
|
+
* and re-written by `npm run update`. A project's own `version` field belongs to the
|
|
101
|
+
* user, and the moment they bump it for their own site — entirely normal — comparing a
|
|
102
|
+
* release against it becomes meaningless. Fall back to it only for projects created
|
|
103
|
+
* before the stamp existed.
|
|
104
|
+
*/
|
|
105
|
+
function readInstalledVersion(): string {
|
|
106
|
+
const manifest = pkg as { version?: string; nextblock?: { version?: string } };
|
|
107
|
+
const stamped = manifest.nextblock?.version?.trim();
|
|
108
|
+
return stamped || manifest.version || '0.0.0';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Latest published `create-nextblock` version, or null when the registry is unreachable. */
|
|
112
|
+
async function fetchLatestFromNpm(): Promise<string | null> {
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(NPM_REGISTRY_API, {
|
|
115
|
+
headers: { Accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
116
|
+
signal: AbortSignal.timeout(15_000),
|
|
117
|
+
next: { revalidate: 3600 },
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) return null;
|
|
120
|
+
const body = (await res.json()) as { version?: string };
|
|
121
|
+
const version = body.version?.trim();
|
|
122
|
+
return version && /^\d+\.\d+\.\d+/.test(version) ? version : null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Is this install the NextBlock MONOREPO (a Vercel 1-click deploy, a GitHub fork, or a
|
|
130
|
+
* clone) rather than the flattened standalone app that `npm create nextblock` produces?
|
|
131
|
+
*
|
|
132
|
+
* This — not the hosting platform — is what decides whether the upstream-sync GitHub
|
|
133
|
+
* Action can work, because that Action merges the monorepo (`apps/`, `libs/`, `tools/`,
|
|
134
|
+
* `nx.json`) into the repository. Merging it into a flat standalone project, whose tree is
|
|
135
|
+
* `app/`, `components/`, `lib/`, would wreck the project. A standalone app pushed to GitHub
|
|
136
|
+
* and deployed on Vercel is still standalone; where it is hosted is irrelevant.
|
|
137
|
+
*
|
|
138
|
+
* The marker lives in the bundled package.json (`nextblock.install`) so it is readable on a
|
|
139
|
+
* serverless filesystem, where neither `nx.json` nor `.github/` is traced into the function.
|
|
140
|
+
* Projects scaffolded before that marker existed fall back to filesystem probes and then to
|
|
141
|
+
* 'standalone' — which at worst shows a 1-click deploy an extra update banner, and is the
|
|
142
|
+
* safe default: it never offers to install the Action where it would do damage.
|
|
143
|
+
*/
|
|
144
|
+
export function isMonorepoInstall(): boolean {
|
|
145
|
+
const manifest = pkg as { nextblock?: { install?: string } };
|
|
146
|
+
const declared = manifest.nextblock?.install;
|
|
147
|
+
if (declared === 'monorepo') return true;
|
|
148
|
+
if (declared === 'standalone') return false;
|
|
149
|
+
|
|
150
|
+
const cwd = process.cwd();
|
|
151
|
+
try {
|
|
152
|
+
if (existsSync(path.join(cwd, 'nx.json'))) return true;
|
|
153
|
+
} catch {
|
|
154
|
+
/* ignore */
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
if (existsSync(path.join(cwd, '.github', 'workflows', 'nextblock-sync.yml'))) return true;
|
|
158
|
+
} catch {
|
|
159
|
+
/* ignore */
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
90
164
|
/**
|
|
91
165
|
* Classify how this install receives updates. Explicit NEXTBLOCK_UPDATE_TRACK wins;
|
|
92
|
-
* otherwise
|
|
93
|
-
*
|
|
166
|
+
* otherwise a monorepo-shaped install with a resolvable GitHub repo is 'git' (Track A —
|
|
167
|
+
* the daily Action merges upstream for it), and everything else is 'standalone' (Track B —
|
|
168
|
+
* it needs `npm run update`).
|
|
169
|
+
*
|
|
170
|
+
* Deliberately NOT keyed on `process.env.VERCEL`: a standalone project deployed to Vercel
|
|
171
|
+
* used to be classified 'git', which suppressed its update banner entirely while no Action
|
|
172
|
+
* existed to update it — leaving it silently frozen.
|
|
94
173
|
*/
|
|
95
174
|
function detectTrack(): UpdateTrack {
|
|
96
175
|
const override = process.env.NEXTBLOCK_UPDATE_TRACK?.trim().toLowerCase();
|
|
97
176
|
if (override === 'git' || override === 'standalone') return override;
|
|
177
|
+
if (!isMonorepoInstall()) return 'standalone';
|
|
98
178
|
|
|
99
|
-
if (process.env.VERCEL === '1') return 'git';
|
|
100
179
|
if (resolveSelfRepo()) {
|
|
101
|
-
// A resolvable GitHub repo identity means git-backed; double-check it's a NextBlock fork.
|
|
102
180
|
const cwd = process.cwd();
|
|
103
181
|
try {
|
|
104
182
|
if (existsSync(path.join(cwd, '.github', 'workflows', 'nextblock-sync.yml'))) return 'git';
|
|
@@ -111,16 +189,77 @@ function detectTrack(): UpdateTrack {
|
|
|
111
189
|
} catch {
|
|
112
190
|
/* ignore */
|
|
113
191
|
}
|
|
192
|
+
// Monorepo-shaped and GitHub-hosted, but the workflow isn't visible from here (normal
|
|
193
|
+
// on a serverless filesystem). Trust the layout marker.
|
|
194
|
+
return 'git';
|
|
114
195
|
}
|
|
115
196
|
return 'standalone';
|
|
116
197
|
}
|
|
117
198
|
|
|
118
199
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
200
|
+
* Determine the newest published NextBlock version. The npm registry is authoritative
|
|
201
|
+
* (see NPM_REGISTRY_API); GitHub Releases is the fallback and also supplies the release
|
|
202
|
+
* notes / archive links when a release exists.
|
|
203
|
+
*/
|
|
204
|
+
async function resolveLatestRelease(): Promise<{
|
|
205
|
+
latestVersion: string | null;
|
|
206
|
+
release: {
|
|
207
|
+
tag_name?: string;
|
|
208
|
+
html_url?: string;
|
|
209
|
+
tarball_url?: string;
|
|
210
|
+
zipball_url?: string;
|
|
211
|
+
published_at?: string;
|
|
212
|
+
} | null;
|
|
213
|
+
error?: string;
|
|
214
|
+
}> {
|
|
215
|
+
const [npmVersion, githubResult] = await Promise.all([
|
|
216
|
+
fetchLatestFromNpm(),
|
|
217
|
+
(async () => {
|
|
218
|
+
try {
|
|
219
|
+
const res = await fetch(RELEASES_API, {
|
|
220
|
+
headers: githubHeaders(),
|
|
221
|
+
signal: AbortSignal.timeout(15_000),
|
|
222
|
+
next: { revalidate: 3600 },
|
|
223
|
+
});
|
|
224
|
+
if (res.status === 404) return { release: null }; // no releases cut yet
|
|
225
|
+
if (!res.ok) return { release: null, error: `GitHub Releases API returned HTTP ${res.status}.` };
|
|
226
|
+
return { release: await res.json() };
|
|
227
|
+
} catch (caught) {
|
|
228
|
+
return {
|
|
229
|
+
release: null,
|
|
230
|
+
error:
|
|
231
|
+
caught instanceof Error
|
|
232
|
+
? `Could not reach the GitHub Releases API: ${caught.message}`
|
|
233
|
+
: 'Could not reach the GitHub Releases API.',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
})(),
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const tagVersion = githubResult.release?.tag_name?.trim().replace(/^v/i, '') || null;
|
|
240
|
+
// Whichever source is ahead wins: a hand-cut tag can lag npm, and npm can lag a
|
|
241
|
+
// release that was published before the packages went out.
|
|
242
|
+
const latestVersion =
|
|
243
|
+
npmVersion && tagVersion
|
|
244
|
+
? compareSemver(npmVersion, tagVersion) >= 0
|
|
245
|
+
? npmVersion
|
|
246
|
+
: tagVersion
|
|
247
|
+
: (npmVersion ?? tagVersion);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
latestVersion,
|
|
251
|
+
release: githubResult.release,
|
|
252
|
+
// Only surface the GitHub error when npm did not answer either.
|
|
253
|
+
error: latestVersion ? undefined : githubResult.error,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Resolve the newest published version and, on a standalone install that is behind,
|
|
259
|
+
* record a runtime_update_available alert (deduped by latest version). Never throws.
|
|
121
260
|
*/
|
|
122
261
|
export async function checkForUpstreamUpdate(): Promise<UpstreamUpdateResult> {
|
|
123
|
-
const currentVersion =
|
|
262
|
+
const currentVersion = readInstalledVersion();
|
|
124
263
|
const track = detectTrack();
|
|
125
264
|
const base: UpstreamUpdateResult = {
|
|
126
265
|
ok: false,
|
|
@@ -131,40 +270,15 @@ export async function checkForUpstreamUpdate(): Promise<UpstreamUpdateResult> {
|
|
|
131
270
|
alertRecorded: false,
|
|
132
271
|
};
|
|
133
272
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
tarball_url?: string;
|
|
138
|
-
zipball_url?: string;
|
|
139
|
-
published_at?: string;
|
|
140
|
-
};
|
|
141
|
-
try {
|
|
142
|
-
const res = await fetch(RELEASES_API, {
|
|
143
|
-
headers: githubHeaders(),
|
|
144
|
-
signal: AbortSignal.timeout(15_000),
|
|
145
|
-
next: { revalidate: 3600 },
|
|
146
|
-
});
|
|
147
|
-
if (res.status === 404) return { ...base, ok: true }; // no releases yet
|
|
148
|
-
if (!res.ok) return { ...base, error: `GitHub Releases API returned HTTP ${res.status}.` };
|
|
149
|
-
release = await res.json();
|
|
150
|
-
} catch (caught) {
|
|
151
|
-
return {
|
|
152
|
-
...base,
|
|
153
|
-
error:
|
|
154
|
-
caught instanceof Error
|
|
155
|
-
? `Could not reach the GitHub Releases API: ${caught.message}`
|
|
156
|
-
: 'Could not reach the GitHub Releases API.',
|
|
157
|
-
};
|
|
158
|
-
}
|
|
273
|
+
const { latestVersion, release, error } = await resolveLatestRelease();
|
|
274
|
+
if (error) return { ...base, error };
|
|
275
|
+
if (!latestVersion) return { ...base, ok: true }; // nothing published yet
|
|
159
276
|
|
|
160
|
-
const tag = release
|
|
161
|
-
if (!tag) return { ...base, ok: true };
|
|
162
|
-
|
|
163
|
-
const latestVersion = tag.replace(/^v/i, '');
|
|
277
|
+
const tag = release?.tag_name?.trim() || `v${latestVersion}`;
|
|
164
278
|
const tarballUrl =
|
|
165
|
-
release
|
|
279
|
+
release?.tarball_url || `https://github.com/${UPSTREAM_REPO}/archive/refs/tags/${tag}.tar.gz`;
|
|
166
280
|
const zipballUrl =
|
|
167
|
-
release
|
|
281
|
+
release?.zipball_url || `https://github.com/${UPSTREAM_REPO}/archive/refs/tags/${tag}.zip`;
|
|
168
282
|
const updateAvailable = compareSemver(latestVersion, currentVersion) > 0;
|
|
169
283
|
|
|
170
284
|
const result: UpstreamUpdateResult = {
|
|
@@ -172,10 +286,10 @@ export async function checkForUpstreamUpdate(): Promise<UpstreamUpdateResult> {
|
|
|
172
286
|
ok: true,
|
|
173
287
|
latestVersion,
|
|
174
288
|
updateAvailable,
|
|
175
|
-
htmlUrl: release
|
|
289
|
+
htmlUrl: release?.html_url,
|
|
176
290
|
tarballUrl,
|
|
177
291
|
zipballUrl,
|
|
178
|
-
publishedAt: release
|
|
292
|
+
publishedAt: release?.published_at,
|
|
179
293
|
};
|
|
180
294
|
|
|
181
295
|
// Git-backed installs auto-merge via Track A — no runtime update alert for them.
|
|
@@ -183,11 +297,15 @@ export async function checkForUpstreamUpdate(): Promise<UpstreamUpdateResult> {
|
|
|
183
297
|
|
|
184
298
|
try {
|
|
185
299
|
const supabase = getServiceRoleSupabaseClient();
|
|
300
|
+
// Deliberately NOT filtered on is_resolved: an admin who dismisses the banner for a
|
|
301
|
+
// given version has answered for that version, and re-inserting it on the next
|
|
302
|
+
// 6-hour poll would make the banner un-dismissable. A genuinely newer version
|
|
303
|
+
// carries a different latest_version and still alerts.
|
|
186
304
|
const { data: existing } = await supabase
|
|
187
305
|
.from('system_alerts')
|
|
188
306
|
.select('id, metadata')
|
|
189
307
|
.eq('alert_type', 'runtime_update_available')
|
|
190
|
-
.
|
|
308
|
+
.order('created_at', { ascending: false })
|
|
191
309
|
.limit(50);
|
|
192
310
|
|
|
193
311
|
const alreadyAlerted = (existing ?? []).some(
|
|
@@ -196,19 +314,22 @@ export async function checkForUpstreamUpdate(): Promise<UpstreamUpdateResult> {
|
|
|
196
314
|
);
|
|
197
315
|
if (alreadyAlerted) return result;
|
|
198
316
|
|
|
199
|
-
const { error } = await supabase.from('system_alerts').insert({
|
|
317
|
+
const { error: insertError } = await supabase.from('system_alerts').insert({
|
|
200
318
|
alert_type: 'runtime_update_available',
|
|
201
319
|
title: `NextBlock ${latestVersion} is available`,
|
|
202
|
-
message: `
|
|
320
|
+
message: `NextBlock ${latestVersion} is out — you are on ${currentVersion}. Run "npm run update" in your project to upgrade the code, dependencies and database schema in one step.`,
|
|
203
321
|
metadata: {
|
|
204
322
|
latest_version: latestVersion,
|
|
205
323
|
current_version: currentVersion,
|
|
324
|
+
update_command: 'npm run update',
|
|
206
325
|
download_url: tarballUrl,
|
|
207
326
|
zipball_url: zipballUrl,
|
|
208
|
-
html_url: release
|
|
327
|
+
html_url: release?.html_url ?? null,
|
|
209
328
|
},
|
|
210
329
|
});
|
|
211
|
-
if (
|
|
330
|
+
if (insertError) {
|
|
331
|
+
return { ...result, error: `Could not record the update alert: ${insertError.message}` };
|
|
332
|
+
}
|
|
212
333
|
return { ...result, alertRecorded: true };
|
|
213
334
|
} catch (caught) {
|
|
214
335
|
return {
|
|
@@ -411,7 +532,7 @@ export async function markSyncWorkflowInstalled(): Promise<void> {
|
|
|
411
532
|
const self = resolveSelfRepo();
|
|
412
533
|
const snapshot: UpstreamStatusSnapshot = {
|
|
413
534
|
checked_at: new Date().toISOString(),
|
|
414
|
-
current_version: prev?.current_version ??
|
|
535
|
+
current_version: prev?.current_version ?? readInstalledVersion(),
|
|
415
536
|
latest_version: prev?.latest_version ?? null,
|
|
416
537
|
update_available: prev?.update_available ?? false,
|
|
417
538
|
track: prev?.track ?? 'git',
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextblock-cms/template",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"private": true,
|
|
5
|
+
"nextblock": {
|
|
6
|
+
"install": "standalone"
|
|
7
|
+
},
|
|
5
8
|
"scripts": {
|
|
6
9
|
"dev": "next dev",
|
|
7
10
|
"prebuild": "node tools/build-migrate.mjs",
|
|
8
11
|
"build": "next build",
|
|
9
12
|
"start": "next start",
|
|
10
13
|
"lint": "next lint",
|
|
14
|
+
"update": "node tools/update.mjs",
|
|
15
|
+
"update:check": "node tools/update.mjs --check",
|
|
11
16
|
"deploy:supabase": "node tools/deploy-supabase.js",
|
|
12
17
|
"configure:supabase-auth": "node tools/configure-supabase-auth.js",
|
|
13
18
|
"docker:setup": "node scripts/docker-setup.mjs",
|