create-nextblock 0.1.0 → 0.2.0

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.
Files changed (42) hide show
  1. package/bin/create-nextblock.js +1193 -920
  2. package/package.json +6 -2
  3. package/scripts/sync-template.js +279 -276
  4. package/templates/nextblock-template/.env.example +1 -14
  5. package/templates/nextblock-template/README.md +1 -1
  6. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -40
  7. package/templates/nextblock-template/app/[slug]/page.tsx +45 -10
  8. package/templates/nextblock-template/app/[slug]/page.utils.ts +92 -45
  9. package/templates/nextblock-template/app/api/revalidate/route.ts +15 -15
  10. package/templates/nextblock-template/app/{blog → article}/[slug]/PostClientContent.tsx +45 -43
  11. package/templates/nextblock-template/app/{blog → article}/[slug]/page.tsx +108 -98
  12. package/templates/nextblock-template/app/{blog → article}/[slug]/page.utils.ts +10 -3
  13. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +25 -19
  14. package/templates/nextblock-template/app/cms/blocks/actions.ts +1 -1
  15. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +1 -1
  16. package/templates/nextblock-template/app/cms/posts/actions.ts +47 -44
  17. package/templates/nextblock-template/app/cms/posts/page.tsx +2 -2
  18. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -15
  19. package/templates/nextblock-template/app/layout.tsx +9 -9
  20. package/templates/nextblock-template/app/lib/sitemap-utils.ts +52 -52
  21. package/templates/nextblock-template/app/sitemap.xml/route.ts +2 -2
  22. package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -16
  23. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -7
  24. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +25 -26
  25. package/templates/nextblock-template/package.json +1 -1
  26. package/templates/nextblock-template/proxy.ts +4 -4
  27. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  28. package/templates/nextblock-template/public/images/developer.webp +0 -0
  29. package/templates/nextblock-template/public/images/nextblock-logo-small.webp +0 -0
  30. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  31. package/templates/nextblock-template/public/images/programmer-upscaled.webp +0 -0
  32. package/templates/nextblock-template/scripts/backup.js +142 -47
  33. package/templates/nextblock-template/scripts/restore-working.js +102 -0
  34. package/templates/nextblock-template/scripts/restore.js +434 -0
  35. package/templates/nextblock-template/app/blog/page.tsx +0 -77
  36. package/templates/nextblock-template/backup/backup_2025-06-19.sql +0 -8057
  37. package/templates/nextblock-template/backup/backup_2025-06-20.sql +0 -8159
  38. package/templates/nextblock-template/backup/backup_2025-07-08.sql +0 -8411
  39. package/templates/nextblock-template/backup/backup_2025-07-09.sql +0 -8442
  40. package/templates/nextblock-template/backup/backup_2025-07-10.sql +0 -8442
  41. package/templates/nextblock-template/backup/backup_2025-10-01.sql +0 -8803
  42. package/templates/nextblock-template/backup/backup_2025-10-02.sql +0 -9749
@@ -1,16 +1,16 @@
1
- // app/blog/[slug]/page.tsx
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: "Post Not Found",
67
- description: "The post you are looking for does not exist or is not yet published.",
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}/blog/${pt.slug}`;
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}/blog/${params.slug}`,
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}/blog/${params.slug}`,
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 translatedSlugs: { [key: string]: string } = {};
129
- if (initialPostData.translation_group_id) {
130
- const supabase = getSsgSupabaseClient(); // Use SSG client
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
- if (translation.languages && translation.languages.length > 0 && typeof translation.languages[0].code === 'string' && translation.slug) {
141
- translatedSlugs[translation.languages[0].code] = translation.slug;
142
- }
143
- });
144
- }
145
- }
146
-
147
- let lcpImageUrl: string | null = null;
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/blog/[slug]/page.utils.ts
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
- 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> {
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 ? `${process.env.NEXT_PUBLIC_R2_BASE_URL}/${postData.media.object_key}` : null,
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
- <div className="h-8 w-8 rounded-md bg-gradient-to-br from-primary to-primary/80 flex items-center justify-center text-white font-bold">
216
- NRH
217
- </div>
218
- <h2 className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/70 dark:from-primary dark:to-primary/80">
219
- CMS
220
- </h2>
221
- </Link>
222
- </div>
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(`/blog/${targetSlug}`);
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 = `/blog/${postWithBlocks.slug}`;
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(`/blog/${newPost.slug}`);
118
-
119
- if (newPost?.id) {
120
- redirect(`/cms/posts/${newPost.id}/edit?success=${encodeURIComponent(successMessage)}`);
121
- } else {
122
- redirect(`/cms/posts?success=${encodeURIComponent(successMessage)}`);
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(`/blog/${existingPost.slug}`);
213
- if (rawFormData.slug && rawFormData.slug !== existingPost.slug) {
214
- revalidatePath(`/blog/${rawFormData.slug}`);
215
- }
216
- revalidatePath(postEditPath);
217
- redirect(`${postEditPath}?success=Post updated successfully`);
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 => `/blog/${p.slug}`);
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(`/blog/${p.slug}`);
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">/blog/{post.slug}</TableCell>
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
- if (data?.id) {
140
- redirect(`/cms/settings/languages/${data.id}/edit?success=Language created successfully`);
141
- } else {
142
- redirect(`/cms/settings/languages?success=Language created successfully`);
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: 'Ac {year} My Ultra-Fast CMS. All rights reserved.' })),
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'] ?? 'Ac {year} My Ultra-Fast CMS. All rights reserved.';
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 ?? 'NRH';
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: 'My Ultra-Fast CMS',
108
- description: 'A block-based TypeScript CMS with Next.js and Supabase',
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,