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,18 +1,19 @@
|
|
|
1
1
|
// app/[slug]/PageClientContent.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
5
|
-
import { useRouter } from 'next/navigation'; // For navigation on lang switch
|
|
6
|
-
import type { Database } from "@nextblock-cms/db";
|
|
7
|
-
import { useLanguage } from '@/context/LanguageContext';
|
|
8
|
-
import { useCurrentContent } from '@/context/CurrentContentContext';
|
|
9
|
-
import Link from 'next/link';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
5
|
+
import { useRouter } from 'next/navigation'; // For navigation on lang switch
|
|
6
|
+
import type { Database } from "@nextblock-cms/db";
|
|
7
|
+
import { useLanguage } from '@/context/LanguageContext';
|
|
8
|
+
import { useCurrentContent } from '@/context/CurrentContentContext';
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
import { createClient } from '@nextblock-cms/db';
|
|
11
|
+
|
|
12
|
+
type PageType = Database['public']['Tables']['pages']['Row'];
|
|
13
|
+
type BlockType = Database['public']['Tables']['blocks']['Row'];
|
|
14
|
+
|
|
15
|
+
interface PageClientContentProps {
|
|
16
|
+
initialPageData: (PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; }) | null;
|
|
16
17
|
currentSlug: string; // The slug of the currently viewed page
|
|
17
18
|
children: React.ReactNode;
|
|
18
19
|
translatedSlugs?: { [key: string]: string };
|
|
@@ -42,36 +43,62 @@ interface PageClientContentProps {
|
|
|
42
43
|
// }
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
export default function PageClientContent({ initialPageData, currentSlug, children, translatedSlugs }: PageClientContentProps) {
|
|
46
|
-
const { currentLocale, isLoadingLanguages } = useLanguage();
|
|
47
|
-
const { currentContent, setCurrentContent } = useCurrentContent();
|
|
48
|
-
const router = useRouter();
|
|
49
|
-
// currentPageData is the data for the slug currently in the URL.
|
|
50
|
-
// It's initially set by the server for the slug it resolved.
|
|
51
|
-
const [currentPageData] = useState(initialPageData);
|
|
52
|
-
const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false);
|
|
46
|
+
export default function PageClientContent({ initialPageData, currentSlug, children, translatedSlugs }: PageClientContentProps) {
|
|
47
|
+
const { currentLocale, isLoadingLanguages } = useLanguage();
|
|
48
|
+
const { currentContent, setCurrentContent } = useCurrentContent();
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
// currentPageData is the data for the slug currently in the URL.
|
|
51
|
+
// It's initially set by the server for the slug it resolved.
|
|
52
|
+
const [currentPageData, setCurrentPageData] = useState(initialPageData);
|
|
53
|
+
const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false);
|
|
54
|
+
const supabase = useMemo(() => createClient(), []);
|
|
53
55
|
|
|
54
56
|
// Memoize pageId and pageSlug
|
|
55
|
-
const pageId = useMemo(() => currentPageData?.id, [currentPageData?.id]);
|
|
56
|
-
const pageSlug = useMemo(() => currentPageData?.slug, [currentPageData?.
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (currentLocale && currentPageData && currentPageData.language_code !== currentLocale && translatedSlugs) {
|
|
60
|
-
// Current page's language doesn't match context, try to navigate to translated version
|
|
61
|
-
setIsLoadingTargetLang(true);
|
|
62
|
-
const targetSlug = translatedSlugs[currentLocale];
|
|
63
|
-
|
|
64
|
-
if (targetSlug && targetSlug !== currentSlug) {
|
|
65
|
-
router.push(`/${targetSlug}`); // Navigate to the translated slug's URL
|
|
66
|
-
} else if (targetSlug && targetSlug === currentSlug) {
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
const pageId = useMemo(() => currentPageData?.id, [currentPageData?.id]);
|
|
58
|
+
const pageSlug = useMemo(() => currentPageData?.slug, [currentPageData?.id, currentLocale]); // include locale so updates propagate
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (currentLocale && currentPageData && currentPageData.language_code !== currentLocale && translatedSlugs) {
|
|
62
|
+
// Current page's language doesn't match context, try to navigate to translated version
|
|
63
|
+
setIsLoadingTargetLang(true);
|
|
64
|
+
const targetSlug = translatedSlugs[currentLocale];
|
|
65
|
+
|
|
66
|
+
if (targetSlug && targetSlug !== currentSlug) {
|
|
67
|
+
router.push(`/${targetSlug}`); // Navigate to the translated slug's URL
|
|
68
|
+
} else if (targetSlug && targetSlug === currentSlug) {
|
|
69
|
+
// Same slug across languages - refetch the page in the target language and update content
|
|
70
|
+
(async () => {
|
|
71
|
+
const { data, error } = await supabase
|
|
72
|
+
.from("pages")
|
|
73
|
+
.select("*, languages!inner(code), blocks(*)")
|
|
74
|
+
.eq("slug", targetSlug)
|
|
75
|
+
.eq("languages.code", currentLocale)
|
|
76
|
+
.eq("status", "published")
|
|
77
|
+
.order('order', { foreignTable: 'blocks', ascending: true })
|
|
78
|
+
.maybeSingle();
|
|
79
|
+
|
|
80
|
+
if (!error && data) {
|
|
81
|
+
const langInfo = Array.isArray(data.languages) ? data.languages[0] : (data.languages as unknown as { code?: string });
|
|
82
|
+
setCurrentPageData({
|
|
83
|
+
...(data as PageType),
|
|
84
|
+
blocks: (data as any).blocks || [],
|
|
85
|
+
language_code: langInfo?.code || currentLocale,
|
|
86
|
+
language_id: data.language_id,
|
|
87
|
+
translation_group_id: data.translation_group_id || currentPageData.translation_group_id,
|
|
88
|
+
} as typeof currentPageData);
|
|
89
|
+
} else {
|
|
90
|
+
// fallback to refresh if fetch fails
|
|
91
|
+
router.refresh();
|
|
92
|
+
}
|
|
93
|
+
setIsLoadingTargetLang(false);
|
|
94
|
+
})();
|
|
95
|
+
} else {
|
|
96
|
+
console.warn(`No published translation found for group ${currentPageData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
|
|
97
|
+
// Optionally, provide feedback to the user that translation is not available
|
|
98
|
+
setIsLoadingTargetLang(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [currentLocale, currentPageData, currentSlug, router, initialPageData, translatedSlugs]); // Rerun if initialPageData changes (e.g. after revalidation)
|
|
75
102
|
|
|
76
103
|
// Update HTML lang attribute based on the *actually displayed* content's language
|
|
77
104
|
useEffect(() => {
|
|
@@ -4,12 +4,15 @@ import { getSsgSupabaseClient } from "@nextblock-cms/db/server";
|
|
|
4
4
|
import { notFound } from "next/navigation";
|
|
5
5
|
import type { Metadata } from 'next';
|
|
6
6
|
import PageClientContent from "./PageClientContent";
|
|
7
|
-
import { getPageDataBySlug } from "./page.utils";
|
|
8
|
-
import BlockRenderer from "../../components/BlockRenderer";
|
|
9
|
-
import type { HeroBlockContent } from '../../lib/blocks/blockRegistry';
|
|
7
|
+
import { getPageDataBySlug } from "./page.utils";
|
|
8
|
+
import BlockRenderer from "../../components/BlockRenderer";
|
|
9
|
+
import type { HeroBlockContent } from '../../lib/blocks/blockRegistry';
|
|
10
|
+
import { cookies, headers } from "next/headers";
|
|
10
11
|
|
|
11
|
-
export const dynamicParams = true;
|
|
12
|
-
export const revalidate =
|
|
12
|
+
export const dynamicParams = true;
|
|
13
|
+
export const revalidate = 360;
|
|
14
|
+
export const dynamic = 'force-dynamic'; // keeps per-request locale; paired with short revalidate
|
|
15
|
+
export const fetchCache = 'force-no-store';
|
|
13
16
|
|
|
14
17
|
interface ResolvedPageParams {
|
|
15
18
|
slug: string;
|
|
@@ -43,8 +46,24 @@ export async function generateStaticParams(): Promise<ResolvedPageParams[]> {
|
|
|
43
46
|
export async function generateMetadata(
|
|
44
47
|
{ params: paramsPromise }: PageProps,
|
|
45
48
|
): Promise<Metadata> {
|
|
46
|
-
const params = await paramsPromise;
|
|
47
|
-
|
|
49
|
+
const params = await paramsPromise;
|
|
50
|
+
let preferredLocale: string | undefined;
|
|
51
|
+
try {
|
|
52
|
+
const store = await cookies();
|
|
53
|
+
preferredLocale = store.get("NEXT_USER_LOCALE")?.value || store.get("NEXT_LOCALE")?.value;
|
|
54
|
+
} catch {
|
|
55
|
+
preferredLocale = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!preferredLocale) {
|
|
58
|
+
try {
|
|
59
|
+
const hdrs = await headers();
|
|
60
|
+
const al = hdrs.get("accept-language");
|
|
61
|
+
if (al) preferredLocale = al.split(",")[0]?.split("-")[0];
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore header lookup errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const pageData = await getPageDataBySlug(params.slug, preferredLocale);
|
|
48
67
|
|
|
49
68
|
if (!pageData) {
|
|
50
69
|
return { title: "Page Not Found" };
|
|
@@ -86,9 +105,25 @@ export async function generateMetadata(
|
|
|
86
105
|
};
|
|
87
106
|
}
|
|
88
107
|
|
|
89
|
-
export default async function DynamicPage({ params: paramsPromise }: PageProps) {
|
|
90
|
-
const params = await paramsPromise;
|
|
91
|
-
|
|
108
|
+
export default async function DynamicPage({ params: paramsPromise }: PageProps) {
|
|
109
|
+
const params = await paramsPromise;
|
|
110
|
+
let preferredLocale: string | undefined;
|
|
111
|
+
try {
|
|
112
|
+
const store = await cookies();
|
|
113
|
+
preferredLocale = store.get("NEXT_USER_LOCALE")?.value || store.get("NEXT_LOCALE")?.value;
|
|
114
|
+
} catch {
|
|
115
|
+
preferredLocale = undefined;
|
|
116
|
+
}
|
|
117
|
+
if (!preferredLocale) {
|
|
118
|
+
try {
|
|
119
|
+
const hdrs = await headers();
|
|
120
|
+
const al = hdrs.get("accept-language");
|
|
121
|
+
if (al) preferredLocale = al.split(",")[0]?.split("-")[0];
|
|
122
|
+
} catch {
|
|
123
|
+
// ignore header lookup errors
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const pageData = await getPageDataBySlug(params.slug, preferredLocale);
|
|
92
127
|
|
|
93
128
|
if (!pageData) {
|
|
94
129
|
notFound();
|
|
@@ -24,51 +24,98 @@ interface SectionOrHeroBlockContent {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Interface to represent a page object after the initial database query and selection
|
|
27
|
-
interface SelectedPageType extends PageType { // Assumes PageType includes fields like id, slug, status, language_id, translation_group_id
|
|
28
|
-
language_details: { id: number; code: string } | null; // From the join; kept nullable due to original code's caution
|
|
29
|
-
blocks: BlockType[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function getPageDataBySlug(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
id, slug, title, meta_title, meta_description, status, language_id, translation_group_id, author_id, created_at, updated_at,
|
|
40
|
-
language_details:languages!inner(id, code),
|
|
41
|
-
blocks (id, page_id, block_type, content, order)
|
|
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
|
-
|
|
27
|
+
interface SelectedPageType extends PageType { // Assumes PageType includes fields like id, slug, status, language_id, translation_group_id
|
|
28
|
+
language_details: { id: number; code: string } | null; // From the join; kept nullable due to original code's caution
|
|
29
|
+
blocks: BlockType[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getPageDataBySlug(
|
|
33
|
+
slug: string,
|
|
34
|
+
preferredLanguageCode?: string,
|
|
35
|
+
): Promise<(PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string | null; }) | null> {
|
|
36
|
+
const supabase = getSsgSupabaseClient();
|
|
37
|
+
|
|
38
|
+
const baseSelect = `
|
|
39
|
+
id, slug, title, meta_title, meta_description, status, language_id, translation_group_id, author_id, created_at, updated_at,
|
|
40
|
+
language_details:languages!inner(id, code),
|
|
41
|
+
blocks (id, page_id, block_type, content, order)
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const toSelected = (rows: any[] | null | undefined): SelectedPageType[] =>
|
|
45
|
+
(rows || []).map(page => ({
|
|
46
|
+
...page,
|
|
47
|
+
language_details: Array.isArray(page.language_details) ? page.language_details[0] : page.language_details,
|
|
48
|
+
})) as SelectedPageType[];
|
|
49
|
+
|
|
50
|
+
let candidatePages: SelectedPageType[] = [];
|
|
51
|
+
|
|
52
|
+
// First try to fetch the preferred language explicitly when provided
|
|
53
|
+
if (preferredLanguageCode) {
|
|
54
|
+
const { data: preferredData, error: preferredError } = await supabase
|
|
55
|
+
.from("pages")
|
|
56
|
+
.select(baseSelect)
|
|
57
|
+
.eq("slug", slug)
|
|
58
|
+
.eq("status", "published")
|
|
59
|
+
.eq("languages.code", preferredLanguageCode)
|
|
60
|
+
.order('order', { foreignTable: 'blocks', ascending: true })
|
|
61
|
+
.maybeSingle();
|
|
62
|
+
if (!preferredError && preferredData) {
|
|
63
|
+
candidatePages = toSelected([preferredData]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback: fetch all published pages with this slug
|
|
68
|
+
if (candidatePages.length === 0) {
|
|
69
|
+
const { data: candidatePagesData, error: pageError } = await supabase
|
|
70
|
+
.from("pages")
|
|
71
|
+
.select(baseSelect)
|
|
72
|
+
.eq("slug", slug)
|
|
73
|
+
.eq("status", "published")
|
|
74
|
+
.order('order', { foreignTable: 'blocks', ascending: true });
|
|
75
|
+
|
|
76
|
+
if (pageError) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
candidatePages = toSelected(candidatePagesData);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (candidatePages.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let selectedPage: SelectedPageType | null = null;
|
|
87
|
+
|
|
88
|
+
if (preferredLanguageCode) {
|
|
89
|
+
selectedPage = candidatePages.find(
|
|
90
|
+
p => p.language_details && p.language_details.code === preferredLanguageCode,
|
|
91
|
+
) || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!selectedPage && candidatePages.length === 1) {
|
|
95
|
+
selectedPage = candidatePages[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!selectedPage) {
|
|
99
|
+
// Prefer default language if available
|
|
100
|
+
const { data: defaultLang } = await supabase
|
|
101
|
+
.from('languages')
|
|
102
|
+
.select('id, code')
|
|
103
|
+
.eq('is_default', true)
|
|
104
|
+
.maybeSingle();
|
|
105
|
+
if (defaultLang) {
|
|
106
|
+
const match = candidatePages.find(p => p.language_details && p.language_details.id === defaultLang.id);
|
|
107
|
+
if (match) selectedPage = match;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!selectedPage) {
|
|
112
|
+
const enPage = candidatePages.find(p => p.language_details && p.language_details.code === 'en');
|
|
113
|
+
if (enPage) {
|
|
114
|
+
selectedPage = enPage;
|
|
115
|
+
} else {
|
|
116
|
+
selectedPage = candidatePages[0];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
72
119
|
|
|
73
120
|
if (!selectedPage) {
|
|
74
121
|
return null;
|
|
@@ -42,8 +42,8 @@ export async function POST(request: NextRequest) {
|
|
|
42
42
|
|
|
43
43
|
if (table === 'pages') {
|
|
44
44
|
pathToRevalidate = `/${relevantRecord.slug}`;
|
|
45
|
-
} else if (table === 'posts') {
|
|
46
|
-
pathToRevalidate = `/
|
|
45
|
+
} else if (table === 'posts') {
|
|
46
|
+
pathToRevalidate = `/article/${relevantRecord.slug}`;
|
|
47
47
|
} else {
|
|
48
48
|
console.log(`Revalidation not configured for table: ${table}`);
|
|
49
49
|
return NextResponse.json({ message: `Revalidation not configured for table: ${table}` }, { status: 200 }); // Acknowledge but don't process
|
|
@@ -59,19 +59,19 @@ export async function POST(request: NextRequest) {
|
|
|
59
59
|
await revalidatePath(normalizedPath, 'page');
|
|
60
60
|
console.log(`Successfully revalidated path: ${normalizedPath}`);
|
|
61
61
|
|
|
62
|
-
// Additionally, if it's
|
|
63
|
-
if (table === 'posts') {
|
|
64
|
-
// Assuming your main
|
|
65
|
-
// This path needs to be known and consistent.
|
|
66
|
-
// If your
|
|
67
|
-
// and you are NOT using [lang] in URL, then the path is just '/
|
|
68
|
-
// However, if your LanguageContext means /
|
|
69
|
-
// revalidating just '/
|
|
70
|
-
// Client-side fetches would still get latest for other languages.
|
|
71
|
-
// For now, let's revalidate a generic /
|
|
72
|
-
// await revalidatePath('/
|
|
73
|
-
// console.log("Also attempted to revalidate /
|
|
74
|
-
}
|
|
62
|
+
// Additionally, if it's an article, you might want to revalidate the main listing page.
|
|
63
|
+
if (table === 'posts') {
|
|
64
|
+
// Assuming your main articles listing page is at '/articles' or similar.
|
|
65
|
+
// This path needs to be known and consistent.
|
|
66
|
+
// If your listing is at the root of the language segment (e.g. /en/articles),
|
|
67
|
+
// and you are NOT using [lang] in URL, then the path is just '/articles'.
|
|
68
|
+
// However, if your LanguageContext means /articles shows different content per lang,
|
|
69
|
+
// revalidating just '/articles' will rebuild its default language version.
|
|
70
|
+
// Client-side fetches would still get latest for other languages.
|
|
71
|
+
// For now, let's revalidate a generic /articles path if it exists.
|
|
72
|
+
// await revalidatePath('/articles', 'page'); // Example: revalidate main listing
|
|
73
|
+
// console.log("Also attempted to revalidate /articles listing page.");
|
|
74
|
+
}
|
|
75
75
|
|
|
76
76
|
return NextResponse.json({ revalidated: true, revalidatedPath: normalizedPath, now: Date.now() });
|
|
77
77
|
} catch (err: unknown) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// app/
|
|
1
|
+
// app/article/[slug]/PostClientContent.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
5
5
|
import { useRouter } from 'next/navigation';
|
|
6
6
|
import Image from 'next/image';
|
|
7
7
|
import type { Database } from "@nextblock-cms/db";
|
|
@@ -17,22 +17,24 @@ export type ImageBlockContent = {
|
|
|
17
17
|
import { useCurrentContent } from '@/context/CurrentContentContext';
|
|
18
18
|
import Link from 'next/link';
|
|
19
19
|
|
|
20
|
-
interface PostClientContentProps {
|
|
21
|
-
initialPostData: (PostType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; feature_image_url?: string | null; }) | null;
|
|
22
|
-
currentSlug: string; // The slug of the currently viewed page/post
|
|
23
|
-
children: React.ReactNode;
|
|
24
|
-
translatedSlugs?: { [key: string]: string };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default function PostClientContent({ initialPostData, currentSlug, children, translatedSlugs }: PostClientContentProps) {
|
|
28
|
-
const { currentLocale, isLoadingLanguages } = useLanguage();
|
|
29
|
-
const { currentContent, setCurrentContent } = useCurrentContent();
|
|
30
|
-
const router = useRouter();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
interface PostClientContentProps {
|
|
21
|
+
initialPostData: (PostType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; feature_image_url?: string | null; }) | null;
|
|
22
|
+
currentSlug: string; // The slug of the currently viewed page/post
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
translatedSlugs?: { [key: string]: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function PostClientContent({ initialPostData, currentSlug, children, translatedSlugs }: PostClientContentProps) {
|
|
28
|
+
const { currentLocale, isLoadingLanguages } = useLanguage();
|
|
29
|
+
const { currentContent, setCurrentContent } = useCurrentContent();
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
|
|
32
|
+
const currentPrefix = "articles";
|
|
33
|
+
|
|
34
|
+
// currentPostData is always for the slug in the URL.
|
|
35
|
+
// It's initially set by the server. It only changes if the URL itself changes (which happens on language switch).
|
|
36
|
+
const [currentPostData, setCurrentPostData] = useState(initialPostData);
|
|
37
|
+
const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false); // For feedback during navigation
|
|
36
38
|
|
|
37
39
|
// Memoize postId and postSlug
|
|
38
40
|
const postId = useMemo(() => currentPostData?.id, [currentPostData?.id]);
|
|
@@ -40,20 +42,20 @@ export default function PostClientContent({ initialPostData, currentSlug, childr
|
|
|
40
42
|
|
|
41
43
|
// This effect handles navigation when the language context changes
|
|
42
44
|
useEffect(() => {
|
|
43
|
-
if (!isLoadingLanguages && currentLocale && initialPostData && initialPostData.language_code !== currentLocale && translatedSlugs) {
|
|
44
|
-
// The current page's language (from initialPostData.language_code)
|
|
45
|
-
// does not match the user's selected language (currentLocale).
|
|
46
|
-
// We need to find the slug for the currentLocale version of this post and navigate.
|
|
47
|
-
setIsLoadingTargetLang(true);
|
|
48
|
-
const targetSlug = translatedSlugs[currentLocale];
|
|
49
|
-
|
|
50
|
-
if (targetSlug && targetSlug !== currentSlug) {
|
|
51
|
-
router.push(`/
|
|
52
|
-
} else if (!targetSlug) {
|
|
53
|
-
console.warn(`No published translation found for post group ${initialPostData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
|
|
54
|
-
// Optionally, provide user feedback here (e.g., a toast message)
|
|
55
|
-
// For now, the user remains on the current page.
|
|
56
|
-
}
|
|
45
|
+
if (!isLoadingLanguages && currentLocale && initialPostData && initialPostData.language_code !== currentLocale && translatedSlugs) {
|
|
46
|
+
// The current page's language (from initialPostData.language_code)
|
|
47
|
+
// does not match the user's selected language (currentLocale).
|
|
48
|
+
// We need to find the slug for the currentLocale version of this post and navigate.
|
|
49
|
+
setIsLoadingTargetLang(true);
|
|
50
|
+
const targetSlug = translatedSlugs[currentLocale];
|
|
51
|
+
|
|
52
|
+
if (targetSlug && targetSlug !== currentSlug) {
|
|
53
|
+
router.push(`/article/${targetSlug}`); // Navigate to the translated slug's URL
|
|
54
|
+
} else if (!targetSlug) {
|
|
55
|
+
console.warn(`No published translation found for post group ${initialPostData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
|
|
56
|
+
// Optionally, provide user feedback here (e.g., a toast message)
|
|
57
|
+
// For now, the user remains on the current page.
|
|
58
|
+
}
|
|
57
59
|
// If targetSlug === currentSlug, we are already on the correct page for the selected language.
|
|
58
60
|
setIsLoadingTargetLang(false);
|
|
59
61
|
}
|
|
@@ -114,25 +116,25 @@ export default function PostClientContent({ initialPostData, currentSlug, childr
|
|
|
114
116
|
// This is a fallback or could indicate an issue if reached.
|
|
115
117
|
return (
|
|
116
118
|
<div className="container mx-auto px-4 py-8 text-center">
|
|
117
|
-
<h1 className="text-2xl font-bold mb-4">
|
|
118
|
-
<p className="text-muted-foreground">The
|
|
119
|
-
<p className="mt-4">
|
|
120
|
-
<Link href=
|
|
121
|
-
<span className="mx-2">|</span>
|
|
122
|
-
<Link href="/" className="text-primary hover:underline">Go to Homepage</Link>
|
|
123
|
-
</p>
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
119
|
+
<h1 className="text-2xl font-bold mb-4">Article Not Found</h1>
|
|
120
|
+
<p className="text-muted-foreground">The article for slug "{currentSlug}" could not be loaded.</p>
|
|
121
|
+
<p className="mt-4">
|
|
122
|
+
<Link href={`/${currentPrefix}`} className="text-primary hover:underline">Back to Articles</Link>
|
|
123
|
+
<span className="mx-2">|</span>
|
|
124
|
+
<Link href="/" className="text-primary hover:underline">Go to Homepage</Link>
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
// If initialPostData was null but we are still loading language context or trying to navigate
|
|
129
131
|
if (!currentPostData && (isLoadingLanguages || isLoadingTargetLang)) {
|
|
130
|
-
return <div className="container mx-auto px-4 py-20 text-center"><p>Loading
|
|
132
|
+
return <div className="container mx-auto px-4 py-20 text-center"><p>Loading article content...</p></div>;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
// If after all attempts, currentPostData is still null (should be caught by notFound in server component ideally)
|
|
134
136
|
if (!currentPostData) {
|
|
135
|
-
return <div className="container mx-auto px-4 py-20 text-center"><p>Could not load
|
|
137
|
+
return <div className="container mx-auto px-4 py-20 text-center"><p>Could not load article content for "{currentSlug}".</p></div>;
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
return (
|