create-nextblock 0.9.95 → 0.9.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/nextblock-template/app/[slug]/page.tsx +9 -2
- package/templates/nextblock-template/app/actions/package-actions.ts +9 -4
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3 -2
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +5 -1
- package/templates/nextblock-template/app/api/draft/route.ts +5 -1
- package/templates/nextblock-template/app/api/media/library/route.ts +6 -2
- package/templates/nextblock-template/app/api/process-image/route.ts +58 -43
- package/templates/nextblock-template/app/api/revalidate/route.ts +21 -18
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +20 -8
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +34 -28
- package/templates/nextblock-template/app/article/[slug]/page.tsx +4 -13
- package/templates/nextblock-template/app/checkout/success/actions.ts +3 -2
- package/templates/nextblock-template/app/cms/media/actions.ts +47 -31
- package/templates/nextblock-template/app/cms/orders/actions.ts +29 -29
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/users/actions.ts +119 -118
- package/templates/nextblock-template/app/cms/users/page.tsx +3 -3
- package/templates/nextblock-template/app/layout.tsx +10 -7
- package/templates/nextblock-template/app/lib/site-settings.ts +7 -4
- package/templates/nextblock-template/app/page.tsx +2 -1
- package/templates/nextblock-template/app/product/[slug]/page.tsx +9 -2
- package/templates/nextblock-template/app/robots.txt/route.ts +6 -3
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +55 -8
- package/templates/nextblock-template/app/setup/page.tsx +17 -1
- package/templates/nextblock-template/app/sitemap.ts +5 -3
- package/templates/nextblock-template/context/language-rest-client.ts +3 -2
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +125 -32
- package/templates/nextblock-template/lib/app-secrets.ts +39 -0
- package/templates/nextblock-template/lib/auth/crypto.ts +4 -1
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +23 -3
- package/templates/nextblock-template/lib/setup/actions.ts +16 -0
- package/templates/nextblock-template/lib/setup/env-status.ts +44 -5
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +172 -0
- package/templates/nextblock-template/lib/setup/schema-apply.ts +23 -7
- package/templates/nextblock-template/lib/site-url.ts +48 -0
- package/templates/nextblock-template/lib/storage/provider.ts +66 -0
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.ts +1 -1
- package/templates/nextblock-template/next.config.js +37 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +39 -4
package/package.json
CHANGED
|
@@ -38,8 +38,15 @@ interface PageTranslation {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function generateStaticParams(): Promise<ResolvedPageParams[]> {
|
|
41
|
-
// Unconfigured instance (pre-/setup): no DB to read slugs from.
|
|
42
|
-
|
|
41
|
+
// Unconfigured instance (pre-/setup): no DB to read slugs from. Accept every Supabase
|
|
42
|
+
// key alias the Vercel integration may inject (incl. the new publishable key).
|
|
43
|
+
const hasSupabaseEnv =
|
|
44
|
+
(process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL) &&
|
|
45
|
+
(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
|
|
46
|
+
process.env.SUPABASE_ANON_KEY ||
|
|
47
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ||
|
|
48
|
+
process.env.SUPABASE_PUBLISHABLE_KEY);
|
|
49
|
+
if (!hasSupabaseEnv) {
|
|
43
50
|
return [];
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -4,6 +4,11 @@ import { createClient } from '@supabase/supabase-js';
|
|
|
4
4
|
import { NEXTBLOCK_PACKAGES } from '@nextblock-cms/utils';
|
|
5
5
|
import { headers } from 'next/headers';
|
|
6
6
|
import { revalidatePath } from 'next/cache';
|
|
7
|
+
import {
|
|
8
|
+
resolveSupabaseAnonKey,
|
|
9
|
+
resolveSupabaseServiceKey,
|
|
10
|
+
resolveSupabaseUrl,
|
|
11
|
+
} from '../../lib/setup/env-status';
|
|
7
12
|
|
|
8
13
|
// Freemius handles both Sandbox and Production keys on the same API domain.
|
|
9
14
|
// The key itself determines the environment.
|
|
@@ -11,15 +16,15 @@ const FM_API_URL = 'https://api.freemius.com/v1';
|
|
|
11
16
|
|
|
12
17
|
// Helper to get service role client
|
|
13
18
|
const getServiceRoleClient = () => {
|
|
14
|
-
const supabaseUrl =
|
|
15
|
-
const supabaseServiceKey =
|
|
19
|
+
const supabaseUrl = resolveSupabaseUrl();
|
|
20
|
+
const supabaseServiceKey = resolveSupabaseServiceKey();
|
|
16
21
|
|
|
17
22
|
if (!supabaseUrl || !supabaseServiceKey) {
|
|
18
23
|
console.error('Missing Supabase credentials');
|
|
19
24
|
throw new Error('Missing Supabase credentials (Service Key required for activation).');
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
if (supabaseServiceKey ===
|
|
27
|
+
if (supabaseServiceKey === resolveSupabaseAnonKey()) {
|
|
23
28
|
console.warn('CRITICAL WARNING: SUPABASE_SERVICE_ROLE_KEY matches NEXT_PUBLIC_SUPABASE_ANON_KEY. This will likely cause Permission Denied errors as RLS cannot be bypassed.');
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -73,7 +78,7 @@ export async function activatePackage(key: string) {
|
|
|
73
78
|
}
|
|
74
79
|
});
|
|
75
80
|
|
|
76
|
-
const responseData = await response.json();
|
|
81
|
+
const responseData = await response.json();
|
|
77
82
|
|
|
78
83
|
// Freemius returns the license object directly if successful, or an error/api_response
|
|
79
84
|
if (response.ok && responseData.install_id) {
|
|
@@ -2728,8 +2728,9 @@ export async function GET(request: NextRequest) {
|
|
|
2728
2728
|
});
|
|
2729
2729
|
}
|
|
2730
2730
|
|
|
2731
|
-
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
2732
|
-
const supabaseServiceKey =
|
|
2731
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
2732
|
+
const supabaseServiceKey =
|
|
2733
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
|
|
2733
2734
|
|
|
2734
2735
|
const r2AccountId = process.env.R2_ACCOUNT_ID;
|
|
2735
2736
|
const r2AccessKeyId = process.env.R2_ACCESS_KEY_ID;
|
|
@@ -6,9 +6,13 @@ export const dynamic = 'force-dynamic';
|
|
|
6
6
|
export const maxDuration = 30;
|
|
7
7
|
|
|
8
8
|
export async function GET(request: NextRequest) {
|
|
9
|
+
// CRON_SECRET is optional: when set we enforce Vercel's `Authorization: Bearer`
|
|
10
|
+
// header, but a zero-config deploy can leave it unset (the job only does low-harm
|
|
11
|
+
// FX-rate sync). Set CRON_SECRET in the project env to lock the endpoint down.
|
|
12
|
+
const cronSecret = process.env.CRON_SECRET?.trim();
|
|
9
13
|
const authHeader = request.headers.get('authorization');
|
|
10
14
|
|
|
11
|
-
if (authHeader !== `Bearer ${
|
|
15
|
+
if (cronSecret && authHeader !== `Bearer ${cronSecret}`) {
|
|
12
16
|
return new NextResponse('Unauthorized', {
|
|
13
17
|
status: 401,
|
|
14
18
|
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resolveRequestOrigin,
|
|
8
8
|
type DraftPathTarget,
|
|
9
9
|
} from "../../../lib/visual-editing/draft-route";
|
|
10
|
+
import { resolveDraftModeSecret } from "../../../lib/app-secrets";
|
|
10
11
|
|
|
11
12
|
export const runtime = "nodejs";
|
|
12
13
|
export const dynamic = "force-dynamic";
|
|
@@ -54,7 +55,10 @@ async function targetExists(target: DraftPathTarget) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export async function GET(request: NextRequest) {
|
|
57
|
-
|
|
58
|
+
// Explicit DRAFT_MODE_SECRET wins; otherwise it is derived from the service-role
|
|
59
|
+
// key so this preview entry works on a zero-config deploy. Empty only when Supabase
|
|
60
|
+
// itself is unconfigured.
|
|
61
|
+
const configuredSecret = resolveDraftModeSecret();
|
|
58
62
|
|
|
59
63
|
if (!configuredSecret) {
|
|
60
64
|
return NextResponse.json(
|
|
@@ -7,8 +7,12 @@ const DEFAULT_MEDIA_LIBRARY_ITEMS = 50;
|
|
|
7
7
|
export const dynamic = 'force-dynamic';
|
|
8
8
|
|
|
9
9
|
function getMediaLibrarySupabaseClient() {
|
|
10
|
-
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
11
|
-
const supabaseAnonKey =
|
|
10
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
11
|
+
const supabaseAnonKey =
|
|
12
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
|
|
13
|
+
process.env.SUPABASE_ANON_KEY ||
|
|
14
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ||
|
|
15
|
+
process.env.SUPABASE_PUBLISHABLE_KEY;
|
|
12
16
|
|
|
13
17
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
14
18
|
throw new Error('Missing Supabase environment variables for media library API');
|
|
@@ -5,6 +5,15 @@ import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
|
5
5
|
import sharp from 'sharp';
|
|
6
6
|
import { Readable } from 'stream';
|
|
7
7
|
import { getPlaiceholder } from 'plaiceholder';
|
|
8
|
+
import {
|
|
9
|
+
getStorageBackend,
|
|
10
|
+
getStorageBucket,
|
|
11
|
+
resolveMediaBaseUrl,
|
|
12
|
+
} from '../../../lib/storage/provider';
|
|
13
|
+
import {
|
|
14
|
+
supabaseDownloadObject,
|
|
15
|
+
supabaseUploadObject,
|
|
16
|
+
} from '../../../lib/storage/supabase-storage';
|
|
8
17
|
|
|
9
18
|
// Helper to convert stream to buffer
|
|
10
19
|
async function streamToBuffer(stream: Readable): Promise<Buffer> {
|
|
@@ -16,12 +25,6 @@ async function streamToBuffer(stream: Readable): Promise<Buffer> {
|
|
|
16
25
|
});
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
20
|
-
// Construct the public URL base. Assumes your R2 bucket is set up for public access.
|
|
21
|
-
// Example: https://<your-bucket>.<account-id>.r2.cloudflarestorage.com
|
|
22
|
-
// Or if you use a custom domain: https://your.custom.domain
|
|
23
|
-
const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL;
|
|
24
|
-
|
|
25
28
|
interface ProcessedImageVariant {
|
|
26
29
|
objectKey: string;
|
|
27
30
|
url: string;
|
|
@@ -44,22 +47,45 @@ const TARGET_FORMAT = 'avif';
|
|
|
44
47
|
const TARGET_MIME_TYPE = 'image/avif';
|
|
45
48
|
|
|
46
49
|
export async function POST(request: NextRequest) {
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
const backend = getStorageBackend();
|
|
51
|
+
const bucket = getStorageBucket();
|
|
52
|
+
const publicUrlBase = resolveMediaBaseUrl();
|
|
53
|
+
|
|
54
|
+
if (!bucket) {
|
|
55
|
+
return NextResponse.json({ error: 'Storage bucket is not configured.' }, { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
// The native Supabase path derives the public base from the project URL — if that's
|
|
58
|
+
// somehow empty (e.g. STORAGE_PROVIDER=supabase forced without a URL), fail loudly
|
|
59
|
+
// rather than minting malformed `/key` URLs that won't resolve to the bucket.
|
|
60
|
+
if (backend === 'supabase' && !publicUrlBase) {
|
|
61
|
+
return NextResponse.json({ error: 'Supabase URL is required for image processing on the Supabase backend.' }, { status: 500 });
|
|
49
62
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
// The S3 path needs an endpoint to construct the public URL base; the native Supabase
|
|
64
|
+
// path derives it from the project URL, so this check only applies to S3/R2.
|
|
65
|
+
if (backend === 's3' && !process.env.R2_S3_ENDPOINT && !process.env.R2_ACCOUNT_ID) {
|
|
66
|
+
console.error('R2_S3_ENDPOINT or R2_ACCOUNT_ID must be set to construct the public URL base');
|
|
67
|
+
return NextResponse.json({ error: 'Server configuration error for storage public URL.' }, { status: 500 });
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
|
|
56
71
|
try {
|
|
57
|
-
const s3Client = await getS3Client();
|
|
58
|
-
if (!s3Client) {
|
|
72
|
+
const s3Client = backend === 's3' ? await getS3Client() : null;
|
|
73
|
+
if (backend === 's3' && !s3Client) {
|
|
59
74
|
console.error('R2 client is not configured. Check your R2 environment variables.');
|
|
60
75
|
return NextResponse.json({ error: 'Image processing is not configured on this server.' }, { status: 500 });
|
|
61
76
|
}
|
|
62
77
|
|
|
78
|
+
// Backend-agnostic object writer used for every processed variant below.
|
|
79
|
+
const putObject = async (key: string, body: Buffer, contentType: string) => {
|
|
80
|
+
if (backend === 'supabase') {
|
|
81
|
+
await supabaseUploadObject(key, body, contentType);
|
|
82
|
+
} else {
|
|
83
|
+
await s3Client!.send(
|
|
84
|
+
new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType }),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
63
89
|
const { objectKey: originalObjectKey, contentType: originalContentType } = await request.json();
|
|
64
90
|
|
|
65
91
|
if (!originalObjectKey || !originalContentType) {
|
|
@@ -70,25 +96,25 @@ export async function POST(request: NextRequest) {
|
|
|
70
96
|
// For now, we only process images. Could be extended for other file types if needed.
|
|
71
97
|
return NextResponse.json({
|
|
72
98
|
message: 'File is not an image. Skipping processing.',
|
|
73
|
-
originalImage: { objectKey: originalObjectKey, fileType: originalContentType, url: `${
|
|
99
|
+
originalImage: { objectKey: originalObjectKey, fileType: originalContentType, url: `${publicUrlBase}/${originalObjectKey}` },
|
|
74
100
|
processedVariants: [],
|
|
75
101
|
blurDataURL: null // Or an empty string, depending on how you want to handle non-images
|
|
76
102
|
}, { status: 200 });
|
|
77
103
|
}
|
|
78
104
|
|
|
79
|
-
// 1. Fetch the original image from
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
// 1. Fetch the original image from storage
|
|
106
|
+
let imageBuffer: Buffer;
|
|
107
|
+
if (backend === 'supabase') {
|
|
108
|
+
imageBuffer = await supabaseDownloadObject(originalObjectKey);
|
|
109
|
+
} else {
|
|
110
|
+
const getObjectResponse = await s3Client!.send(
|
|
111
|
+
new GetObjectCommand({ Bucket: bucket, Key: originalObjectKey }),
|
|
112
|
+
);
|
|
113
|
+
if (!getObjectResponse.Body) {
|
|
114
|
+
throw new Error('Failed to retrieve image from storage: Empty body.');
|
|
115
|
+
}
|
|
116
|
+
imageBuffer = await streamToBuffer(getObjectResponse.Body as Readable);
|
|
89
117
|
}
|
|
90
|
-
|
|
91
|
-
let imageBuffer = await streamToBuffer(getObjectResponse.Body as Readable);
|
|
92
118
|
let sharpInstance = sharp(imageBuffer);
|
|
93
119
|
let originalMetadata = await sharpInstance.metadata();
|
|
94
120
|
|
|
@@ -132,15 +158,9 @@ export async function POST(request: NextRequest) {
|
|
|
132
158
|
// already .avif — strip it so keys read `..._large.avif`, not `..._large_avif.avif`.
|
|
133
159
|
const fileSuffix = size.label.replace(/_avif$/, '');
|
|
134
160
|
const newObjectKey = `${baseName}_${fileSuffix}.${TARGET_FORMAT}`;
|
|
135
|
-
const newPublicUrl = `${
|
|
161
|
+
const newPublicUrl = `${publicUrlBase}/${newObjectKey}`;
|
|
136
162
|
|
|
137
|
-
|
|
138
|
-
Bucket: R2_BUCKET_NAME,
|
|
139
|
-
Key: newObjectKey,
|
|
140
|
-
Body: processedImageBuffer,
|
|
141
|
-
ContentType: TARGET_MIME_TYPE,
|
|
142
|
-
};
|
|
143
|
-
await s3Client.send(new PutObjectCommand(putObjectParams));
|
|
163
|
+
await putObject(newObjectKey, processedImageBuffer, TARGET_MIME_TYPE);
|
|
144
164
|
|
|
145
165
|
const newMetadata = await sharp(processedImageBuffer).metadata();
|
|
146
166
|
processedVariants.push({
|
|
@@ -163,14 +183,9 @@ export async function POST(request: NextRequest) {
|
|
|
163
183
|
.toBuffer();
|
|
164
184
|
|
|
165
185
|
const originalAvifObjectKey = `${baseName}_original.${TARGET_FORMAT}`;
|
|
166
|
-
const originalAvifPublicUrl = `${
|
|
167
|
-
|
|
168
|
-
await
|
|
169
|
-
Bucket: R2_BUCKET_NAME,
|
|
170
|
-
Key: originalAvifObjectKey,
|
|
171
|
-
Body: originalAvifBuffer,
|
|
172
|
-
ContentType: TARGET_MIME_TYPE,
|
|
173
|
-
}));
|
|
186
|
+
const originalAvifPublicUrl = `${publicUrlBase}/${originalAvifObjectKey}`;
|
|
187
|
+
|
|
188
|
+
await putObject(originalAvifObjectKey, originalAvifBuffer, TARGET_MIME_TYPE);
|
|
174
189
|
const originalAvifMetadata = await sharp(originalAvifBuffer).metadata();
|
|
175
190
|
processedVariants.push({
|
|
176
191
|
objectKey: originalAvifObjectKey,
|
|
@@ -188,7 +203,7 @@ export async function POST(request: NextRequest) {
|
|
|
188
203
|
// The client already has some of this, but good to have a consistent structure.
|
|
189
204
|
const originalImageDetails: ProcessedImageVariant = {
|
|
190
205
|
objectKey: originalObjectKey,
|
|
191
|
-
url: `${
|
|
206
|
+
url: `${publicUrlBase}/${originalObjectKey}`,
|
|
192
207
|
width: originalMetadata.width || 0,
|
|
193
208
|
height: originalMetadata.height || 0,
|
|
194
209
|
fileType: originalContentType,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// app/api/revalidate/route.ts
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { revalidatePath } from 'next/cache';
|
|
4
|
-
|
|
5
|
-
const REVALIDATE_SECRET = process.env.REVALIDATE_SECRET_TOKEN;
|
|
4
|
+
import { resolveRevalidateSecret } from '../../../lib/app-secrets';
|
|
6
5
|
|
|
7
6
|
// Define the expected structure of the Supabase webhook payload
|
|
8
7
|
interface SupabaseWebhookPayload {
|
|
@@ -15,8 +14,12 @@ interface SupabaseWebhookPayload {
|
|
|
15
14
|
|
|
16
15
|
export async function POST(request: NextRequest) {
|
|
17
16
|
const secret = request.headers.get('x-revalidate-secret');
|
|
17
|
+
// Explicit REVALIDATE_SECRET_TOKEN wins; otherwise derived from the service-role
|
|
18
|
+
// key. Empty only when Supabase is unconfigured — reject in that case rather than
|
|
19
|
+
// allowing an empty-header match.
|
|
20
|
+
const expectedSecret = resolveRevalidateSecret();
|
|
18
21
|
|
|
19
|
-
if (secret !==
|
|
22
|
+
if (!expectedSecret || secret !== expectedSecret) {
|
|
20
23
|
console.warn("Revalidation attempt with invalid secret token.");
|
|
21
24
|
return NextResponse.json({ message: 'Invalid secret token' }, { status: 401 });
|
|
22
25
|
}
|
|
@@ -42,8 +45,8 @@ export async function POST(request: NextRequest) {
|
|
|
42
45
|
|
|
43
46
|
if (table === 'pages') {
|
|
44
47
|
pathToRevalidate = `/${relevantRecord.slug}`;
|
|
45
|
-
} else if (table === 'posts') {
|
|
46
|
-
pathToRevalidate = `/article/${relevantRecord.slug}`;
|
|
48
|
+
} else if (table === 'posts') {
|
|
49
|
+
pathToRevalidate = `/article/${relevantRecord.slug}`;
|
|
47
50
|
} else {
|
|
48
51
|
console.log(`Revalidation not configured for table: ${table}`);
|
|
49
52
|
return NextResponse.json({ message: `Revalidation not configured for table: ${table}` }, { status: 200 }); // Acknowledge but don't process
|
|
@@ -59,19 +62,19 @@ export async function POST(request: NextRequest) {
|
|
|
59
62
|
await revalidatePath(normalizedPath, 'page');
|
|
60
63
|
console.log(`Successfully revalidated path: ${normalizedPath}`);
|
|
61
64
|
|
|
62
|
-
// Additionally, if it's an article, you might want to revalidate the main listing page.
|
|
63
|
-
if (table === 'posts') {
|
|
64
|
-
// Assuming your main articles listing page is at '/articles' or similar.
|
|
65
|
-
// This path needs to be known and consistent.
|
|
66
|
-
// If your listing is at the root of the language segment (e.g. /en/articles),
|
|
67
|
-
// and you are NOT using [lang] in URL, then the path is just '/articles'.
|
|
68
|
-
// However, if your LanguageContext means /articles shows different content per lang,
|
|
69
|
-
// revalidating just '/articles' will rebuild its default language version.
|
|
70
|
-
// Client-side fetches would still get latest for other languages.
|
|
71
|
-
// For now, let's revalidate a generic /articles path if it exists.
|
|
72
|
-
// await revalidatePath('/articles', 'page'); // Example: revalidate main listing
|
|
73
|
-
// console.log("Also attempted to revalidate /articles listing page.");
|
|
74
|
-
}
|
|
65
|
+
// Additionally, if it's an article, you might want to revalidate the main listing page.
|
|
66
|
+
if (table === 'posts') {
|
|
67
|
+
// Assuming your main articles listing page is at '/articles' or similar.
|
|
68
|
+
// This path needs to be known and consistent.
|
|
69
|
+
// If your listing is at the root of the language segment (e.g. /en/articles),
|
|
70
|
+
// and you are NOT using [lang] in URL, then the path is just '/articles'.
|
|
71
|
+
// However, if your LanguageContext means /articles shows different content per lang,
|
|
72
|
+
// revalidating just '/articles' will rebuild its default language version.
|
|
73
|
+
// Client-side fetches would still get latest for other languages.
|
|
74
|
+
// For now, let's revalidate a generic /articles path if it exists.
|
|
75
|
+
// await revalidatePath('/articles', 'page'); // Example: revalidate main listing
|
|
76
|
+
// console.log("Also attempted to revalidate /articles listing page.");
|
|
77
|
+
}
|
|
75
78
|
|
|
76
79
|
return NextResponse.json({ revalidated: true, revalidatedPath: normalizedPath, now: Date.now() });
|
|
77
80
|
} catch (err: unknown) {
|
|
@@ -4,8 +4,8 @@ import { createClient } from "@nextblock-cms/db/server"; // Server client for au
|
|
|
4
4
|
import { getS3PresignClient } from "@nextblock-cms/utils/server";
|
|
5
5
|
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
|
6
6
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { getStorageBackend, getStorageBucket } from "../../../../lib/storage/provider";
|
|
8
|
+
import { supabaseCreateSignedUpload } from "../../../../lib/storage/supabase-storage";
|
|
9
9
|
|
|
10
10
|
export async function POST(request: NextRequest) {
|
|
11
11
|
const supabase = createClient();
|
|
@@ -26,14 +26,16 @@ export async function POST(request: NextRequest) {
|
|
|
26
26
|
return NextResponse.json({ error: "Forbidden: Insufficient permissions" }, { status: 403 });
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const backend = getStorageBackend();
|
|
30
|
+
const bucket = getStorageBucket();
|
|
31
|
+
if (!bucket) {
|
|
32
|
+
console.error("No storage bucket is configured.");
|
|
31
33
|
return NextResponse.json({ error: "Server configuration error for file uploads." }, { status: 500 });
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
try {
|
|
35
|
-
const s3Client = await getS3PresignClient();
|
|
36
|
-
if (!s3Client) {
|
|
37
|
+
const s3Client = backend === 's3' ? await getS3PresignClient() : null;
|
|
38
|
+
if (backend === 's3' && !s3Client) {
|
|
37
39
|
console.error('R2 client is not configured. Check your R2 environment variables.');
|
|
38
40
|
return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
|
|
39
41
|
}
|
|
@@ -79,8 +81,18 @@ export async function POST(request: NextRequest) {
|
|
|
79
81
|
|
|
80
82
|
const uniqueKey = `${folder}${sanitizedBaseFilename}_${timestamp}${fileExtension ? '.' + fileExtension : ''}`;
|
|
81
83
|
|
|
84
|
+
if (backend === 'supabase') {
|
|
85
|
+
// Native Supabase Storage: hand the browser a signed upload URL to PUT to directly.
|
|
86
|
+
const { signedUrl } = await supabaseCreateSignedUpload(uniqueKey);
|
|
87
|
+
return NextResponse.json({
|
|
88
|
+
presignedUrl: signedUrl,
|
|
89
|
+
objectKey: uniqueKey,
|
|
90
|
+
method: "PUT",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
const command = new PutObjectCommand({
|
|
83
|
-
Bucket:
|
|
95
|
+
Bucket: bucket,
|
|
84
96
|
Key: uniqueKey,
|
|
85
97
|
ContentType: contentType,
|
|
86
98
|
// ACL: 'public-read', // R2 objects are private by default unless bucket is public or presigned URL for GET is used
|
|
@@ -91,7 +103,7 @@ export async function POST(request: NextRequest) {
|
|
|
91
103
|
});
|
|
92
104
|
|
|
93
105
|
const expiresIn = 300; // 5 minutes
|
|
94
|
-
const presignedUrl = await getSignedUrl(s3Client
|
|
106
|
+
const presignedUrl = await getSignedUrl(s3Client!, command, { expiresIn });
|
|
95
107
|
|
|
96
108
|
return NextResponse.json({
|
|
97
109
|
presignedUrl,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// apps/nextblock/app/api/upload/proxy/route.ts
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
-
import { getS3Client } from '@nextblock-cms/utils/server';
|
|
4
|
+
import { getS3Client } from '@nextblock-cms/utils/server';
|
|
5
5
|
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { getStorageBackend, getStorageBucket } from '../../../../lib/storage/provider';
|
|
7
|
+
import { supabaseUploadObject } from '../../../../lib/storage/supabase-storage';
|
|
8
8
|
|
|
9
9
|
export async function POST(request: NextRequest) {
|
|
10
10
|
const supabase = createClient();
|
|
@@ -14,19 +14,22 @@ export async function POST(request: NextRequest) {
|
|
|
14
14
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
const backend = getStorageBackend();
|
|
18
|
+
const bucket = getStorageBucket();
|
|
19
|
+
if (!bucket) {
|
|
20
|
+
console.error('No storage bucket is configured.');
|
|
21
|
+
return NextResponse.json({ error: 'Server configuration error for file uploads.' }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// On the S3 path we need a configured client; the native Supabase path needs none.
|
|
26
|
+
const s3Client = backend === 's3' ? await getS3Client() : null;
|
|
27
|
+
if (backend === 's3' && !s3Client) {
|
|
28
|
+
console.error('R2 client is not configured. Check your R2 environment variables.');
|
|
29
|
+
return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const formData = await request.formData();
|
|
30
33
|
const file = formData.get('file') as File | null;
|
|
31
34
|
const rawFolder = (formData.get('folder') as string | null) ?? undefined;
|
|
32
35
|
|
|
@@ -58,18 +61,21 @@ export async function POST(request: NextRequest) {
|
|
|
58
61
|
const bytes = await file.arrayBuffer();
|
|
59
62
|
const buffer = Buffer.from(bytes);
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
if (backend === 'supabase') {
|
|
65
|
+
await supabaseUploadObject(uniqueKey, buffer, file.type);
|
|
66
|
+
} else {
|
|
67
|
+
const command = new PutObjectCommand({
|
|
68
|
+
Bucket: bucket,
|
|
69
|
+
Key: uniqueKey,
|
|
70
|
+
Body: buffer,
|
|
71
|
+
ContentType: file.type,
|
|
72
|
+
ContentLength: file.size,
|
|
73
|
+
Metadata: {
|
|
74
|
+
'uploader-user-id': user.id,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
await s3Client!.send(command);
|
|
78
|
+
}
|
|
73
79
|
|
|
74
80
|
return NextResponse.json({
|
|
75
81
|
objectKey: uniqueKey,
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// app/article/[slug]/page.tsx
|
|
2
2
|
import React from 'react';
|
|
3
|
-
// Remove or alias the problematic import if only used by other functions:
|
|
4
|
-
// import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
-
import { createClient as createSupabaseJsClient } from '@supabase/supabase-js'; // Import base client
|
|
6
3
|
import { notFound } from "next/navigation";
|
|
7
4
|
import type { Metadata } from 'next';
|
|
8
5
|
import PostClientContent from "./PostClientContent";
|
|
@@ -58,16 +55,10 @@ const resolveLanguageCode = (languagesField: PostTranslation["languages"]): stri
|
|
|
58
55
|
};
|
|
59
56
|
|
|
60
57
|
export async function generateStaticParams(): Promise<ResolvedPostParams[]> {
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!supabaseUrl || !supabaseAnonKey) {
|
|
66
|
-
console.warn('Missing Supabase environment variables in generateStaticParams (Posts)');
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const supabase = createSupabaseJsClient(supabaseUrl, supabaseAnonKey);
|
|
58
|
+
// Cookie-free SSG client. getSsgSupabaseClient() resolves the Supabase URL/anon key
|
|
59
|
+
// under every alias the Vercel integration may inject (incl. the new publishable key)
|
|
60
|
+
// and degrades to a dummy client when unconfigured (the query below then returns []).
|
|
61
|
+
const supabase = getSsgSupabaseClient();
|
|
71
62
|
|
|
72
63
|
const { data: posts, error } = await supabase
|
|
73
64
|
.from("posts")
|
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
stripe,
|
|
9
9
|
syncStripeOrderFromSession,
|
|
10
10
|
} from '@nextblock-cms/ecommerce/server';
|
|
11
|
+
import { resolveSupabaseServiceKey, resolveSupabaseUrl } from '../../../lib/setup/env-status';
|
|
11
12
|
|
|
12
13
|
function getServiceRoleSupabaseClient() {
|
|
13
|
-
const supabaseUrl =
|
|
14
|
-
const serviceRoleKey =
|
|
14
|
+
const supabaseUrl = resolveSupabaseUrl();
|
|
15
|
+
const serviceRoleKey = resolveSupabaseServiceKey();
|
|
15
16
|
|
|
16
17
|
if (!supabaseUrl || !serviceRoleKey) {
|
|
17
18
|
throw new Error('Missing Supabase Service Role environment variables');
|