create-nextblock 0.2.58 → 0.2.61

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.
@@ -4,6 +4,7 @@
4
4
  import { createClient } from "@nextblock-cms/db/server";
5
5
  import { restorePageToVersion, restorePostToVersion, reconstructPageVersionContent, reconstructPostVersionContent } from './service';
6
6
  import { getFullPageContent, getFullPostContent } from './utils';
7
+ import { compare } from 'fast-json-patch';
7
8
 
8
9
  type RevisionListItem = {
9
10
  id: number;
@@ -14,30 +15,230 @@ type RevisionListItem = {
14
15
  author?: { full_name?: string | null; username?: string | null } | null;
15
16
  };
16
17
 
17
- export async function listPageRevisions(pageId: number) {
18
+ const REVISIONS_PER_PAGE = 10; // Reduced to 10 as requested
19
+
20
+ export async function listPageRevisions(pageId: number, page = 1, startDate?: string, endDate?: string) {
18
21
  const supabase = createClient();
19
- const { data, error } = await supabase
22
+
23
+ let query = supabase
20
24
  .from('page_revisions')
21
- .select('id, page_id, author_id, version, revision_type, created_at, author:profiles(full_name, username)')
25
+ .select('id, page_id, author_id, version, revision_type, created_at, content, author:profiles(full_name, username)', { count: 'exact' })
22
26
  .eq('page_id', pageId)
23
- .order('version', { ascending: false });
27
+ .order('version', { ascending: false })
28
+ .limit(1000); // Fetch up to 1000 recent revisions to allow dense pagination after filtering
29
+
30
+ if (startDate) {
31
+ query = query.gte('created_at', `${startDate}T00:00:00.000Z`);
32
+ }
33
+ if (endDate) {
34
+ query = query.lte('created_at', `${endDate}T23:59:59.999Z`);
35
+ }
36
+
37
+ const { data, error } = await query; // count is less useful now as we filter
38
+
24
39
  if (error) return { error: error.message } as const;
25
- const { data: pageRow } = await supabase.from('pages').select('version').eq('id', pageId).single();
40
+
41
+ const { data: pageRow } = await supabase.from('pages').select('version, created_at').eq('id', pageId).single();
26
42
  const currentVersion = pageRow?.version ?? null;
27
- return { success: true as const, revisions: data as RevisionListItem[], currentVersion };
43
+ const currentContent = await getFullPageContent(pageId);
44
+
45
+ // Map data to include useful flags instead of filtering
46
+ const allRevisions = (data || []).map((r: any) => {
47
+ let has_changes = true;
48
+
49
+ // 1. Check Diffs
50
+ if (r.revision_type === 'diff') {
51
+ let ops = r.content;
52
+ if (typeof ops === 'string') {
53
+ try { ops = JSON.parse(ops); } catch { ops = []; }
54
+ }
55
+
56
+ // Strict check: Diff must be a non-empty array of operations
57
+ if (!Array.isArray(ops) || ops.length === 0) {
58
+ has_changes = false;
59
+ } else {
60
+ // Check metadata/variant
61
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
62
+ const usefulOps = ops.filter((op: any) => {
63
+ if (!op.path) return true;
64
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
65
+ });
66
+ if (usefulOps.length === 0) has_changes = false;
67
+ }
68
+ }
69
+
70
+ // 2. Check Snapshots
71
+ if (r.revision_type === 'snapshot' && currentContent && r.version !== currentVersion) {
72
+ let snapshotContent = r.content;
73
+ if (typeof snapshotContent === 'string') {
74
+ try { snapshotContent = JSON.parse(snapshotContent); } catch { /* default true */ }
75
+ }
76
+
77
+ const ops = compare(currentContent, snapshotContent);
78
+
79
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
80
+ const usefulOps = ops.filter((op: any) => {
81
+ if (!op.path) return true;
82
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
83
+ });
84
+
85
+ if (usefulOps.length === 0) has_changes = false;
86
+ }
87
+
88
+ return { ...r, has_changes };
89
+ }) as (RevisionListItem & { has_changes: boolean; content: any })[];
90
+
91
+ // Append Synthetic V1
92
+ const hasVersion1 = allRevisions.some(r => r.version === 1);
93
+ const pageCreatedAt = pageRow?.created_at;
94
+
95
+ let matchesDate = true;
96
+ if (pageCreatedAt) {
97
+ if (startDate && pageCreatedAt < `${startDate}T00:00:00.000Z`) matchesDate = false;
98
+ if (endDate && pageCreatedAt > `${endDate}T23:59:59.999Z`) matchesDate = false;
99
+ }
100
+
101
+ if (!hasVersion1 && (currentVersion ?? 0) > 1 && matchesDate) {
102
+ allRevisions.push({
103
+ id: -1,
104
+ version: 1,
105
+ revision_type: 'snapshot',
106
+ created_at: pageCreatedAt ?? new Date().toISOString(),
107
+ author_id: null,
108
+ author: { full_name: 'System (Initial)', username: 'system' },
109
+ has_changes: true, // V1 always counts
110
+ content: null
111
+ });
112
+ }
113
+
114
+ allRevisions.sort((a, b) => b.version - a.version);
115
+
116
+ const totalFiltered = allRevisions.length;
117
+ const totalPages = Math.ceil(totalFiltered / REVISIONS_PER_PAGE);
118
+
119
+ const fromIndex = (page - 1) * REVISIONS_PER_PAGE;
120
+ // Use map to strip heavy content if not needed, but keep flags
121
+ const slicedRevisions = allRevisions
122
+ .slice(fromIndex, fromIndex + REVISIONS_PER_PAGE)
123
+ .map(({ content, ...rest }) => rest);
124
+
125
+ return {
126
+ success: true as const,
127
+ revisions: slicedRevisions,
128
+ currentVersion,
129
+ count: totalFiltered,
130
+ totalPages,
131
+ hasMore: (page * REVISIONS_PER_PAGE) < totalFiltered
132
+ };
28
133
  }
29
134
 
30
- export async function listPostRevisions(postId: number) {
135
+ export async function listPostRevisions(postId: number, page = 1, startDate?: string, endDate?: string) {
31
136
  const supabase = createClient();
32
- const { data, error } = await supabase
137
+
138
+
139
+
140
+ let query = supabase
33
141
  .from('post_revisions')
34
- .select('id, post_id, author_id, version, revision_type, created_at, author:profiles(full_name, username)')
142
+ .select('id, post_id, author_id, version, revision_type, created_at, content, author:profiles(full_name, username)', { count: 'exact' })
35
143
  .eq('post_id', postId)
36
- .order('version', { ascending: false });
144
+ .order('version', { ascending: false })
145
+ .limit(1000);
146
+
147
+ if (startDate) {
148
+ query = query.gte('created_at', `${startDate}T00:00:00.000Z`);
149
+ }
150
+ if (endDate) {
151
+ query = query.lte('created_at', `${endDate}T23:59:59.999Z`);
152
+ }
153
+
154
+ const { data, error } = await query;
155
+
37
156
  if (error) return { error: error.message } as const;
38
- const { data: postRow } = await supabase.from('posts').select('version').eq('id', postId).single();
157
+
158
+ const { data: postRow } = await supabase.from('posts').select('version, created_at').eq('id', postId).single();
39
159
  const currentVersion = postRow?.version ?? null;
40
- return { success: true as const, revisions: data as RevisionListItem[], currentVersion };
160
+ const currentContent = await getFullPostContent(postId);
161
+
162
+ const allRevisions = (data || []).map((r: any) => {
163
+ let has_changes = true;
164
+
165
+ // 1. Check Diffs
166
+ if (r.revision_type === 'diff') {
167
+ let ops = r.content;
168
+ if (typeof ops === 'string') {
169
+ try { ops = JSON.parse(ops); } catch { ops = []; }
170
+ }
171
+
172
+ // Strict check: Diff must be a non-empty array of operations
173
+ if (!Array.isArray(ops) || ops.length === 0) {
174
+ has_changes = false;
175
+ } else {
176
+ // Check metadata/variant
177
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
178
+ const usefulOps = ops.filter((op: any) => {
179
+ if (!op.path) return true;
180
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
181
+ });
182
+ if (usefulOps.length === 0) has_changes = false;
183
+ }
184
+ }
185
+
186
+ // 2. Check Snapshots
187
+ if (r.revision_type === 'snapshot' && currentContent && r.version !== currentVersion) {
188
+ let snapshotContent = r.content;
189
+ if (typeof snapshotContent === 'string') {
190
+ try { snapshotContent = JSON.parse(snapshotContent); } catch { /* default true */ }
191
+ }
192
+
193
+ const ops = compare(currentContent, snapshotContent);
194
+
195
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
196
+ const usefulOps = ops.filter((op: any) => {
197
+ if (!op.path) return true;
198
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
199
+ });
200
+
201
+ if (usefulOps.length === 0) has_changes = false;
202
+ }
203
+
204
+ return { ...r, has_changes };
205
+ }) as (RevisionListItem & { has_changes: boolean; content: any })[];
206
+
207
+ // Synthetic V1
208
+ const hasVersion1 = allRevisions.some(r => r.version === 1);
209
+ const postCreatedAt = postRow?.created_at;
210
+
211
+ let matchesDate = true;
212
+ if (postCreatedAt) {
213
+ if (startDate && postCreatedAt < `${startDate}T00:00:00.000Z`) matchesDate = false;
214
+ if (endDate && postCreatedAt > `${endDate}T23:59:59.999Z`) matchesDate = false;
215
+ }
216
+
217
+ if (!hasVersion1 && (currentVersion ?? 0) > 1 && matchesDate) {
218
+ allRevisions.push({
219
+ id: -1,
220
+ version: 1,
221
+ revision_type: 'snapshot',
222
+ created_at: postCreatedAt ?? new Date().toISOString(),
223
+ author_id: null,
224
+ author: { full_name: 'System (Initial)', username: 'system' },
225
+ has_changes: true,
226
+ content: null
227
+ });
228
+ }
229
+
230
+ allRevisions.sort((a, b) => b.version - a.version);
231
+
232
+ const totalFiltered = allRevisions.length;
233
+ const totalPages = Math.ceil(totalFiltered / REVISIONS_PER_PAGE);
234
+
235
+ const fromIndex = (page - 1) * REVISIONS_PER_PAGE;
236
+ // Use map to strip heavy content if not needed, but keep flags
237
+ const slicedRevisions = allRevisions
238
+ .slice(fromIndex, fromIndex + REVISIONS_PER_PAGE)
239
+ .map(({ content, ...rest }) => rest);
240
+
241
+ return { success: true as const, revisions: slicedRevisions, currentVersion, count: totalFiltered, totalPages, hasMore: (page * REVISIONS_PER_PAGE) < totalFiltered };
41
242
  }
42
243
 
43
244
  export async function restorePageVersion(pageId: number, targetVersion: number) {
@@ -28,12 +28,31 @@ export async function createPageRevision(
28
28
  .single();
29
29
  if (pageError || !page) return { error: 'Page not found' } as const;
30
30
 
31
- const nextVersion = (page.version ?? 1) + 1;
32
- const makeSnapshot = shouldCreateSnapshot(page.version ?? 1) || nextVersion === 2; // ensure early snapshot cadence
31
+ const currentVersion = page.version ?? 1;
32
+ const nextVersion = currentVersion + 1;
33
+
34
+ // If we are moving to version 2, it means Version 1 was never saved (it was the initial state).
35
+ // We should save Version 1 now so we have a history base.
36
+ if (nextVersion === 2) {
37
+ await supabase.from('page_revisions').insert({
38
+ page_id: pageId,
39
+ author_id: authorId, // Can be current author or null
40
+ version: 1,
41
+ revision_type: 'snapshot',
42
+ content: previousContent as unknown as Json,
43
+ });
44
+ }
45
+
46
+ const makeSnapshot = shouldCreateSnapshot(currentVersion) || nextVersion === 2; // ensure early snapshot cadence
33
47
 
34
48
  const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
35
49
  const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
36
50
 
51
+ // If it's a diff and there are no changes, skip creating revision
52
+ if (revisionType === 'diff' && Array.isArray(content) && content.length === 0) {
53
+ return { success: true as const, version: currentVersion }; // Return current version as we didn't bump
54
+ }
55
+
37
56
  const { error: insertError } = await supabase.from('page_revisions').insert({
38
57
  page_id: pageId,
39
58
  author_id: authorId,
@@ -67,12 +86,28 @@ export async function createPostRevision(
67
86
  .single();
68
87
  if (postError || !post) return { error: 'Post not found' } as const;
69
88
 
70
- const nextVersion = (post.version ?? 1) + 1;
71
- const makeSnapshot = shouldCreateSnapshot(post.version ?? 1) || nextVersion === 2;
89
+ const currentVersion = post.version ?? 1;
90
+ const nextVersion = currentVersion + 1;
91
+
92
+ if (nextVersion === 2) {
93
+ await supabase.from('post_revisions').insert({
94
+ post_id: postId,
95
+ author_id: authorId,
96
+ version: 1,
97
+ revision_type: 'snapshot',
98
+ content: previousContent as unknown as Json,
99
+ });
100
+ }
101
+
102
+ const makeSnapshot = shouldCreateSnapshot(currentVersion) || nextVersion === 2;
72
103
 
73
104
  const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
74
105
  const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
75
106
 
107
+ if (revisionType === 'diff' && Array.isArray(content) && content.length === 0) {
108
+ return { success: true as const, version: currentVersion };
109
+ }
110
+
76
111
  const { error: insertError } = await supabase.from('post_revisions').insert({
77
112
  post_id: postId,
78
113
  author_id: authorId,
@@ -104,27 +139,50 @@ export async function restorePageToVersion(pageId: number, targetVersion: number
104
139
  .order('version', { ascending: false })
105
140
  .limit(1)
106
141
  .maybeSingle();
107
- if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
108
-
109
- let content = snapshot.content as unknown as FullPageContent;
110
142
 
111
- // 2. Fetch diffs up to target and apply
112
- const { data: diffs, error: diffsError } = await supabase
113
- .from('page_revisions')
114
- .select('version, content, revision_type')
115
- .eq('page_id', pageId)
116
- .gt('version', snapshot.version)
117
- .lte('version', targetVersion)
118
- .order('version', { ascending: true });
119
- if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
143
+ let content: FullPageContent;
144
+ let baseVersion = 0;
145
+
146
+ if (snapshot) {
147
+ content = snapshot.content as unknown as FullPageContent;
148
+ baseVersion = snapshot.version;
149
+ } else if (targetVersion === 1) {
150
+ // Fallback for missing Version 1: use empty content with current meta
151
+ const { data: pageMeta } = await supabase
152
+ .from('pages')
153
+ .select('title, slug, language_id, status, meta_title, meta_description')
154
+ .eq('id', pageId)
155
+ .single();
156
+ if (!pageMeta) return { error: 'Page not found.' } as const;
157
+ content = {
158
+ meta: pageMeta,
159
+ blocks: [],
160
+ };
161
+ baseVersion = 1;
162
+ } else {
163
+ if (snapshotError) return { error: `Snapshot error: ${snapshotError.message}` } as const;
164
+ return { error: 'No snapshot found at or before target version.' } as const;
165
+ }
120
166
 
121
- for (const r of diffs || []) {
122
- if (r.revision_type === 'diff') {
123
- const ops = r.content as any[];
124
- const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
125
- content = result.newDocument as unknown as FullPageContent;
126
- } else {
127
- content = r.content as unknown as FullPageContent;
167
+ // 2. Fetch diffs up to target and apply (only if we are not already at target)
168
+ if (baseVersion < targetVersion) {
169
+ const { data: diffs, error: diffsError } = await supabase
170
+ .from('page_revisions')
171
+ .select('version, content, revision_type')
172
+ .eq('page_id', pageId)
173
+ .gt('version', baseVersion)
174
+ .lte('version', targetVersion)
175
+ .order('version', { ascending: true });
176
+ if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
177
+
178
+ for (const r of diffs || []) {
179
+ if (r.revision_type === 'diff') {
180
+ const ops = r.content as any[];
181
+ const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
182
+ content = result.newDocument as unknown as FullPageContent;
183
+ } else {
184
+ content = r.content as unknown as FullPageContent;
185
+ }
128
186
  }
129
187
  }
130
188
 
@@ -193,17 +251,38 @@ export async function restorePostToVersion(postId: number, targetVersion: number
193
251
  .order('version', { ascending: false })
194
252
  .limit(1)
195
253
  .maybeSingle();
196
- if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
197
254
 
198
- let content = snapshot.content as unknown as FullPostContent;
255
+ let content: FullPostContent;
256
+ let baseVersion = 0;
257
+
258
+ if (snapshot) {
259
+ content = snapshot.content as unknown as FullPostContent;
260
+ baseVersion = snapshot.version;
261
+ } else if (targetVersion === 1) {
262
+ const { data: postMeta } = await supabase
263
+ .from('posts')
264
+ .select('title, slug, language_id, status, meta_title, meta_description, excerpt, published_at, feature_image_id')
265
+ .eq('id', postId)
266
+ .single();
267
+ if (!postMeta) return { error: 'Post not found.' } as const;
268
+ content = {
269
+ meta: postMeta,
270
+ blocks: [],
271
+ };
272
+ baseVersion = 1;
273
+ } else {
274
+ if (snapshotError) return { error: `Snapshot error: ${snapshotError.message}` } as const;
275
+ return { error: 'No snapshot found at or before target version.' } as const;
276
+ }
199
277
 
200
- const { data: diffs, error: diffsError } = await supabase
201
- .from('post_revisions')
202
- .select('version, content, revision_type')
203
- .eq('post_id', postId)
204
- .gt('version', snapshot.version)
205
- .lte('version', targetVersion)
206
- .order('version', { ascending: true });
278
+ if (baseVersion < targetVersion) {
279
+ const { data: diffs, error: diffsError } = await supabase
280
+ .from('post_revisions')
281
+ .select('version, content, revision_type')
282
+ .eq('post_id', postId)
283
+ .gt('version', baseVersion)
284
+ .lte('version', targetVersion)
285
+ .order('version', { ascending: true });
207
286
  if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
208
287
 
209
288
  for (const r of diffs || []) {
@@ -214,6 +293,7 @@ export async function restorePostToVersion(postId: number, targetVersion: number
214
293
  } else {
215
294
  content = r.content as unknown as FullPostContent;
216
295
  }
296
+ }
217
297
  }
218
298
 
219
299
  // Determine next version for post
@@ -281,17 +361,38 @@ export async function reconstructPageVersionContent(pageId: number, targetVersio
281
361
  .order('version', { ascending: false })
282
362
  .limit(1)
283
363
  .maybeSingle();
284
- if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
285
364
 
286
- let content = snapshot.content as unknown as FullPageContent;
365
+ let content: FullPageContent;
366
+ let baseVersion = 0;
367
+
368
+ if (snapshot) {
369
+ content = snapshot.content as unknown as FullPageContent;
370
+ baseVersion = snapshot.version;
371
+ } else if (targetVersion === 1) {
372
+ const { data: pageMeta } = await supabase
373
+ .from('pages')
374
+ .select('title, slug, language_id, status, meta_title, meta_description')
375
+ .eq('id', pageId)
376
+ .single();
377
+ if (!pageMeta) return { error: 'Page not found.' } as const;
378
+ content = {
379
+ meta: pageMeta,
380
+ blocks: [],
381
+ };
382
+ baseVersion = 1;
383
+ } else {
384
+ if (snapshotError) return { error: `Snapshot error: ${snapshotError.message}` } as const;
385
+ return { error: 'No snapshot found at or before target version.' } as const;
386
+ }
287
387
 
288
- const { data: diffs, error: diffsError } = await supabase
289
- .from('page_revisions')
290
- .select('version, content, revision_type')
291
- .eq('page_id', pageId)
292
- .gt('version', snapshot.version)
293
- .lte('version', targetVersion)
294
- .order('version', { ascending: true });
388
+ if (baseVersion < targetVersion) {
389
+ const { data: diffs, error: diffsError } = await supabase
390
+ .from('page_revisions')
391
+ .select('version, content, revision_type')
392
+ .eq('page_id', pageId)
393
+ .gt('version', baseVersion)
394
+ .lte('version', targetVersion)
395
+ .order('version', { ascending: true });
295
396
  if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
296
397
 
297
398
  for (const r of diffs || []) {
@@ -303,6 +404,7 @@ export async function reconstructPageVersionContent(pageId: number, targetVersio
303
404
  content = r.content as unknown as FullPageContent;
304
405
  }
305
406
  }
407
+ }
306
408
  return { success: true as const, content };
307
409
  }
308
410
 
@@ -318,17 +420,38 @@ export async function reconstructPostVersionContent(postId: number, targetVersio
318
420
  .order('version', { ascending: false })
319
421
  .limit(1)
320
422
  .maybeSingle();
321
- if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
322
423
 
323
- let content = snapshot.content as unknown as FullPostContent;
424
+ let content: FullPostContent;
425
+ let baseVersion = 0;
426
+
427
+ if (snapshot) {
428
+ content = snapshot.content as unknown as FullPostContent;
429
+ baseVersion = snapshot.version;
430
+ } else if (targetVersion === 1) {
431
+ const { data: postMeta } = await supabase
432
+ .from('posts')
433
+ .select('title, slug, language_id, status, meta_title, meta_description, excerpt, published_at, feature_image_id')
434
+ .eq('id', postId)
435
+ .single();
436
+ if (!postMeta) return { error: 'Post not found.' } as const;
437
+ content = {
438
+ meta: postMeta,
439
+ blocks: [],
440
+ };
441
+ baseVersion = 1;
442
+ } else {
443
+ if (snapshotError) return { error: `Snapshot error: ${snapshotError.message}` } as const;
444
+ return { error: 'No snapshot found at or before target version.' } as const;
445
+ }
324
446
 
325
- const { data: diffs, error: diffsError } = await supabase
326
- .from('post_revisions')
327
- .select('version, content, revision_type')
328
- .eq('post_id', postId)
329
- .gt('version', snapshot.version)
330
- .lte('version', targetVersion)
331
- .order('version', { ascending: true });
447
+ if (baseVersion < targetVersion) {
448
+ const { data: diffs, error: diffsError } = await supabase
449
+ .from('post_revisions')
450
+ .select('version, content, revision_type')
451
+ .eq('post_id', postId)
452
+ .gt('version', baseVersion)
453
+ .lte('version', targetVersion)
454
+ .order('version', { ascending: true });
332
455
  if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
333
456
 
334
457
  for (const r of diffs || []) {
@@ -340,5 +463,6 @@ export async function reconstructPostVersionContent(postId: number, targetVersio
340
463
  content = r.content as unknown as FullPostContent;
341
464
  }
342
465
  }
466
+ }
343
467
  return { success: true as const, content };
344
468
  }
@@ -17,9 +17,7 @@ import { getTranslations } from '@/app/cms/settings/extra-translations/actions';
17
17
  import type { Database } from '@nextblock-cms/db';
18
18
  import { headers, cookies } from 'next/headers';
19
19
 
20
- const defaultUrl = process.env.NEXT_PUBLIC_URL
21
- ? `https://${process.env.NEXT_PUBLIC_URL}`
22
- : "http://localhost:3000";
20
+ const defaultUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3000";
23
21
 
24
22
  const DEFAULT_LOCALE_FOR_LAYOUT = 'en';
25
23
 
@@ -106,6 +104,28 @@ export const metadata: Metadata = {
106
104
  metadataBase: new URL(defaultUrl),
107
105
  title: 'Nextblock CMS',
108
106
  description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
107
+ openGraph: {
108
+ title: 'Nextblock CMS',
109
+ description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
110
+ url: defaultUrl,
111
+ siteName: 'Nextblock CMS',
112
+ images: [
113
+ {
114
+ url: '/images/metadata_image.webp',
115
+ width: 1200,
116
+ height: 630,
117
+ alt: 'Nextblock CMS',
118
+ },
119
+ ],
120
+ locale: 'en_US',
121
+ type: 'website',
122
+ },
123
+ twitter: {
124
+ card: 'summary_large_image',
125
+ title: 'Nextblock CMS',
126
+ description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
127
+ images: ['/images/metadata_image.webp'],
128
+ },
109
129
  icons: {
110
130
  icon: [
111
131
  { url: '/favicon/favicon.ico' },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/template",
3
- "version": "0.2.36",
3
+ "version": "0.2.39",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -189,14 +189,37 @@ export default async function proxy(request: NextRequest) {
189
189
  if (process.env.NODE_ENV === 'production') {
190
190
  const nonceValue = requestHeaders.get('x-nonce');
191
191
  if (nonceValue) {
192
+ const supabaseHostname = new URL(supabaseUrl).hostname;
193
+
194
+ const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
195
+ const r2PublicUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
196
+ const r2BucketName = process.env.R2_BUCKET_NAME;
197
+
198
+ let r2Hostnames = '';
199
+ if (r2BaseUrl) {
200
+ try {
201
+ r2Hostnames += ` https://${new URL(r2BaseUrl).hostname}`;
202
+ } catch (e) {
203
+ console.error('Invalid NEXT_PUBLIC_R2_BASE_URL', e);
204
+ }
205
+ }
206
+ if (r2PublicUrl && r2BucketName) {
207
+ try {
208
+ const publicHostname = new URL(r2PublicUrl).hostname;
209
+ r2Hostnames += ` https://${r2BucketName}.${publicHostname}`;
210
+ } catch (e) {
211
+ console.error('Invalid NEXT_PUBLIC_R2_PUBLIC_URL', e);
212
+ }
213
+ }
214
+
192
215
  const csp = [
193
216
  "default-src 'self'",
194
217
  `script-src 'self' blob: data: 'nonce-${nonceValue}'`,
195
218
  "style-src 'self' 'unsafe-inline'",
196
- "img-src 'self' data: blob: https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
219
+ `img-src 'self' data: blob:${r2Hostnames}`,
197
220
  "font-src 'self'",
198
221
  "object-src 'none'",
199
- "connect-src 'self' https://ppcppwsfnrptznvbxnsz.supabase.co wss://ppcppwsfnrptznvbxnsz.supabase.co https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
222
+ `connect-src 'self' https://${supabaseHostname} wss://${supabaseHostname}${r2Hostnames}`,
200
223
  "frame-src 'self' blob: data: https://www.youtube.com",
201
224
  "form-action 'self'",
202
225
  "base-uri 'self'",
@@ -37,7 +37,6 @@
37
37
  "**/*.jsx",
38
38
  "next-env.d.ts",
39
39
  ".next/types/**/*.ts",
40
- "../../dist/apps/nextblock/.next/types/**/*.ts",
41
40
  "../../libs/ui/tailwind.config.js"
42
41
  ],
43
42
  "exclude": [