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.
- package/bin/create-nextblock.js +197 -88
- package/libs/sdk/tsconfig.lib.json +3 -0
- package/package.json +1 -1
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +118 -67
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +231 -77
- package/templates/nextblock-template/app/cms/revisions/actions.ts +213 -12
- package/templates/nextblock-template/app/cms/revisions/service.ts +174 -50
- package/templates/nextblock-template/app/layout.tsx +23 -3
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +25 -2
- package/templates/nextblock-template/public/images/metadata_image.webp +0 -0
- package/templates/nextblock-template/tsconfig.json +0 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
32
|
-
const
|
|
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
|
|
71
|
-
const
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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' },
|
|
@@ -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
|
-
|
|
219
|
+
`img-src 'self' data: blob:${r2Hostnames}`,
|
|
197
220
|
"font-src 'self'",
|
|
198
221
|
"object-src 'none'",
|
|
199
|
-
|
|
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'",
|
|
Binary file
|