create-nextblock 0.1.0 → 0.2.1
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 +1193 -920
- package/package.json +6 -2
- package/scripts/sync-template.js +279 -276
- package/templates/nextblock-template/.env.example +1 -14
- package/templates/nextblock-template/README.md +1 -1
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -40
- package/templates/nextblock-template/app/[slug]/page.tsx +45 -10
- package/templates/nextblock-template/app/[slug]/page.utils.ts +92 -45
- package/templates/nextblock-template/app/api/revalidate/route.ts +15 -15
- package/templates/nextblock-template/app/{blog → article}/[slug]/PostClientContent.tsx +45 -43
- package/templates/nextblock-template/app/{blog → article}/[slug]/page.tsx +108 -98
- package/templates/nextblock-template/app/{blog → article}/[slug]/page.utils.ts +10 -3
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +25 -19
- package/templates/nextblock-template/app/cms/blocks/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/actions.ts +47 -44
- package/templates/nextblock-template/app/cms/posts/page.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -15
- package/templates/nextblock-template/app/layout.tsx +9 -9
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +52 -52
- package/templates/nextblock-template/app/sitemap.xml/route.ts +2 -2
- package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -16
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -7
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +25 -26
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +4 -4
- package/templates/nextblock-template/public/images/NBcover.webp +0 -0
- package/templates/nextblock-template/public/images/developer.webp +0 -0
- package/templates/nextblock-template/public/images/nextblock-logo-small.webp +0 -0
- package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
- package/templates/nextblock-template/public/images/programmer-upscaled.webp +0 -0
- package/templates/nextblock-template/scripts/backup.js +142 -47
- package/templates/nextblock-template/scripts/restore-working.js +102 -0
- package/templates/nextblock-template/scripts/restore.js +434 -0
- package/templates/nextblock-template/app/blog/page.tsx +0 -77
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +0 -8057
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +0 -8159
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +0 -8411
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +0 -8442
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +0 -8442
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +0 -8803
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +0 -9749
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// app/
|
|
1
|
+
// app/article/[slug]/page.tsx
|
|
2
2
|
import React from 'react';
|
|
3
3
|
// Remove or alias the problematic import if only used by other functions:
|
|
4
4
|
// import { createClient } from "@nextblock-cms/db/server";
|
|
5
5
|
import { createClient as createSupabaseJsClient } from '@supabase/supabase-js'; // Import base client
|
|
6
6
|
import { notFound } from "next/navigation";
|
|
7
7
|
import type { Metadata } from 'next';
|
|
8
|
-
import PostClientContent from "./PostClientContent";
|
|
9
|
-
|
|
10
|
-
import { getPostDataBySlug } from "./page.utils";
|
|
11
|
-
import BlockRenderer from "../../../components/BlockRenderer";
|
|
12
|
-
import { getSsgSupabaseClient } from "@nextblock-cms/db"; // Correct import
|
|
13
|
-
import type { HeroBlockContent } from '../../../lib/blocks/blockRegistry';
|
|
8
|
+
import PostClientContent from "./PostClientContent";
|
|
9
|
+
|
|
10
|
+
import { getPostDataBySlug } from "./page.utils";
|
|
11
|
+
import BlockRenderer from "../../../components/BlockRenderer";
|
|
12
|
+
import { getSsgSupabaseClient } from "@nextblock-cms/db"; // Correct import
|
|
13
|
+
import type { HeroBlockContent } from '../../../lib/blocks/blockRegistry';
|
|
14
14
|
|
|
15
15
|
export const dynamicParams = true;
|
|
16
16
|
export const revalidate = 3600;
|
|
@@ -23,12 +23,23 @@ interface PostPageProps {
|
|
|
23
23
|
params: Promise<ResolvedPostParams>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
interface PostTranslation {
|
|
27
|
-
slug: string;
|
|
28
|
-
languages: {
|
|
29
|
-
code: string;
|
|
30
|
-
}[];
|
|
31
|
-
}
|
|
26
|
+
interface PostTranslation {
|
|
27
|
+
slug: string;
|
|
28
|
+
languages: {
|
|
29
|
+
code: string;
|
|
30
|
+
}[] | { code: string };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resolveLanguageCode = (languagesField: PostTranslation["languages"]): string | null => {
|
|
34
|
+
if (!languagesField) return null;
|
|
35
|
+
if (Array.isArray(languagesField)) {
|
|
36
|
+
return languagesField[0]?.code ?? null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof languagesField === 'object' && 'code' in languagesField) {
|
|
39
|
+
return (languagesField as { code?: string }).code ?? null;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
32
43
|
|
|
33
44
|
export async function generateStaticParams(): Promise<ResolvedPostParams[]> {
|
|
34
45
|
// Use a new Supabase client instance that doesn't rely on cookies
|
|
@@ -58,49 +69,49 @@ export async function generateStaticParams(): Promise<ResolvedPostParams[]> {
|
|
|
58
69
|
export async function generateMetadata(
|
|
59
70
|
{ params: paramsPromise }: PostPageProps,
|
|
60
71
|
): Promise<Metadata> {
|
|
61
|
-
const params = await paramsPromise; // Await the promise to get the actual params
|
|
62
|
-
const postData = await getPostDataBySlug(params.slug);
|
|
63
|
-
|
|
64
|
-
if (!postData) {
|
|
65
|
-
return {
|
|
66
|
-
title: "
|
|
67
|
-
description: "The
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
|
|
72
|
-
const supabase = getSsgSupabaseClient();
|
|
73
|
-
const { data: languages } = await supabase.from('languages').select('id, code');
|
|
74
|
-
const { data: postTranslations } = await supabase
|
|
75
|
-
.from('posts')
|
|
76
|
-
.select('language_id, slug')
|
|
77
|
-
.eq('translation_group_id', postData.translation_group_id)
|
|
78
|
-
.eq('status', 'published')
|
|
72
|
+
const params = await paramsPromise; // Await the promise to get the actual params
|
|
73
|
+
const postData = await getPostDataBySlug(params.slug);
|
|
74
|
+
|
|
75
|
+
if (!postData) {
|
|
76
|
+
return {
|
|
77
|
+
title: "Article Not Found",
|
|
78
|
+
description: "The article you are looking for does not exist or is not yet published.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
|
|
83
|
+
const supabase = getSsgSupabaseClient();
|
|
84
|
+
const { data: languages } = await supabase.from('languages').select('id, code');
|
|
85
|
+
const { data: postTranslations } = await supabase
|
|
86
|
+
.from('posts')
|
|
87
|
+
.select('language_id, slug')
|
|
88
|
+
.eq('translation_group_id', postData.translation_group_id)
|
|
89
|
+
.eq('status', 'published')
|
|
79
90
|
.or(`published_at.is.null,published_at.lte.${new Date().toISOString()}`);
|
|
80
91
|
|
|
81
|
-
const alternates: { [key: string]: string } = {};
|
|
82
|
-
if (languages && postTranslations) {
|
|
83
|
-
postTranslations.forEach(pt => {
|
|
84
|
-
const langInfo = languages.find(l => l.id === pt.language_id);
|
|
85
|
-
if (langInfo) {
|
|
86
|
-
alternates[langInfo.code] = `${siteUrl}/
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
title: postData.meta_title || postData.title,
|
|
93
|
-
description: postData.meta_description || postData.excerpt || "",
|
|
94
|
-
openGraph: {
|
|
92
|
+
const alternates: { [key: string]: string } = {};
|
|
93
|
+
if (languages && postTranslations) {
|
|
94
|
+
postTranslations.forEach(pt => {
|
|
95
|
+
const langInfo = languages.find(l => l.id === pt.language_id);
|
|
96
|
+
if (langInfo) {
|
|
97
|
+
alternates[langInfo.code] = `${siteUrl}/article/${pt.slug}`;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
title: postData.meta_title || postData.title,
|
|
104
|
+
description: postData.meta_description || postData.excerpt || "",
|
|
105
|
+
openGraph: {
|
|
95
106
|
title: postData.meta_title || postData.title,
|
|
96
|
-
description: postData.meta_description || postData.excerpt || "",
|
|
97
|
-
type: 'article',
|
|
98
|
-
publishedTime: postData.published_at || postData.created_at,
|
|
99
|
-
url: `${siteUrl}/
|
|
100
|
-
images: postData.feature_image_url
|
|
101
|
-
? [
|
|
102
|
-
{
|
|
103
|
-
url: postData.feature_image_url,
|
|
107
|
+
description: postData.meta_description || postData.excerpt || "",
|
|
108
|
+
type: 'article',
|
|
109
|
+
publishedTime: postData.published_at || postData.created_at,
|
|
110
|
+
url: `${siteUrl}/article/${params.slug}`,
|
|
111
|
+
images: postData.feature_image_url
|
|
112
|
+
? [
|
|
113
|
+
{
|
|
114
|
+
url: postData.feature_image_url,
|
|
104
115
|
// You can optionally add width, height, and alt here if known
|
|
105
116
|
// width: 1200, // Example
|
|
106
117
|
// height: 630, // Example
|
|
@@ -108,44 +119,43 @@ export async function generateMetadata(
|
|
|
108
119
|
},
|
|
109
120
|
]
|
|
110
121
|
: undefined, // Or an empty array if you prefer: [],
|
|
111
|
-
},
|
|
112
|
-
alternates: {
|
|
113
|
-
canonical: `${siteUrl}/
|
|
114
|
-
languages: Object.keys(alternates).length > 0 ? alternates : undefined,
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
122
|
+
},
|
|
123
|
+
alternates: {
|
|
124
|
+
canonical: `${siteUrl}/article/${params.slug}`,
|
|
125
|
+
languages: Object.keys(alternates).length > 0 ? alternates : undefined,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
118
129
|
|
|
119
130
|
// Server Component: Fetches data for the specific slug and passes to Client Component
|
|
120
131
|
export default async function DynamicPostPage({ params: paramsPromise }: PostPageProps) { // Destructure the promise
|
|
121
132
|
const params = await paramsPromise; // Await the promise
|
|
122
133
|
const initialPostData = await getPostDataBySlug(params.slug);
|
|
123
134
|
|
|
124
|
-
if (!initialPostData) {
|
|
125
|
-
notFound();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const { data: translations } = await supabase
|
|
132
|
-
.from("posts")
|
|
133
|
-
.select("slug, languages!inner(code)")
|
|
134
|
-
.eq("translation_group_id", initialPostData.translation_group_id)
|
|
135
|
-
.eq("status", "published")
|
|
136
|
-
.or(`published_at.is.null,published_at.lte.${new Date().toISOString()}`);
|
|
137
|
-
|
|
138
|
-
if (translations) {
|
|
139
|
-
translations.forEach((translation: PostTranslation) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
135
|
+
if (!initialPostData) {
|
|
136
|
+
notFound();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const supabase = getSsgSupabaseClient(); // Use SSG client
|
|
140
|
+
const translatedSlugs: { [key: string]: string } = {};
|
|
141
|
+
if (initialPostData.translation_group_id) {
|
|
142
|
+
const { data: translations } = await supabase
|
|
143
|
+
.from("posts")
|
|
144
|
+
.select("slug, languages!inner(code)")
|
|
145
|
+
.eq("translation_group_id", initialPostData.translation_group_id)
|
|
146
|
+
.eq("status", "published")
|
|
147
|
+
.or(`published_at.is.null,published_at.lte.${new Date().toISOString()}`);
|
|
148
|
+
|
|
149
|
+
if (translations) {
|
|
150
|
+
translations.forEach((translation: PostTranslation) => {
|
|
151
|
+
const code = resolveLanguageCode(translation.languages);
|
|
152
|
+
if (code && translation.slug) translatedSlugs[code] = translation.slug;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let lcpImageUrl: string | null = null;
|
|
158
|
+
const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
149
159
|
|
|
150
160
|
if (initialPostData && initialPostData.blocks && r2BaseUrl) {
|
|
151
161
|
const heroBlock = initialPostData.blocks.find(block => block.block_type === 'hero');
|
|
@@ -162,16 +172,16 @@ export default async function DynamicPostPage({ params: paramsPromise }: PostPag
|
|
|
162
172
|
}
|
|
163
173
|
}
|
|
164
174
|
|
|
165
|
-
const postBlocks = initialPostData ? <BlockRenderer blocks={initialPostData.blocks} languageId={initialPostData.language_id} /> : null;
|
|
166
|
-
|
|
167
|
-
return (
|
|
168
|
-
<>
|
|
169
|
-
{lcpImageUrl && (
|
|
170
|
-
<link rel="preload" as="image" href={lcpImageUrl} />
|
|
171
|
-
)}
|
|
172
|
-
<PostClientContent initialPostData={initialPostData} currentSlug={params.slug} translatedSlugs={translatedSlugs}>
|
|
173
|
-
{postBlocks}
|
|
174
|
-
</PostClientContent>
|
|
175
|
-
</>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
175
|
+
const postBlocks = initialPostData ? <BlockRenderer blocks={initialPostData.blocks} languageId={initialPostData.language_id} /> : null;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
{lcpImageUrl && (
|
|
180
|
+
<link rel="preload" as="image" href={lcpImageUrl} />
|
|
181
|
+
)}
|
|
182
|
+
<PostClientContent initialPostData={initialPostData} currentSlug={params.slug} translatedSlugs={translatedSlugs}>
|
|
183
|
+
{postBlocks}
|
|
184
|
+
</PostClientContent>
|
|
185
|
+
</>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// app/
|
|
1
|
+
// app/article/[slug]/page.utils.ts
|
|
2
2
|
import { createClient } from "@nextblock-cms/db/server";
|
|
3
3
|
import type { Database } from "@nextblock-cms/db";
|
|
4
4
|
|
|
@@ -24,7 +24,14 @@ interface SectionOrHeroBlockContent {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
// Includes logic to fetch object_key for image blocks.
|
|
27
|
-
|
|
27
|
+
const buildMediaUrl = (objectKey?: string | null) => {
|
|
28
|
+
if (!objectKey) return null;
|
|
29
|
+
if (objectKey.startsWith('/')) return objectKey;
|
|
30
|
+
const base = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
31
|
+
return base ? `${base}/${objectKey}` : objectKey;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function getPostDataBySlug(slug: string): Promise<(PostType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; feature_image_url?: string | null; feature_image_blur_data_url?: string | null; }) | null> {
|
|
28
35
|
const supabase = createClient();
|
|
29
36
|
|
|
30
37
|
const { data: postData, error: postError } = await supabase
|
|
@@ -130,7 +137,7 @@ export async function getPostDataBySlug(slug: string): Promise<(PostType & { blo
|
|
|
130
137
|
language_code: langInfo.code,
|
|
131
138
|
language_id: langInfo.id,
|
|
132
139
|
translation_group_id: postData.translation_group_id,
|
|
133
|
-
feature_image_url: postData.media?.object_key
|
|
140
|
+
feature_image_url: buildMediaUrl(postData.media?.object_key),
|
|
134
141
|
feature_image_blur_data_url: postData.media?.blur_data_url,
|
|
135
142
|
} as (PostType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; feature_image_url?: string | null; feature_image_blur_data_url?: string | null; });
|
|
136
143
|
}
|
|
@@ -5,14 +5,15 @@ import React, { type ReactNode, useEffect } from "react"
|
|
|
5
5
|
import { useAuth } from "@/context/AuthContext"
|
|
6
6
|
import { useRouter, usePathname } from "next/navigation" // Import usePathname
|
|
7
7
|
import Link from "next/link"
|
|
8
|
-
import {
|
|
9
|
-
LayoutDashboard, FileText, PenTool, Users, Settings, ChevronRight, LogOut, Menu, ListTree, Image as ImageIconLucide, X, Languages as LanguagesIconLucide, MessageSquare,
|
|
10
|
-
Copyright as CopyrightIcon,
|
|
11
|
-
} from "lucide-react"
|
|
12
|
-
import { Button } from "@nextblock-cms/ui"
|
|
13
|
-
import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui"
|
|
14
|
-
import { cn } from "@nextblock-cms/utils"
|
|
15
|
-
import { signOutAction } from "@/app/actions";
|
|
8
|
+
import {
|
|
9
|
+
LayoutDashboard, FileText, PenTool, Users, Settings, ChevronRight, LogOut, Menu, ListTree, Image as ImageIconLucide, X, Languages as LanguagesIconLucide, MessageSquare,
|
|
10
|
+
Copyright as CopyrightIcon,
|
|
11
|
+
} from "lucide-react"
|
|
12
|
+
import { Button } from "@nextblock-cms/ui"
|
|
13
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui"
|
|
14
|
+
import { cn } from "@nextblock-cms/utils"
|
|
15
|
+
import { signOutAction } from "@/app/actions";
|
|
16
|
+
import Image from "next/image";
|
|
16
17
|
|
|
17
18
|
const LoadingSpinner = () => (
|
|
18
19
|
<div className="flex justify-center items-center h-full w-full py-20">
|
|
@@ -210,16 +211,21 @@ export default function CmsClientLayout({ children }: { children: ReactNode }) {
|
|
|
210
211
|
)}
|
|
211
212
|
>
|
|
212
213
|
<div className="flex flex-col h-full">
|
|
213
|
-
<div className="p-4 border-b dark:border-slate-700/60 h-16 flex items-center shrink-0">
|
|
214
|
-
<Link href="/cms/dashboard" className="flex items-center gap-2 px-2">
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
214
|
+
<div className="p-4 border-b dark:border-slate-700/60 h-16 flex items-center shrink-0">
|
|
215
|
+
<Link href="/cms/dashboard" className="flex items-center gap-2 px-2">
|
|
216
|
+
<Image
|
|
217
|
+
src="/images/nextblock-logo-small.webp"
|
|
218
|
+
alt="Nextblock logo"
|
|
219
|
+
width={32}
|
|
220
|
+
height={32}
|
|
221
|
+
className="h-8 w-auto"
|
|
222
|
+
priority
|
|
223
|
+
/>
|
|
224
|
+
<h2 className="text-xl font-bold text-foreground">
|
|
225
|
+
Nextblock CMS
|
|
226
|
+
</h2>
|
|
227
|
+
</Link>
|
|
228
|
+
</div>
|
|
223
229
|
|
|
224
230
|
<nav className="px-3 py-4 flex-1 overflow-y-auto">
|
|
225
231
|
<ul className="space-y-1.5">
|
|
@@ -318,4 +324,4 @@ export default function CmsClientLayout({ children }: { children: ReactNode }) {
|
|
|
318
324
|
)}
|
|
319
325
|
</div>
|
|
320
326
|
)
|
|
321
|
-
}
|
|
327
|
+
}
|
|
@@ -418,7 +418,7 @@ export async function copyBlocksFromLanguage(
|
|
|
418
418
|
console.warn("Could not fetch target post slug for revalidation:", postError);
|
|
419
419
|
} else {
|
|
420
420
|
targetSlug = postData.slug;
|
|
421
|
-
if (targetSlug) revalidatePath(`/
|
|
421
|
+
if (targetSlug) revalidatePath(`/article/${targetSlug}`);
|
|
422
422
|
}
|
|
423
423
|
revalidatePath(`/cms/posts/${parentId}/edit`); // Revalidate edit page
|
|
424
424
|
}
|
|
@@ -113,7 +113,7 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
const updatePostWithId = updatePost.bind(null, postId);
|
|
116
|
-
const publicPostUrl = `/
|
|
116
|
+
const publicPostUrl = `/article/${postWithBlocks.slug}`;
|
|
117
117
|
|
|
118
118
|
return (
|
|
119
119
|
<UploadFolderProvider defaultFolder={`posts/${postWithBlocks.slug}/`}>
|
|
@@ -10,15 +10,15 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
10
10
|
type PageStatus = Database['public']['Enums']['page_status'];
|
|
11
11
|
import { encodedRedirect } from "@nextblock-cms/utils/server"; // Ensure this is correctly imported
|
|
12
12
|
import { getFullPostContent } from "../revisions/utils";
|
|
13
|
-
import { createPostRevision } from "../revisions/service";
|
|
14
|
-
|
|
15
|
-
// --- createPost and updatePost functions to be updated similarly for error returns ---
|
|
16
|
-
|
|
17
|
-
export async function createPost(formData: FormData) {
|
|
18
|
-
const supabase = createClient();
|
|
19
|
-
const { data: { user } } = await supabase.auth.getUser();
|
|
20
|
-
if (!user) {
|
|
21
|
-
return encodedRedirect("error", "/cms/posts/new", "User not authenticated.");
|
|
13
|
+
import { createPostRevision } from "../revisions/service";
|
|
14
|
+
|
|
15
|
+
// --- createPost and updatePost functions to be updated similarly for error returns ---
|
|
16
|
+
|
|
17
|
+
export async function createPost(formData: FormData) {
|
|
18
|
+
const supabase = createClient();
|
|
19
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
20
|
+
if (!user) {
|
|
21
|
+
return encodedRedirect("error", "/cms/posts/new", "User not authenticated.");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const featureImageIdStr_create = formData.get("feature_image_id") as string;
|
|
@@ -111,15 +111,16 @@ export async function createPost(formData: FormData) {
|
|
|
111
111
|
successMessage += ` ${placeholderCreations} placeholder version(s) also created (draft status, please edit their slugs and content).`;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
revalidatePath("/cms/posts");
|
|
117
|
-
if (newPost?.slug) revalidatePath(`/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
revalidatePath("/cms/posts");
|
|
117
|
+
if (newPost?.slug) revalidatePath(`/article/${newPost.slug}`);
|
|
118
|
+
revalidatePath("/articles");
|
|
119
|
+
|
|
120
|
+
if (newPost?.id) {
|
|
121
|
+
redirect(`/cms/posts/${newPost.id}/edit?success=${encodeURIComponent(successMessage)}`);
|
|
122
|
+
} else {
|
|
123
|
+
redirect(`/cms/posts?success=${encodeURIComponent(successMessage)}`);
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -206,17 +207,18 @@ export async function updatePost(postId: number, formData: FormData) {
|
|
|
206
207
|
if (newContent) {
|
|
207
208
|
await createPostRevision(postId, user.id, previousContent, newContent);
|
|
208
209
|
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
revalidatePath("/cms/posts");
|
|
212
|
-
if (existingPost.slug) revalidatePath(`/
|
|
213
|
-
if (rawFormData.slug && rawFormData.slug !== existingPost.slug) {
|
|
214
|
-
revalidatePath(`/
|
|
215
|
-
}
|
|
216
|
-
revalidatePath(
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
revalidatePath("/cms/posts");
|
|
213
|
+
if (existingPost.slug) revalidatePath(`/article/${existingPost.slug}`);
|
|
214
|
+
if (rawFormData.slug && rawFormData.slug !== existingPost.slug) {
|
|
215
|
+
revalidatePath(`/article/${rawFormData.slug}`);
|
|
216
|
+
}
|
|
217
|
+
revalidatePath("/articles");
|
|
218
|
+
revalidatePath(postEditPath);
|
|
219
|
+
redirect(`${postEditPath}?success=Post updated successfully`);
|
|
220
|
+
}
|
|
221
|
+
|
|
220
222
|
|
|
221
223
|
export async function deletePost(postId: number) {
|
|
222
224
|
const supabase = createClient();
|
|
@@ -246,11 +248,11 @@ export async function deletePost(postId: number) {
|
|
|
246
248
|
|
|
247
249
|
// 3. Delete All Associated Navigation Links
|
|
248
250
|
if (relatedPosts && relatedPosts.length > 0) {
|
|
249
|
-
const urlsToDelete = relatedPosts.map(p => `/
|
|
250
|
-
const { error: navError } = await supabase
|
|
251
|
-
.from("navigation_items")
|
|
252
|
-
.delete()
|
|
253
|
-
.in("url", urlsToDelete);
|
|
251
|
+
const urlsToDelete = relatedPosts.map(p => `/article/${p.slug}`);
|
|
252
|
+
const { error: navError } = await supabase
|
|
253
|
+
.from("navigation_items")
|
|
254
|
+
.delete()
|
|
255
|
+
.in("url", urlsToDelete);
|
|
254
256
|
|
|
255
257
|
if (navError) {
|
|
256
258
|
console.error("Error deleting navigation links:", navError);
|
|
@@ -268,16 +270,17 @@ export async function deletePost(postId: number) {
|
|
|
268
270
|
return encodedRedirect("error", "/cms/posts", `Failed to delete posts: ${deletePostsError.message}`);
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
// Revalidate paths
|
|
272
|
-
revalidatePath("/cms/posts");
|
|
273
|
-
revalidatePath("/cms/navigation");
|
|
274
|
-
if (relatedPosts) {
|
|
275
|
-
relatedPosts.forEach(p => {
|
|
276
|
-
if (p.slug) {
|
|
277
|
-
revalidatePath(`/
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
}
|
|
273
|
+
// Revalidate paths
|
|
274
|
+
revalidatePath("/cms/posts");
|
|
275
|
+
revalidatePath("/cms/navigation");
|
|
276
|
+
if (relatedPosts) {
|
|
277
|
+
relatedPosts.forEach(p => {
|
|
278
|
+
if (p.slug) {
|
|
279
|
+
revalidatePath(`/article/${p.slug}`);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
revalidatePath("/articles");
|
|
281
284
|
|
|
282
285
|
// 5. Update Redirect Message
|
|
283
286
|
redirect(`/cms/posts?success=${encodeURIComponent("Post and all its translations were deleted successfully.")}`);
|
|
@@ -158,7 +158,7 @@ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
|
|
|
158
158
|
</Badge>
|
|
159
159
|
</TableCell>
|
|
160
160
|
<TableCell><Badge variant="outline" className="dark:border-slate-600">{languageCode}</Badge></TableCell>
|
|
161
|
-
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">/
|
|
161
|
+
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">/article/{post.slug}</TableCell>
|
|
162
162
|
<TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
|
|
163
163
|
{post.published_at ? new Date(post.published_at).toLocaleDateString() : "Not yet"}
|
|
164
164
|
</TableCell>
|
|
@@ -189,4 +189,4 @@ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
|
|
|
189
189
|
)}
|
|
190
190
|
</div>
|
|
191
191
|
);
|
|
192
|
-
}
|
|
192
|
+
}
|
|
@@ -121,27 +121,28 @@ export async function createLanguage(formData: FormData) {
|
|
|
121
121
|
.select()
|
|
122
122
|
.single();
|
|
123
123
|
|
|
124
|
-
if (error) {
|
|
125
|
-
console.error("Error creating language:", error);
|
|
126
|
-
if (error.code === '23505') { // Unique violation
|
|
127
|
-
if (error.message.includes('languages_code_key')) {
|
|
128
|
-
return { error: `Language code '${languageData.code}' already exists.` };
|
|
124
|
+
if (error) {
|
|
125
|
+
console.error("Error creating language:", error);
|
|
126
|
+
if (error.code === '23505') { // Unique violation
|
|
127
|
+
if (error.message.includes('languages_code_key')) {
|
|
128
|
+
return { error: `Language code '${languageData.code}' already exists.` };
|
|
129
129
|
}
|
|
130
130
|
if (error.message.includes('ensure_single_default_language_idx')) {
|
|
131
131
|
return { error: `Cannot set this language as default. Another language is already default, or an error occurred unsetting it.` };
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
return { error: `Failed to create language: ${error.message}` };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
revalidatePath("/cms/settings/languages");
|
|
138
|
-
revalidatePath("/"); // Revalidate home page as language switcher might change
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
revalidatePath("/cms/settings/languages");
|
|
138
|
+
revalidatePath("/"); // Revalidate home page as language switcher might change
|
|
139
|
+
revalidatePath("/cms/settings/extra-translations");
|
|
140
|
+
if (data?.id) {
|
|
141
|
+
redirect(`/cms/settings/extra-translations?language=${encodeURIComponent(data.code)}&success=${encodeURIComponent("Language created successfully. Please fill the extra translations.")}`);
|
|
142
|
+
} else {
|
|
143
|
+
redirect(`/cms/settings/extra-translations?success=${encodeURIComponent("Language created successfully. Please fill the extra translations.")}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
145
146
|
|
|
146
147
|
export async function updateLanguage(languageId: number, formData: FormData) {
|
|
147
148
|
const supabase = createClient();
|
|
@@ -49,7 +49,7 @@ async function loadLayoutData() {
|
|
|
49
49
|
] = await Promise.all([
|
|
50
50
|
supabase.auth.getUser(),
|
|
51
51
|
getActiveLanguagesServerSide().catch(() => []),
|
|
52
|
-
getCopyrightSettings().catch(() => ({ en: '
|
|
52
|
+
getCopyrightSettings().catch(() => ({ en: '© {year} Nextblock CMS. All rights reserved.' })),
|
|
53
53
|
getTranslations().catch(() => []),
|
|
54
54
|
]);
|
|
55
55
|
|
|
@@ -65,8 +65,8 @@ async function loadLayoutData() {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const copyrightSettings = copyrightSettingsResult as Record<string, string>;
|
|
68
|
-
const fallbackTemplate =
|
|
69
|
-
copyrightSettings['en'] ?? '
|
|
68
|
+
const fallbackTemplate =
|
|
69
|
+
copyrightSettings['en'] ?? '© {year} Nextblock CMS. All rights reserved.';
|
|
70
70
|
const templateForLocale =
|
|
71
71
|
copyrightSettings[serverDeterminedLocale] ?? fallbackTemplate;
|
|
72
72
|
const copyrightText = templateForLocale.replace('{year}', new Date().getFullYear().toString());
|
|
@@ -82,7 +82,7 @@ async function loadLayoutData() {
|
|
|
82
82
|
|
|
83
83
|
const role = profile?.role ?? null;
|
|
84
84
|
const canAccessCms = role === 'ADMIN' || role === 'WRITER';
|
|
85
|
-
const siteTitle = logo?.site_title ?? '
|
|
85
|
+
const siteTitle = logo?.site_title ?? 'Nextblock';
|
|
86
86
|
|
|
87
87
|
return {
|
|
88
88
|
user,
|
|
@@ -102,11 +102,11 @@ async function loadLayoutData() {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export const metadata: Metadata = {
|
|
106
|
-
metadataBase: new URL(defaultUrl),
|
|
107
|
-
title: '
|
|
108
|
-
description: '
|
|
109
|
-
};
|
|
105
|
+
export const metadata: Metadata = {
|
|
106
|
+
metadataBase: new URL(defaultUrl),
|
|
107
|
+
title: 'Nextblock CMS',
|
|
108
|
+
description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
|
|
109
|
+
};
|
|
110
110
|
|
|
111
111
|
export default async function RootLayout({
|
|
112
112
|
children,
|