create-nextblock 0.16.2 → 0.16.3
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/api/cron/reset-sandbox/route.ts +4 -4
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +1827 -189
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +7 -1
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +8 -1
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +18 -1
- package/templates/nextblock-template/lib/seo/page-document.test.ts +34 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +52 -1
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +192 -177
- package/templates/nextblock-template/package.json +1 -1
|
@@ -1,227 +1,273 @@
|
|
|
1
|
-
// app/cms/pages/page.tsx
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { Button } from "@nextblock-cms/ui";
|
|
6
|
-
import {
|
|
7
|
-
Table,
|
|
8
|
-
TableBody,
|
|
9
|
-
TableCell,
|
|
10
|
-
TableHead,
|
|
11
|
-
TableHeader,
|
|
12
|
-
TableRow,
|
|
13
|
-
} from "@nextblock-cms/ui";
|
|
14
|
-
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
-
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
16
|
-
import {
|
|
17
|
-
MoreHorizontal,
|
|
18
|
-
PlusCircle,
|
|
19
|
-
Edit3,
|
|
20
|
-
FileText,
|
|
21
|
-
} from "lucide-react";
|
|
22
|
-
import {
|
|
23
|
-
DropdownMenu,
|
|
24
|
-
DropdownMenuContent,
|
|
25
|
-
DropdownMenuItem,
|
|
26
|
-
DropdownMenuButtonTrigger,
|
|
27
|
-
DropdownMenuSeparator,
|
|
28
|
-
} from "@nextblock-cms/ui";
|
|
29
|
-
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import
|
|
36
|
-
import
|
|
37
|
-
import
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
1
|
+
// app/cms/pages/page.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Button } from "@nextblock-cms/ui";
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from "@nextblock-cms/ui";
|
|
14
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
+
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
16
|
+
import {
|
|
17
|
+
MoreHorizontal,
|
|
18
|
+
PlusCircle,
|
|
19
|
+
Edit3,
|
|
20
|
+
FileText,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import {
|
|
23
|
+
DropdownMenu,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuButtonTrigger,
|
|
27
|
+
DropdownMenuSeparator,
|
|
28
|
+
} from "@nextblock-cms/ui";
|
|
29
|
+
import type { Database } from "@nextblock-cms/db";
|
|
30
|
+
import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
|
|
31
|
+
import { auditSeo } from "@nextblock-cms/utils/seo";
|
|
32
|
+
import { buildPageSeoDocument } from "../../../lib/seo/page-document";
|
|
33
|
+
|
|
34
|
+
type Page = Database["public"]["Tables"]["pages"]["Row"];
|
|
35
|
+
import LanguageFilterSelect from "../components/LanguageFilterSelect";
|
|
36
|
+
import DeletePageButtonClient from "./components/DeletePageButtonClient";
|
|
37
|
+
import { ContentTransferControls } from "../import-export/ContentTransferControls";
|
|
38
|
+
import VisibilityBadge from "../components/VisibilityBadge";
|
|
39
|
+
import SeoScoreBadge from "../components/SeoScoreBadge";
|
|
40
|
+
import TablePagination from "../components/TablePagination";
|
|
41
|
+
|
|
42
|
+
async function getPagesWithDetails(
|
|
43
|
+
filterLanguageId?: number,
|
|
44
|
+
pageNumber: number = 1,
|
|
45
|
+
pageSize: number = 25
|
|
46
|
+
): Promise<{
|
|
47
|
+
items: { page: Page; languageCode: string; seoScore: number }[];
|
|
48
|
+
totalCount: number;
|
|
49
|
+
}> {
|
|
50
|
+
const supabase = createClient();
|
|
51
|
+
const languages = await getActiveLanguagesServerSide();
|
|
52
|
+
const langMap = new Map(languages.map((l) => [l.id, l.code]));
|
|
53
|
+
|
|
54
|
+
let query = supabase
|
|
55
|
+
.from("pages")
|
|
56
|
+
.select("*, languages!inner(code), blocks(id, block_type, content, order)", { count: "exact" })
|
|
57
|
+
.order("created_at", { ascending: false });
|
|
58
|
+
|
|
59
|
+
if (filterLanguageId) {
|
|
60
|
+
query = query.eq("language_id", filterLanguageId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const from = (pageNumber - 1) * pageSize;
|
|
64
|
+
const to = from + pageSize - 1;
|
|
65
|
+
|
|
66
|
+
const { data: pagesData, count, error } = await query.range(from, to);
|
|
67
|
+
|
|
68
|
+
if (error) {
|
|
69
|
+
console.error("Error fetching pages:", error);
|
|
70
|
+
return { items: [], totalCount: 0 };
|
|
71
|
+
}
|
|
72
|
+
if (!pagesData) return { items: [], totalCount: 0 };
|
|
73
|
+
|
|
74
|
+
const items = pagesData.map((p) => {
|
|
75
|
+
const langInfo = p.languages as unknown as { code: string } | null;
|
|
76
|
+
const rawBlocks = (p.blocks || []) as unknown as Parameters<typeof buildPageSeoDocument>[0];
|
|
77
|
+
const doc = buildPageSeoDocument(rawBlocks);
|
|
78
|
+
const auditResult = auditSeo({
|
|
79
|
+
document: doc,
|
|
80
|
+
metaTitle: p.meta_title ?? undefined,
|
|
81
|
+
metaDescription: p.meta_description ?? undefined,
|
|
82
|
+
scope: "page",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const langCode = langInfo?.code || langMap.get(p.language_id) || "N/A";
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
page: p as unknown as Page,
|
|
89
|
+
languageCode: String(langCode).toUpperCase(),
|
|
90
|
+
seoScore: auditResult.score,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { items, totalCount: count ?? items.length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface CmsPagesListPageProps {
|
|
98
|
+
searchParams?: Promise<{
|
|
99
|
+
lang?: string;
|
|
100
|
+
success?: string;
|
|
101
|
+
page?: string;
|
|
102
|
+
pageSize?: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default async function CmsPagesListPage(props: CmsPagesListPageProps) {
|
|
107
|
+
const searchParams = await props.searchParams;
|
|
108
|
+
const allLanguages = await getActiveLanguagesServerSide();
|
|
109
|
+
const selectedLangId = searchParams?.lang
|
|
110
|
+
? parseInt(searchParams.lang, 10)
|
|
111
|
+
: undefined;
|
|
112
|
+
|
|
113
|
+
const isValidLangId = selectedLangId
|
|
114
|
+
? allLanguages.some((l) => l.id === selectedLangId)
|
|
115
|
+
: true;
|
|
116
|
+
const filterLangId = isValidLangId ? selectedLangId : undefined;
|
|
117
|
+
|
|
118
|
+
const pageNumber = searchParams?.page
|
|
119
|
+
? Math.max(1, parseInt(searchParams.page, 10) || 1)
|
|
120
|
+
: 1;
|
|
121
|
+
const pageSize = searchParams?.pageSize
|
|
122
|
+
? Math.max(1, parseInt(searchParams.pageSize, 10) || 25)
|
|
123
|
+
: 25;
|
|
124
|
+
|
|
125
|
+
const { items: pagesWithDetails, totalCount } = await getPagesWithDetails(
|
|
126
|
+
filterLangId,
|
|
127
|
+
pageNumber,
|
|
128
|
+
pageSize
|
|
129
|
+
);
|
|
130
|
+
const successMessage = searchParams?.success;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="w-full">
|
|
134
|
+
<div className="flex justify-between items-center mb-6 flex-wrap gap-4">
|
|
135
|
+
<h1 className="text-2xl font-semibold">Manage Pages</h1>
|
|
136
|
+
<div className="flex items-center gap-3">
|
|
137
|
+
<ContentTransferControls
|
|
138
|
+
contentType="pages"
|
|
139
|
+
label="Pages"
|
|
140
|
+
languageId={filterLangId}
|
|
141
|
+
hasContent={pagesWithDetails.length > 0}
|
|
142
|
+
/>
|
|
143
|
+
<LanguageFilterSelect
|
|
144
|
+
allLanguages={allLanguages}
|
|
145
|
+
currentFilterLangId={filterLangId}
|
|
146
|
+
basePath="/cms/pages"
|
|
147
|
+
/>
|
|
148
|
+
<Button variant="default" asChild>
|
|
149
|
+
<Link href="/cms/pages/new">
|
|
150
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Create New Page
|
|
151
|
+
</Link>
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{successMessage && (
|
|
157
|
+
<Alert variant="success" className="mb-4">
|
|
158
|
+
<AlertDescription>
|
|
159
|
+
{decodeURIComponent(successMessage)}
|
|
160
|
+
</AlertDescription>
|
|
161
|
+
</Alert>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{totalCount === 0 ? (
|
|
165
|
+
<div className="text-center py-10 border rounded-lg dark:border-slate-700">
|
|
166
|
+
<FileText className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
167
|
+
<h3 className="mt-2 text-sm font-medium text-foreground">
|
|
168
|
+
{filterLangId
|
|
169
|
+
? "No pages found for the selected language."
|
|
170
|
+
: "No pages found."}
|
|
171
|
+
</h3>
|
|
172
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
173
|
+
Get started by creating a new page.
|
|
174
|
+
</p>
|
|
175
|
+
<div className="mt-6">
|
|
176
|
+
<Button asChild>
|
|
177
|
+
<Link href="/cms/pages/new">
|
|
178
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Create Page
|
|
179
|
+
</Link>
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
) : (
|
|
184
|
+
<div className="rounded-lg border overflow-hidden dark:border-slate-700">
|
|
185
|
+
<Table>
|
|
186
|
+
<TableHeader>
|
|
187
|
+
<TableRow className="dark:border-slate-700">
|
|
188
|
+
<TableHead className="w-[280px] sm:w-[350px]">Title</TableHead>
|
|
189
|
+
<TableHead>Status</TableHead>
|
|
190
|
+
<TableHead>Language</TableHead>
|
|
191
|
+
<TableHead className="hidden md:table-cell">Slug</TableHead>
|
|
192
|
+
<TableHead>SEO</TableHead>
|
|
193
|
+
<TableHead className="hidden lg:table-cell">
|
|
194
|
+
Last Updated
|
|
195
|
+
</TableHead>
|
|
196
|
+
<TableHead className="text-right w-[80px]">Actions</TableHead>
|
|
197
|
+
</TableRow>
|
|
198
|
+
</TableHeader>
|
|
199
|
+
<TableBody>
|
|
200
|
+
{pagesWithDetails.map(({ page, languageCode, seoScore }) => (
|
|
201
|
+
<TableRow key={page.id} className="dark:border-slate-700">
|
|
202
|
+
<TableCell className="font-medium">
|
|
203
|
+
<Link
|
|
204
|
+
href={`/cms/pages/${page.id}/edit`}
|
|
205
|
+
className="flex items-center cursor-pointer"
|
|
206
|
+
>
|
|
207
|
+
<Edit3 className="mr-2 h-4 w-4" />
|
|
208
|
+
{page.title}
|
|
209
|
+
</Link>
|
|
210
|
+
</TableCell>
|
|
211
|
+
<TableCell>
|
|
212
|
+
<VisibilityBadge
|
|
213
|
+
type="page"
|
|
214
|
+
status={page.status}
|
|
215
|
+
publishedAt={page.published_at}
|
|
216
|
+
/>
|
|
217
|
+
</TableCell>
|
|
218
|
+
<TableCell>
|
|
219
|
+
<Badge variant="outline" className="dark:border-slate-600">
|
|
220
|
+
{languageCode}
|
|
221
|
+
</Badge>
|
|
222
|
+
</TableCell>
|
|
223
|
+
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">
|
|
224
|
+
/{page.slug}
|
|
225
|
+
</TableCell>
|
|
226
|
+
<TableCell>
|
|
227
|
+
<SeoScoreBadge score={seoScore} />
|
|
228
|
+
</TableCell>
|
|
229
|
+
<TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
|
|
230
|
+
{new Date(page.updated_at).toLocaleDateString()}
|
|
231
|
+
</TableCell>
|
|
232
|
+
<TableCell className="text-right">
|
|
233
|
+
<DropdownMenu>
|
|
234
|
+
<DropdownMenuButtonTrigger id={`page-trigger-${page.id}`}>
|
|
235
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
236
|
+
<span className="sr-only">
|
|
237
|
+
Page actions for {page.title}
|
|
238
|
+
</span>
|
|
239
|
+
</DropdownMenuButtonTrigger>
|
|
240
|
+
<DropdownMenuContent align="end">
|
|
241
|
+
<DropdownMenuItem asChild>
|
|
242
|
+
<Link
|
|
243
|
+
href={`/cms/pages/${page.id}/edit`}
|
|
244
|
+
className="flex items-center cursor-pointer"
|
|
245
|
+
>
|
|
246
|
+
<Edit3 className="mr-2 h-4 w-4" /> Edit
|
|
247
|
+
</Link>
|
|
248
|
+
</DropdownMenuItem>
|
|
249
|
+
<DropdownMenuSeparator />
|
|
250
|
+
<DeletePageButtonClient
|
|
251
|
+
pageId={page.id}
|
|
252
|
+
pageTitle={page.title}
|
|
253
|
+
/>
|
|
254
|
+
</DropdownMenuContent>
|
|
255
|
+
</DropdownMenu>
|
|
256
|
+
</TableCell>
|
|
257
|
+
</TableRow>
|
|
258
|
+
))}
|
|
259
|
+
</TableBody>
|
|
260
|
+
</Table>
|
|
261
|
+
|
|
262
|
+
<TablePagination
|
|
263
|
+
currentPage={pageNumber}
|
|
264
|
+
pageSize={pageSize}
|
|
265
|
+
totalCount={totalCount}
|
|
266
|
+
basePath="/cms/pages"
|
|
267
|
+
itemLabel="pages"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
@@ -189,6 +189,8 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
189
189
|
paint instead of an empty document.
|
|
190
190
|
*/}
|
|
191
191
|
<PageSeoProvider
|
|
192
|
+
documentTitle={postWithBlocks.title}
|
|
193
|
+
documentType="post"
|
|
192
194
|
initialBlocks={postWithBlocks.blocks}
|
|
193
195
|
initialMetaDescription={postWithBlocks.meta_description}
|
|
194
196
|
initialMetaTitle={postWithBlocks.meta_title}
|
|
@@ -336,11 +336,17 @@ export default function PostForm({
|
|
|
336
336
|
* also bails when the values are unchanged, so nothing here can loop back into a render of
|
|
337
337
|
* this form.
|
|
338
338
|
*/
|
|
339
|
-
const
|
|
339
|
+
const pageSeo = usePageSeo();
|
|
340
|
+
const setPageSeoMeta = pageSeo?.setMeta;
|
|
341
|
+
const setPageSeoDocumentTitle = pageSeo?.setDocumentTitle;
|
|
340
342
|
useEffect(() => {
|
|
341
343
|
setPageSeoMeta?.({ metaDescription, metaTitle });
|
|
342
344
|
}, [metaDescription, metaTitle, setPageSeoMeta]);
|
|
343
345
|
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
setPageSeoDocumentTitle?.(title);
|
|
348
|
+
}, [title, setPageSeoDocumentTitle]);
|
|
349
|
+
|
|
344
350
|
// Remove languagesLoading from this condition
|
|
345
351
|
if (authLoading) {
|
|
346
352
|
return <div>Loading form...</div>;
|