create-nextblock 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/nextblock-template/app/actions/email.ts +4 -3
- package/templates/nextblock-template/app/actions/formActions.ts +51 -42
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +281 -0
- package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +4 -0
- package/templates/nextblock-template/app/checkout/success/actions.ts +2 -1
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +64 -20
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -0
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +118 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +6 -11
- package/templates/nextblock-template/app/cms/layout.tsx +8 -3
- package/templates/nextblock-template/app/cms/settings/email/actions.ts +60 -0
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -0
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -0
- package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -0
- package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -0
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +5 -6
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +0 -48
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +4 -3
- package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -0
- package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -0
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +3 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +20 -0
- package/templates/nextblock-template/app/layout.tsx +5 -1
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +15 -158
- package/templates/nextblock-template/app/setup/page.tsx +0 -8
- package/templates/nextblock-template/components/AppShell.tsx +0 -7
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -1
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +25 -20
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +13 -2
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +11 -0
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +59 -8
- package/templates/nextblock-template/docs/README.md +3 -0
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +11 -13
- package/templates/nextblock-template/lib/auth/twoFactor.ts +41 -0
- package/templates/nextblock-template/lib/config/email-settings.ts +217 -0
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -0
- package/templates/nextblock-template/lib/onboarding/status.ts +136 -0
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -0
- package/templates/nextblock-template/lib/privacy/settings.ts +12 -0
- package/templates/nextblock-template/lib/privacy/types.ts +3 -1
- package/templates/nextblock-template/lib/setup/actions.ts +6 -21
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +10 -0
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -35,14 +35,6 @@ export interface StoragePrefill {
|
|
|
35
35
|
secretAccessKey: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
interface SmtpPrefill {
|
|
39
|
-
host: string;
|
|
40
|
-
port: string;
|
|
41
|
-
user: string;
|
|
42
|
-
fromEmail: string;
|
|
43
|
-
fromName: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
38
|
export interface SupabaseEnvDetected {
|
|
47
39
|
NEXT_PUBLIC_SUPABASE_URL: boolean;
|
|
48
40
|
SUPABASE_URL: boolean;
|
|
@@ -58,20 +50,19 @@ interface Props {
|
|
|
58
50
|
writable: boolean;
|
|
59
51
|
siteUrl: string;
|
|
60
52
|
storagePrefill: StoragePrefill;
|
|
61
|
-
smtpPrefill: SmtpPrefill;
|
|
62
|
-
turnstilePrefill: { siteKey: string };
|
|
63
53
|
/** Which Supabase env vars the running deployment can actually see (read-only channels). */
|
|
64
54
|
supabaseEnvDetected: SupabaseEnvDetected;
|
|
65
55
|
}
|
|
66
56
|
|
|
67
|
-
|
|
57
|
+
// Email (SMTP), bot protection, and sign-up policy are no longer collected here — they
|
|
58
|
+
// moved to the CMS (Settings → Configuration) and are nudged from the dashboard onboarding
|
|
59
|
+
// checklist. A fresh install only needs the database connection, media storage, and the
|
|
60
|
+
// first administrator account.
|
|
61
|
+
type StepId = 'connection' | 'storage' | 'admin';
|
|
68
62
|
|
|
69
63
|
const STEP_TITLES: Record<StepId, string> = {
|
|
70
64
|
connection: 'Database connection',
|
|
71
65
|
storage: 'Media storage',
|
|
72
|
-
email: 'Email (SMTP)',
|
|
73
|
-
bot: 'Bot protection',
|
|
74
|
-
signups: 'Sign-ups',
|
|
75
66
|
admin: 'Administrator account',
|
|
76
67
|
};
|
|
77
68
|
|
|
@@ -89,8 +80,6 @@ export default function SetupWizard({
|
|
|
89
80
|
writable,
|
|
90
81
|
siteUrl,
|
|
91
82
|
storagePrefill,
|
|
92
|
-
smtpPrefill,
|
|
93
|
-
turnstilePrefill,
|
|
94
83
|
supabaseEnvDetected,
|
|
95
84
|
}: Props) {
|
|
96
85
|
const [isPending, startTransition] = useTransition();
|
|
@@ -110,15 +99,8 @@ export default function SetupWizard({
|
|
|
110
99
|
// "Start from a clean database" — only offered/honored on a local fresh install.
|
|
111
100
|
const [resetFirst, setResetFirst] = useState(true);
|
|
112
101
|
|
|
113
|
-
// Storage /
|
|
102
|
+
// Storage / admin.
|
|
114
103
|
const [storage, setStorage] = useState(storagePrefill);
|
|
115
|
-
const [smtp, setSmtp] = useState({ ...smtpPrefill, pass: '' });
|
|
116
|
-
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
|
117
|
-
const [turnstile, setTurnstile] = useState({
|
|
118
|
-
siteKey: turnstilePrefill.siteKey,
|
|
119
|
-
secretKey: '',
|
|
120
|
-
});
|
|
121
|
-
const [autoAccept, setAutoAccept] = useState(false); // off by default (defense-in-depth)
|
|
122
104
|
const [admin, setAdmin] = useState({ email: '', password: '', fullName: '' });
|
|
123
105
|
|
|
124
106
|
const steps = useMemo<StepId[]>(() => {
|
|
@@ -131,7 +113,7 @@ export default function SetupWizard({
|
|
|
131
113
|
if (channel !== 'vercel') {
|
|
132
114
|
list.push('storage');
|
|
133
115
|
}
|
|
134
|
-
list.push('
|
|
116
|
+
list.push('admin');
|
|
135
117
|
return list;
|
|
136
118
|
}, [configured, channel]);
|
|
137
119
|
|
|
@@ -205,12 +187,6 @@ export default function SetupWizard({
|
|
|
205
187
|
// (baseUrl only differs if a channel prefilled a separate custom domain).
|
|
206
188
|
put('NEXT_PUBLIC_R2_PUBLIC_URL', storage.publicUrl);
|
|
207
189
|
put('NEXT_PUBLIC_R2_BASE_URL', storage.baseUrl || storage.publicUrl);
|
|
208
|
-
put('SMTP_HOST', smtp.host);
|
|
209
|
-
put('SMTP_PORT', smtp.port);
|
|
210
|
-
put('SMTP_USER', smtp.user);
|
|
211
|
-
put('SMTP_PASS', smtp.pass);
|
|
212
|
-
put('SMTP_FROM_EMAIL', smtp.fromEmail);
|
|
213
|
-
put('SMTP_FROM_NAME', smtp.fromName);
|
|
214
190
|
}
|
|
215
191
|
|
|
216
192
|
setMessage(null);
|
|
@@ -218,12 +194,8 @@ export default function SetupWizard({
|
|
|
218
194
|
startTransition(async () => {
|
|
219
195
|
const result = await completeSetup({
|
|
220
196
|
admin,
|
|
221
|
-
autoAcceptSignups: autoAccept,
|
|
222
197
|
envValues,
|
|
223
198
|
resetFirst: resetFirst && writable,
|
|
224
|
-
turnstile: turnstileEnabled
|
|
225
|
-
? { provider: 'turnstile', siteKey: turnstile.siteKey, secretKey: turnstile.secretKey }
|
|
226
|
-
: { provider: 'none', siteKey: '', secretKey: '' },
|
|
227
199
|
});
|
|
228
200
|
|
|
229
201
|
if (!result.ok) {
|
|
@@ -400,122 +372,6 @@ export default function SetupWizard({
|
|
|
400
372
|
<StorageStep storage={storage} setStorage={setStorage} channel={channel} />
|
|
401
373
|
)}
|
|
402
374
|
|
|
403
|
-
{current === 'email' && (
|
|
404
|
-
<div className="space-y-4">
|
|
405
|
-
{channel === 'docker' && (
|
|
406
|
-
<p className="text-xs text-muted-foreground">
|
|
407
|
-
Docker auto-confirms sign-ups when SMTP is not configured, so this is optional.
|
|
408
|
-
</p>
|
|
409
|
-
)}
|
|
410
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
411
|
-
<Field label="SMTP host" htmlFor="smtpHost">
|
|
412
|
-
<Input
|
|
413
|
-
id="smtpHost"
|
|
414
|
-
value={smtp.host}
|
|
415
|
-
onChange={(e) => setSmtp({ ...smtp, host: e.target.value })}
|
|
416
|
-
/>
|
|
417
|
-
</Field>
|
|
418
|
-
<Field label="Port" htmlFor="smtpPort">
|
|
419
|
-
<Input
|
|
420
|
-
id="smtpPort"
|
|
421
|
-
value={smtp.port}
|
|
422
|
-
onChange={(e) => setSmtp({ ...smtp, port: e.target.value })}
|
|
423
|
-
/>
|
|
424
|
-
</Field>
|
|
425
|
-
<Field label="Username" htmlFor="smtpUser">
|
|
426
|
-
<Input
|
|
427
|
-
id="smtpUser"
|
|
428
|
-
value={smtp.user}
|
|
429
|
-
onChange={(e) => setSmtp({ ...smtp, user: e.target.value })}
|
|
430
|
-
/>
|
|
431
|
-
</Field>
|
|
432
|
-
<Field label="Password" htmlFor="smtpPass">
|
|
433
|
-
<Input
|
|
434
|
-
id="smtpPass"
|
|
435
|
-
type="password"
|
|
436
|
-
value={smtp.pass}
|
|
437
|
-
onChange={(e) => setSmtp({ ...smtp, pass: e.target.value })}
|
|
438
|
-
/>
|
|
439
|
-
</Field>
|
|
440
|
-
<Field label="From email" htmlFor="smtpFromEmail">
|
|
441
|
-
<Input
|
|
442
|
-
id="smtpFromEmail"
|
|
443
|
-
value={smtp.fromEmail}
|
|
444
|
-
onChange={(e) => setSmtp({ ...smtp, fromEmail: e.target.value })}
|
|
445
|
-
/>
|
|
446
|
-
</Field>
|
|
447
|
-
<Field label="From name" htmlFor="smtpFromName">
|
|
448
|
-
<Input
|
|
449
|
-
id="smtpFromName"
|
|
450
|
-
value={smtp.fromName}
|
|
451
|
-
onChange={(e) => setSmtp({ ...smtp, fromName: e.target.value })}
|
|
452
|
-
/>
|
|
453
|
-
</Field>
|
|
454
|
-
</div>
|
|
455
|
-
{!writable && channel !== 'docker' && (
|
|
456
|
-
<p className="text-xs text-muted-foreground">
|
|
457
|
-
On this platform, set SMTP_* as environment variables in your hosting dashboard.
|
|
458
|
-
</p>
|
|
459
|
-
)}
|
|
460
|
-
</div>
|
|
461
|
-
)}
|
|
462
|
-
|
|
463
|
-
{current === 'bot' && (
|
|
464
|
-
<div className="space-y-4">
|
|
465
|
-
<label className="flex items-start gap-3">
|
|
466
|
-
<Checkbox
|
|
467
|
-
checked={turnstileEnabled}
|
|
468
|
-
onCheckedChange={(c) => setTurnstileEnabled(c === true)}
|
|
469
|
-
className="mt-1"
|
|
470
|
-
/>
|
|
471
|
-
<span className="text-sm">
|
|
472
|
-
<span className="font-medium">Enable Cloudflare Turnstile</span>
|
|
473
|
-
<span className="block text-xs text-muted-foreground">
|
|
474
|
-
Protects sign-up / sign-in forms. Stored securely in the database.
|
|
475
|
-
</span>
|
|
476
|
-
</span>
|
|
477
|
-
</label>
|
|
478
|
-
{turnstileEnabled && (
|
|
479
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
480
|
-
<Field label="Site key" htmlFor="tsSite">
|
|
481
|
-
<Input
|
|
482
|
-
id="tsSite"
|
|
483
|
-
value={turnstile.siteKey}
|
|
484
|
-
onChange={(e) => setTurnstile({ ...turnstile, siteKey: e.target.value })}
|
|
485
|
-
/>
|
|
486
|
-
</Field>
|
|
487
|
-
<Field label="Secret key" htmlFor="tsSecret">
|
|
488
|
-
<Input
|
|
489
|
-
id="tsSecret"
|
|
490
|
-
type="password"
|
|
491
|
-
value={turnstile.secretKey}
|
|
492
|
-
onChange={(e) => setTurnstile({ ...turnstile, secretKey: e.target.value })}
|
|
493
|
-
/>
|
|
494
|
-
</Field>
|
|
495
|
-
</div>
|
|
496
|
-
)}
|
|
497
|
-
</div>
|
|
498
|
-
)}
|
|
499
|
-
|
|
500
|
-
{current === 'signups' && (
|
|
501
|
-
<label className="flex items-start gap-3">
|
|
502
|
-
<Checkbox
|
|
503
|
-
checked={autoAccept}
|
|
504
|
-
onCheckedChange={(c) => setAutoAccept(c === true)}
|
|
505
|
-
className="mt-1"
|
|
506
|
-
/>
|
|
507
|
-
<span className="text-sm">
|
|
508
|
-
<span className="font-medium">
|
|
509
|
-
Auto-approve local registrations (skip outbound email verification)
|
|
510
|
-
</span>
|
|
511
|
-
<span className="block text-xs text-muted-foreground">
|
|
512
|
-
New sign-ups become active immediately, even without SMTP configured. Convenient
|
|
513
|
-
for local / self-hosted use; leave off for public production sites.
|
|
514
|
-
</span>
|
|
515
|
-
</span>
|
|
516
|
-
</label>
|
|
517
|
-
)}
|
|
518
|
-
|
|
519
375
|
{current === 'admin' && (
|
|
520
376
|
<div className="space-y-4">
|
|
521
377
|
<Field label="Full name" htmlFor="adminName">
|
|
@@ -546,6 +402,13 @@ export default function SetupWizard({
|
|
|
546
402
|
verification email needed). Finishing also applies the database schema and saved
|
|
547
403
|
settings, so it can take up to a minute.
|
|
548
404
|
</p>
|
|
405
|
+
<Alert className="py-2 px-4">
|
|
406
|
+
<AlertDescription className="text-xs">
|
|
407
|
+
Next, your dashboard will guide you through finishing setup — branding, copyright,
|
|
408
|
+
email (SMTP), payment providers, and bot protection are all configured there, no
|
|
409
|
+
environment variables required.
|
|
410
|
+
</AlertDescription>
|
|
411
|
+
</Alert>
|
|
549
412
|
|
|
550
413
|
{writable && (
|
|
551
414
|
<label className="flex items-start gap-3 rounded-lg border p-3">
|
|
@@ -606,14 +469,8 @@ function stepDescription(step: StepId, channel: DeployChannel): string {
|
|
|
606
469
|
: channel === 'vercel'
|
|
607
470
|
? 'Using Supabase Storage (S3-compatible) for media.'
|
|
608
471
|
: 'Bring your own Cloudflare R2 bucket for media storage.';
|
|
609
|
-
case 'email':
|
|
610
|
-
return 'Optional. Used for verification emails, password resets, and 2FA codes.';
|
|
611
|
-
case 'bot':
|
|
612
|
-
return 'Optional. Add Cloudflare Turnstile to your auth forms.';
|
|
613
|
-
case 'signups':
|
|
614
|
-
return 'Choose how new public registrations are handled.';
|
|
615
472
|
case 'admin':
|
|
616
|
-
return 'Create the first administrator account.';
|
|
473
|
+
return 'Create the first administrator account — the last step. Email, payments, branding, and the rest are configured from your dashboard.';
|
|
617
474
|
default:
|
|
618
475
|
return '';
|
|
619
476
|
}
|
|
@@ -91,14 +91,6 @@ export default async function SetupPage() {
|
|
|
91
91
|
writable={isLocalWritableEnv()}
|
|
92
92
|
siteUrl={process.env.NEXT_PUBLIC_URL ?? ''}
|
|
93
93
|
storagePrefill={buildStoragePrefill(channel, supabaseUrl)}
|
|
94
|
-
smtpPrefill={{
|
|
95
|
-
host: process.env.SMTP_HOST ?? '',
|
|
96
|
-
port: process.env.SMTP_PORT ?? '465',
|
|
97
|
-
user: process.env.SMTP_USER ?? '',
|
|
98
|
-
fromEmail: process.env.SMTP_FROM_EMAIL ?? '',
|
|
99
|
-
fromName: process.env.SMTP_FROM_NAME ?? '',
|
|
100
|
-
}}
|
|
101
|
-
turnstilePrefill={{ siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ?? '' }}
|
|
102
94
|
supabaseEnvDetected={{
|
|
103
95
|
NEXT_PUBLIC_SUPABASE_URL: Boolean(process.env.NEXT_PUBLIC_SUPABASE_URL),
|
|
104
96
|
SUPABASE_URL: Boolean(process.env.SUPABASE_URL),
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import type { Database } from '@nextblock-cms/db';
|
|
4
4
|
import { cn } from '@nextblock-cms/utils';
|
|
5
5
|
import { usePathname } from 'next/navigation';
|
|
6
|
-
import Link from 'next/link';
|
|
7
6
|
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
8
7
|
import Header from './Header';
|
|
9
8
|
import FooterNavigation from './FooterNavigation';
|
|
@@ -148,12 +147,6 @@ export function AppShell({
|
|
|
148
147
|
)}
|
|
149
148
|
<div className="flex flex-row items-center gap-4">
|
|
150
149
|
<p className="text-muted-foreground">{copyrightText}</p>
|
|
151
|
-
<Link href="/privacy-policy" className="hover:underline">
|
|
152
|
-
Privacy Policy
|
|
153
|
-
</Link>
|
|
154
|
-
<Link href="/terms-of-service" className="hover:underline">
|
|
155
|
-
Terms of Service
|
|
156
|
-
</Link>
|
|
157
150
|
<ThemeSwitcher />
|
|
158
151
|
</div>
|
|
159
152
|
</div>
|
|
@@ -17,6 +17,7 @@ import ClientTextBlockRenderer from "./blocks/renderers/ClientTextBlockRenderer"
|
|
|
17
17
|
import { getCachedCustomBlockDefinitionBySlug } from "../lib/custom-block-definitions";
|
|
18
18
|
import { CachedDynamicLayoutEngine } from "./renderers/CachedDynamicLayoutEngine";
|
|
19
19
|
import { resolveBlockRelations } from "../lib/resolve-block-relations";
|
|
20
|
+
import { substitutePrivacyMergeTags } from "../lib/privacy/contact-emails";
|
|
20
21
|
|
|
21
22
|
const ECOMMERCE_BLOCK_TYPES = new Set([
|
|
22
23
|
"product_grid",
|
|
@@ -143,9 +144,16 @@ async function renderLoadedBlock({
|
|
|
143
144
|
|
|
144
145
|
// Keep common LCP-adjacent text blocks out of the dynamic renderer manifest.
|
|
145
146
|
if (block.block_type === 'text') {
|
|
147
|
+
// Top-level text blocks bypass the server TextBlockRenderer, so resolve any
|
|
148
|
+
// merge tags (e.g. {{privacy_email}} on the Privacy/Terms pages) here.
|
|
149
|
+
const textContent = block.content as { html_content?: string } | null;
|
|
150
|
+
const rawHtml = typeof textContent?.html_content === 'string' ? textContent.html_content : '';
|
|
151
|
+
const html = rawHtml.includes('{{')
|
|
152
|
+
? await substitutePrivacyMergeTags(rawHtml)
|
|
153
|
+
: rawHtml;
|
|
146
154
|
return (
|
|
147
155
|
<ClientTextBlockRenderer
|
|
148
|
-
content={
|
|
156
|
+
content={{ ...(textContent as any), html_content: html }}
|
|
149
157
|
languageId={languageId}
|
|
150
158
|
visualEditAttributes={visualEditAttributes}
|
|
151
159
|
/>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ComponentType } from "react";
|
|
4
|
+
|
|
5
|
+
interface DeferredGoogleAnalyticsProps {
|
|
6
|
+
gaId?: string;
|
|
7
|
+
nonce?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const interactionEvents: Array<keyof WindowEventMap> = [
|
|
11
|
+
"pointerdown",
|
|
12
|
+
"keydown",
|
|
13
|
+
"scroll",
|
|
14
|
+
"touchstart",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
type GoogleAnalyticsComponent = ComponentType<{
|
|
18
|
+
gaId: string;
|
|
19
|
+
nonce?: string;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
export function DeferredGoogleAnalytics({
|
|
23
|
+
gaId,
|
|
24
|
+
nonce,
|
|
25
|
+
}: DeferredGoogleAnalyticsProps) {
|
|
26
|
+
const [GoogleAnalytics, setGoogleAnalytics] =
|
|
27
|
+
useState<GoogleAnalyticsComponent | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!gaId) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let isMounted = true;
|
|
35
|
+
|
|
36
|
+
function removeListeners() {
|
|
37
|
+
interactionEvents.forEach((eventName) => {
|
|
38
|
+
window.removeEventListener(eventName, enableGa);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function enableGa() {
|
|
43
|
+
removeListeners();
|
|
44
|
+
|
|
45
|
+
void import("@next/third-parties/google").then(({ GoogleAnalytics }) => {
|
|
46
|
+
if (isMounted) {
|
|
47
|
+
setGoogleAnalytics(() => GoogleAnalytics as GoogleAnalyticsComponent);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interactionEvents.forEach((eventName) => {
|
|
53
|
+
window.addEventListener(eventName, enableGa, {
|
|
54
|
+
once: true,
|
|
55
|
+
passive: true,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
isMounted = false;
|
|
61
|
+
removeListeners();
|
|
62
|
+
};
|
|
63
|
+
}, [gaId]);
|
|
64
|
+
|
|
65
|
+
if (!gaId || !GoogleAnalytics) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return <GoogleAnalytics gaId={gaId} nonce={nonce} />;
|
|
70
|
+
}
|
|
@@ -2,17 +2,18 @@ import React from "react";
|
|
|
2
2
|
import { headers } from 'next/headers';
|
|
3
3
|
import ClientTextBlockRenderer from "./ClientTextBlockRenderer";
|
|
4
4
|
import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
|
|
5
|
+
import { substitutePrivacyMergeTags } from "../../../lib/privacy/contact-emails";
|
|
5
6
|
|
|
6
7
|
export type TextBlockContent = {
|
|
7
8
|
html_content?: string;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
interface TextBlockRendererProps {
|
|
11
|
-
content: TextBlockContent;
|
|
12
|
-
languageId: number;
|
|
13
|
-
visualEditAttributes?: VisualEditAttributes;
|
|
14
|
-
renderContext?: 'prose' | 'section';
|
|
15
|
-
}
|
|
11
|
+
interface TextBlockRendererProps {
|
|
12
|
+
content: TextBlockContent;
|
|
13
|
+
languageId: number;
|
|
14
|
+
visualEditAttributes?: VisualEditAttributes;
|
|
15
|
+
renderContext?: 'prose' | 'section';
|
|
16
|
+
}
|
|
16
17
|
|
|
17
18
|
function addNonceToInlineScripts(html: string, nonce: string): string {
|
|
18
19
|
if (!html || !nonce) return html || '';
|
|
@@ -24,23 +25,27 @@ function addNonceToInlineScripts(html: string, nonce: string): string {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const TextBlockRenderer: React.FC<TextBlockRendererProps> = async ({
|
|
27
|
-
content,
|
|
28
|
-
languageId,
|
|
29
|
-
visualEditAttributes,
|
|
30
|
-
renderContext = 'prose',
|
|
31
|
-
}) => {
|
|
28
|
+
content,
|
|
29
|
+
languageId,
|
|
30
|
+
visualEditAttributes,
|
|
31
|
+
renderContext = 'prose',
|
|
32
|
+
}) => {
|
|
32
33
|
const hdrs = await headers();
|
|
33
34
|
const nonce = hdrs.get('x-nonce') || '';
|
|
34
|
-
|
|
35
|
+
let html = content.html_content || '';
|
|
36
|
+
if (html.includes('{{')) {
|
|
37
|
+
html = await substitutePrivacyMergeTags(html);
|
|
38
|
+
}
|
|
39
|
+
const htmlWithNonce = html ? addNonceToInlineScripts(html, nonce) : '';
|
|
35
40
|
const patchedContent = { ...content, html_content: htmlWithNonce };
|
|
36
41
|
return (
|
|
37
|
-
<ClientTextBlockRenderer
|
|
38
|
-
content={patchedContent}
|
|
39
|
-
languageId={languageId}
|
|
40
|
-
visualEditAttributes={visualEditAttributes}
|
|
41
|
-
renderContext={renderContext}
|
|
42
|
-
/>
|
|
43
|
-
);
|
|
44
|
-
};
|
|
42
|
+
<ClientTextBlockRenderer
|
|
43
|
+
content={patchedContent}
|
|
44
|
+
languageId={languageId}
|
|
45
|
+
visualEditAttributes={visualEditAttributes}
|
|
46
|
+
renderContext={renderContext}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
45
50
|
|
|
46
51
|
export default TextBlockRenderer;
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// shows zero analytics bytes, preserving the Lighthouse budget.
|
|
6
6
|
import { useEffect, useRef, useState } from 'react';
|
|
7
7
|
import { DeferredGoogleTagManager } from '../DeferredGoogleTagManager';
|
|
8
|
+
import { DeferredGoogleAnalytics } from '../DeferredGoogleAnalytics';
|
|
8
9
|
import { CONSENT_CHANGE_EVENT, readConsent } from '../../lib/privacy/consent-client';
|
|
9
10
|
|
|
10
11
|
interface ConsentGatedAnalyticsProps {
|
|
11
12
|
gtmId?: string;
|
|
13
|
+
gaMeasurementId?: string;
|
|
12
14
|
customScripts?: string;
|
|
13
15
|
nonce?: string;
|
|
14
16
|
}
|
|
@@ -29,6 +31,7 @@ function injectCustomScripts(markup: string, nonce?: string) {
|
|
|
29
31
|
|
|
30
32
|
export function ConsentGatedAnalytics({
|
|
31
33
|
gtmId,
|
|
34
|
+
gaMeasurementId,
|
|
32
35
|
customScripts,
|
|
33
36
|
nonce,
|
|
34
37
|
}: ConsentGatedAnalyticsProps) {
|
|
@@ -54,6 +57,14 @@ export function ConsentGatedAnalytics({
|
|
|
54
57
|
}
|
|
55
58
|
}, [analyticsAllowed, customScripts, nonce]);
|
|
56
59
|
|
|
57
|
-
if (!analyticsAllowed
|
|
58
|
-
|
|
60
|
+
if (!analyticsAllowed) return null;
|
|
61
|
+
if (!gtmId && !gaMeasurementId) return null;
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
{gtmId && <DeferredGoogleTagManager gtmId={gtmId} nonce={nonce} />}
|
|
65
|
+
{gaMeasurementId && (
|
|
66
|
+
<DeferredGoogleAnalytics gaId={gaMeasurementId} nonce={nonce} />
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
59
70
|
}
|
|
@@ -109,6 +109,17 @@ repo expects at least:
|
|
|
109
109
|
- `CRON_SECRET`, `DRAFT_MODE_SECRET`, `REVALIDATE_SECRET_TOKEN` — auto-generated
|
|
110
110
|
by `npm run setup`
|
|
111
111
|
|
|
112
|
+
> **Supabase key aliases.** The names above are the local-dev canon, but the app also
|
|
113
|
+
> accepts the *new-style* names the hosted/Vercel Supabase Marketplace integration
|
|
114
|
+
> injects: `SUPABASE_URL` (non-prefixed), `SUPABASE_PUBLISHABLE_KEY` /
|
|
115
|
+
> `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (anon-equivalent), and `SUPABASE_SECRET_KEY`
|
|
116
|
+
> (service-role-equivalent). Resolution lives in `apps/nextblock/lib/setup/env-status.ts`
|
|
117
|
+
> (`resolveSupabaseUrl` / `resolveSupabaseAnonKey` / `resolveSupabaseServiceKey`); read
|
|
118
|
+
> Supabase env through those (or the same inline alias chain in published libs) rather
|
|
119
|
+
> than a single raw name. `DRAFT_MODE_SECRET` / `REVALIDATE_SECRET_TOKEN` are optional in
|
|
120
|
+
> production — when unset they are derived from the service-role key (see
|
|
121
|
+
> `apps/nextblock/lib/app-secrets.ts`). See [12-VERCEL-DEPLOYMENT.md](./12-VERCEL-DEPLOYMENT.md).
|
|
122
|
+
|
|
112
123
|
Captured by `npm run setup` and needed for a complete CMS:
|
|
113
124
|
|
|
114
125
|
- R2 credentials for media storage. The app builds and serves without them, but
|
|
@@ -115,11 +115,62 @@ package activation state.
|
|
|
115
115
|
|
|
116
116
|
## Publishing and Release Notes
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
A generated project installs the libraries from **npm**, so a feature only reaches
|
|
119
|
+
scaffolds after the libs are republished. (The monorepo's own Vercel deploy builds the
|
|
120
|
+
libs from source, so it sees changes immediately — only `npm create` scaffolds need a
|
|
121
|
+
republish.)
|
|
122
|
+
|
|
123
|
+
### Release commands
|
|
124
|
+
|
|
125
|
+
- `npm run release:all -- <version>` — build **and publish every package** at one
|
|
126
|
+
synchronized version, in dependency order: `utils → ui → sdk → db → editor → ecom`,
|
|
127
|
+
then `release-cli.js` (which stamps the root + template + `create-nextblock`, re-syncs
|
|
128
|
+
the template, and publishes the CLI). Pass an explicit semver (e.g. `0.10.2`);
|
|
129
|
+
`--dry-run` prints the plan only.
|
|
130
|
+
- `npm run build:<lib>` (`build:utils|ui|db|editor|sdk|ecom`) and
|
|
131
|
+
`node tools/scripts/release-lib.js <lib> <version>` — build + publish a **single** lib
|
|
132
|
+
(`<lib>` is the nx project name, so use `ecommerce`, which maps to the published
|
|
133
|
+
`@nextblock-cms/ecom`). `npx nx build <lib>` only *compiles*, it does not publish.
|
|
134
|
+
|
|
135
|
+
### npm 2FA / OTP (and capturing a log)
|
|
136
|
+
|
|
137
|
+
Publishing requires a one-time password if the npm account has 2FA. **Piping the command
|
|
138
|
+
output breaks the interactive OTP prompt** — `npm run release:all -- … 2>&1 | Tee-Object …`
|
|
139
|
+
(or `| tee`) fails with `npm error code EOTP` because npm no longer has a TTY. Either:
|
|
140
|
+
|
|
141
|
+
- set an npm **Automation** token (`npm config set //registry.npmjs.org/:_authToken …`),
|
|
142
|
+
which bypasses 2FA — then piping to a log file is fine; or
|
|
143
|
+
- capture with PowerShell `Start-Transcript -Path release.log; npm run release:all -- … ;
|
|
144
|
+
Stop-Transcript`, which records the session while npm keeps its terminal.
|
|
145
|
+
|
|
146
|
+
`release:all` has no "already published" guard: if it dies partway, re-running the same
|
|
147
|
+
version re-publishes from the top and 403s on the first already-published lib. Finish a
|
|
148
|
+
partial release by running the **remaining** libs individually
|
|
149
|
+
(`node tools/scripts/release-lib.js <lib> <version>` … then `release-cli.js <version>`),
|
|
150
|
+
or bump to a fresh version and re-run the whole thing.
|
|
151
|
+
|
|
152
|
+
### Library build gotchas (dts / tsconfig)
|
|
153
|
+
|
|
154
|
+
Each lib emits its `.d.ts` via `vite-plugin-dts` running tsc on `tsconfig.lib.json`. When a
|
|
155
|
+
lib imports a sibling (`ui`/`db` import `utils`; `ecom` imports all), how you wire the
|
|
156
|
+
tsconfig decides whether the build log is clean:
|
|
157
|
+
|
|
158
|
+
- A **composite** lib (`ui`, `db` — `db` inherits it) must **list the imported sibling's
|
|
159
|
+
sources in `include`** (e.g. `"../utils/src/**/*.ts"`) and keep `"references": []`.
|
|
160
|
+
Mirror `libs/editor`, which always built clean this way. A composite project
|
|
161
|
+
`reference` to the sibling triggers `TS6305` ("output not built" — vite never produces
|
|
162
|
+
the `tsc -b` out-tsc output); empty `references` *without* the `include` triggers
|
|
163
|
+
`TS6307` ("file not listed"). Both are non-fatal log noise but should stay at zero.
|
|
164
|
+
- A **non-composite** lib (`ecom`, which extends `tsconfig.base.json`) just needs
|
|
165
|
+
`"references": []` — no `include` of siblings.
|
|
166
|
+
- A **strict** lib (`db`/`sdk` set `noPropertyAccessFromIndexSignature`) compiles the
|
|
167
|
+
sibling's *source* under its strict rules, so `libs/utils` must stay strict-clean
|
|
168
|
+
(bracket-access undeclared keys, e.g. `process.env['R2_BUCKET_NAME']`).
|
|
169
|
+
|
|
170
|
+
`vite-plugin-dts` `entryRoot: 'src'` keeps emission to the lib's own `src`, so listing
|
|
171
|
+
sibling sources does **not** leak their `.d.ts` into the tarball. The published `bin` path
|
|
172
|
+
in `apps/create-nextblock/package.json` should have **no leading `./`** (`bin/…`, not
|
|
173
|
+
`./bin/…`) or npm "auto-corrects" it with a publish warning.
|
|
174
|
+
|
|
175
|
+
If a generated project looks stale, check the sync script and template output (and whether
|
|
176
|
+
the libs were actually republished) before assuming the source app is missing the feature.
|
|
@@ -17,6 +17,7 @@ library surfaces rather than historical planning notes.
|
|
|
17
17
|
- Live draft (visual editing) mode: [09-LIVE-DRAFT-MODE.md](./09-LIVE-DRAFT-MODE.md)
|
|
18
18
|
- Custom blocks (data-driven CRUD): [10-CUSTOM-BLOCKS.md](./10-CUSTOM-BLOCKS.md)
|
|
19
19
|
- Self-hosted local Docker stack: [11-SELF-HOSTED-DOCKER.md](./11-SELF-HOSTED-DOCKER.md)
|
|
20
|
+
- One-click cloud deploy (Deploy to Vercel): [12-VERCEL-DEPLOYMENT.md](./12-VERCEL-DEPLOYMENT.md)
|
|
20
21
|
|
|
21
22
|
## Audience Guide
|
|
22
23
|
|
|
@@ -26,7 +27,9 @@ library surfaces rather than historical planning notes.
|
|
|
26
27
|
- Custom block work: read `03`, then `10`.
|
|
27
28
|
- AI / Cortex work: read `08`.
|
|
28
29
|
- CLI or template work: read `06`.
|
|
30
|
+
- Publishing the libraries / scaffold CLI: read `06`.
|
|
29
31
|
- Running everything locally without cloud accounts: read `11`.
|
|
32
|
+
- One-click cloud deploy and the browser setup wizard: read `12`.
|
|
30
33
|
- AI agents: start with this index, then move directly to the subsystem file that
|
|
31
34
|
matches the task. Treat `apps/nextblock`, `libs/*`, and
|
|
32
35
|
`libs/db/src/supabase/migrations` as the final authority if a doc and code ever
|