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.
@@ -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"; // Trash2 removed from here
22
- import {
23
- DropdownMenu,
24
- DropdownMenuContent,
25
- DropdownMenuItem,
26
- DropdownMenuButtonTrigger,
27
- DropdownMenuSeparator,
28
- } from "@nextblock-cms/ui";
29
- // Server action `deletePage` is used by DeletePageButtonClient
30
- import type { Database } from "@nextblock-cms/db";
31
- import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
32
-
33
- type Page = Database["public"]["Tables"]["pages"]["Row"];
34
- import LanguageFilterSelect from "../components/LanguageFilterSelect";
35
- import DeletePageButtonClient from "./components/DeletePageButtonClient"; // Import the client component
36
- import { ContentTransferControls } from "../import-export/ContentTransferControls";
37
- import VisibilityBadge from "../components/VisibilityBadge";
38
-
39
- async function getPagesWithDetails(
40
- filterLanguageId?: number
41
- ): Promise<{ page: Page; languageCode: string }[]> {
42
- const supabase = createClient();
43
- const languages = await getActiveLanguagesServerSide();
44
- const langMap = new Map(languages.map((l) => [l.id, l.code]));
45
-
46
- let query = supabase
47
- .from("pages")
48
- .select("*, languages!inner(code)")
49
- .order("created_at", { ascending: false });
50
-
51
- if (filterLanguageId) {
52
- query = query.eq("language_id", filterLanguageId);
53
- }
54
-
55
- const { data: pagesData, error } = await query;
56
-
57
- if (error) {
58
- console.error("Error fetching pages:", error);
59
- return [];
60
- }
61
- if (!pagesData) return [];
62
-
63
- return pagesData.map((p) => {
64
- const langInfo = p.languages as unknown as { code: string } | null;
65
- return {
66
- page: p as Page,
67
- languageCode:
68
- langInfo?.code?.toUpperCase() ||
69
- langMap.get(p.language_id)?.toUpperCase() ||
70
- "N/A",
71
- };
72
- });
73
- }
74
-
75
- interface CmsPagesListPageProps {
76
- searchParams?: Promise<{
77
- lang?: string;
78
- success?: string;
79
- }>;
80
- }
81
-
82
- export default async function CmsPagesListPage(props: CmsPagesListPageProps) {
83
- const searchParams = await props.searchParams;
84
- const allLanguages = await getActiveLanguagesServerSide();
85
- const selectedLangId = searchParams?.lang
86
- ? parseInt(searchParams.lang, 10)
87
- : undefined;
88
-
89
- const isValidLangId = selectedLangId
90
- ? allLanguages.some((l) => l.id === selectedLangId)
91
- : true;
92
- const filterLangId = isValidLangId ? selectedLangId : undefined;
93
-
94
- const pagesWithDetails = await getPagesWithDetails(filterLangId);
95
- const successMessage = searchParams?.success;
96
-
97
- return (
98
- <div className="w-full">
99
- <div className="flex justify-between items-center mb-6 flex-wrap gap-4">
100
- <h1 className="text-2xl font-semibold">Manage Pages</h1>
101
- <div className="flex items-center gap-3">
102
- <ContentTransferControls
103
- contentType="pages"
104
- label="Pages"
105
- languageId={filterLangId}
106
- hasContent={pagesWithDetails.length > 0}
107
- />
108
- <LanguageFilterSelect
109
- allLanguages={allLanguages}
110
- currentFilterLangId={filterLangId}
111
- basePath="/cms/pages"
112
- />
113
- <Button variant="default" asChild>
114
- <Link href="/cms/pages/new">
115
- <PlusCircle className="mr-2 h-4 w-4" /> Create New Page
116
- </Link>
117
- </Button>
118
- </div>
119
- </div>
120
-
121
- {successMessage && (
122
- <Alert variant="success" className="mb-4">
123
- <AlertDescription>
124
- {decodeURIComponent(successMessage)}
125
- </AlertDescription>
126
- </Alert>
127
- )}
128
-
129
- {pagesWithDetails.length === 0 ? (
130
- <div className="text-center py-10 border rounded-lg dark:border-slate-700">
131
- <FileText className="mx-auto h-12 w-12 text-muted-foreground" />
132
- <h3 className="mt-2 text-sm font-medium text-foreground">
133
- {filterLangId
134
- ? "No pages found for the selected language."
135
- : "No pages found."}
136
- </h3>
137
- <p className="mt-1 text-sm text-muted-foreground">
138
- Get started by creating a new page.
139
- </p>
140
- <div className="mt-6">
141
- <Button asChild>
142
- <Link href="/cms/pages/new">
143
- <PlusCircle className="mr-2 h-4 w-4" /> Create Page
144
- </Link>
145
- </Button>
146
- </div>
147
- </div>
148
- ) : (
149
- <div className="rounded-lg border overflow-hidden dark:border-slate-700">
150
- <Table>
151
- <TableHeader>
152
- <TableRow className="dark:border-slate-700">
153
- <TableHead className="w-[300px] sm:w-[400px]">Title</TableHead>
154
- <TableHead>Status</TableHead>
155
- <TableHead>Language</TableHead>
156
- <TableHead className="hidden md:table-cell">Slug</TableHead>
157
- <TableHead className="hidden lg:table-cell">
158
- Last Updated
159
- </TableHead>
160
- <TableHead className="text-right w-[80px]">Actions</TableHead>
161
- </TableRow>
162
- </TableHeader>
163
- <TableBody>
164
- {pagesWithDetails.map(({ page, languageCode }) => (
165
- <TableRow key={page.id} className="dark:border-slate-700">
166
- <TableCell className="font-medium">
167
- <Link
168
- href={`/cms/pages/${page.id}/edit`}
169
- className="flex items-center cursor-pointer"
170
- >
171
- <Edit3 className="mr-2 h-4 w-4" />
172
- {page.title}
173
- </Link>
174
- </TableCell>
175
- <TableCell>
176
- <VisibilityBadge
177
- type="page"
178
- status={page.status}
179
- publishedAt={page.published_at}
180
- />
181
- </TableCell>
182
- <TableCell>
183
- <Badge variant="outline" className="dark:border-slate-600">
184
- {languageCode}
185
- </Badge>
186
- </TableCell>
187
- <TableCell className="text-muted-foreground text-xs hidden md:table-cell">
188
- /{page.slug}
189
- </TableCell>
190
- <TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
191
- {new Date(page.updated_at).toLocaleDateString()}
192
- </TableCell>
193
- <TableCell className="text-right">
194
- <DropdownMenu>
195
- <DropdownMenuButtonTrigger id={`page-trigger-${page.id}`}>
196
- <MoreHorizontal className="h-4 w-4" />
197
- <span className="sr-only">
198
- Page actions for {page.title}
199
- </span>
200
- </DropdownMenuButtonTrigger>
201
- <DropdownMenuContent align="end">
202
- <DropdownMenuItem asChild>
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" /> Edit
208
- </Link>
209
- </DropdownMenuItem>
210
- <DropdownMenuSeparator />
211
- {/* Use the Client Component for the delete button */}
212
- <DeletePageButtonClient
213
- pageId={page.id}
214
- pageTitle={page.title}
215
- />
216
- </DropdownMenuContent>
217
- </DropdownMenu>
218
- </TableCell>
219
- </TableRow>
220
- ))}
221
- </TableBody>
222
- </Table>
223
- </div>
224
- )}
225
- </div>
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 setPageSeoMeta = usePageSeo()?.setMeta;
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>;