468 lines
21 KiB
TypeScript
468 lines
21 KiB
TypeScript
|
|
import { notFound } from 'next/navigation';
|
||
|
|
import ReactMarkdown from 'react-markdown';
|
||
|
|
import remarkGfm from 'remark-gfm';
|
||
|
|
import { getTranslations, Locale, getNavigationPaths } from '@/lib/i18n';
|
||
|
|
import { generateMetadata as generateSEOMetadata } from '@/lib/seo';
|
||
|
|
import { getNewsArticle, getAvailableLocalesForArticle, checkArticleExists } from '@/lib/markdown';
|
||
|
|
import Layout from '@/components/Layout';
|
||
|
|
import LanguageSwitcher from '@/components/LanguageSwitcher';
|
||
|
|
import Link from 'next/link';
|
||
|
|
|
||
|
|
interface NewsDetailPageProps {
|
||
|
|
params: {
|
||
|
|
locale: Locale;
|
||
|
|
id: string;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function generateStaticParams() {
|
||
|
|
const locales = ['zh-CN', 'zh-TW', 'en'];
|
||
|
|
const articles = ['ai-transformation-2024', 'cloud-security-best-practices', 'company-expansion-2024'];
|
||
|
|
|
||
|
|
const params = [];
|
||
|
|
for (const locale of locales) {
|
||
|
|
for (const id of articles) {
|
||
|
|
params.push({ locale, id });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return params;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function generateMetadata({ params }: NewsDetailPageProps) {
|
||
|
|
const { locale, id } = params;
|
||
|
|
|
||
|
|
// Check if article exists
|
||
|
|
if (!checkArticleExists(id, locale)) {
|
||
|
|
return {
|
||
|
|
title: 'Article Not Found',
|
||
|
|
description: 'The requested article could not be found.',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const article = await getNewsArticle(id, locale);
|
||
|
|
if (!article) {
|
||
|
|
return {
|
||
|
|
title: 'Article Not Found',
|
||
|
|
description: 'The requested article could not be found.',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
title: article.title,
|
||
|
|
description: article.excerpt,
|
||
|
|
openGraph: {
|
||
|
|
title: article.title,
|
||
|
|
description: article.excerpt,
|
||
|
|
type: 'article',
|
||
|
|
publishedTime: article.date,
|
||
|
|
authors: [article.author],
|
||
|
|
tags: article.tags,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export default async function NewsDetailPage({ params }: NewsDetailPageProps) {
|
||
|
|
const { locale, id } = params;
|
||
|
|
|
||
|
|
// Check if article exists in the requested locale
|
||
|
|
if (!checkArticleExists(id, locale)) {
|
||
|
|
notFound();
|
||
|
|
}
|
||
|
|
|
||
|
|
const article = await getNewsArticle(id, locale);
|
||
|
|
if (!article) {
|
||
|
|
notFound();
|
||
|
|
}
|
||
|
|
|
||
|
|
const [common, availableLocales] = await Promise.all([
|
||
|
|
getTranslations(locale, 'common'),
|
||
|
|
getAvailableLocalesForArticle(id),
|
||
|
|
]);
|
||
|
|
|
||
|
|
const navigationPaths = getNavigationPaths(locale);
|
||
|
|
const navigation = [
|
||
|
|
{
|
||
|
|
name: common.navigation.home,
|
||
|
|
href: navigationPaths.find((p) => p.key === 'home')?.path || '/',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: common.navigation.products,
|
||
|
|
href: navigationPaths.find((p) => p.key === 'products')?.path || '/products',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: common.navigation.news,
|
||
|
|
href: navigationPaths.find((p) => p.key === 'news')?.path || '/news',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: common.navigation.support,
|
||
|
|
href: navigationPaths.find((p) => p.key === 'support')?.path || '/support',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: common.navigation.about,
|
||
|
|
href: navigationPaths.find((p) => p.key === 'about')?.path || '/about',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Layout locale={locale} navigation={navigation} common={common} data-oid="c6mjboc">
|
||
|
|
{/* Breadcrumb */}
|
||
|
|
<section className="py-4 px-4 sm:px-6 lg:px-8 bg-gray-50 border-b" data-oid="7s5l-j9">
|
||
|
|
<div className="max-w-4xl mx-auto" data-oid="hqsvp79">
|
||
|
|
<nav
|
||
|
|
className="flex items-center space-x-2 text-sm text-gray-600"
|
||
|
|
data-oid="m0w56xh"
|
||
|
|
>
|
||
|
|
<Link
|
||
|
|
href={navigationPaths.find((p) => p.key === 'home')?.path || '/'}
|
||
|
|
className="hover:text-blue-600 transition-colors"
|
||
|
|
data-oid="kpvk_7j"
|
||
|
|
>
|
||
|
|
{common.navigation.home}
|
||
|
|
</Link>
|
||
|
|
<span data-oid="tw5-oe_">/</span>
|
||
|
|
<Link
|
||
|
|
href={navigationPaths.find((p) => p.key === 'news')?.path || '/news'}
|
||
|
|
className="hover:text-blue-600 transition-colors"
|
||
|
|
data-oid=":-tqwq_"
|
||
|
|
>
|
||
|
|
{common.navigation.news}
|
||
|
|
</Link>
|
||
|
|
<span data-oid="o12p947">/</span>
|
||
|
|
<span className="text-gray-900" data-oid="49bdp62">
|
||
|
|
{article.title}
|
||
|
|
</span>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Language Switcher */}
|
||
|
|
<section className="py-4 px-4 sm:px-6 lg:px-8 bg-white border-b" data-oid="zmlodji">
|
||
|
|
<div className="max-w-4xl mx-auto" data-oid="gxv6myl">
|
||
|
|
<div className="flex items-center justify-between" data-oid="1c10d2u">
|
||
|
|
<div className="flex items-center space-x-2" data-oid="5c7b2mr">
|
||
|
|
<span className="text-sm text-gray-600" data-oid="wrn.7x1">
|
||
|
|
{locale === 'en'
|
||
|
|
? 'Available in:'
|
||
|
|
: locale === 'zh-TW'
|
||
|
|
? '可用語言:'
|
||
|
|
: '可用语言:'}
|
||
|
|
</span>
|
||
|
|
<LanguageSwitcher
|
||
|
|
currentLocale={locale}
|
||
|
|
availableLocales={availableLocales}
|
||
|
|
articleId={id}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Back to News */}
|
||
|
|
<Link
|
||
|
|
href={navigationPaths.find((p) => p.key === 'news')?.path || '/news'}
|
||
|
|
className="text-sm text-blue-600 hover:text-blue-800 flex items-center space-x-1"
|
||
|
|
data-oid="eatue3o"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
data-oid="k:sapi1"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M15 19l-7-7 7-7"
|
||
|
|
data-oid="e06c-co"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
<span data-oid="efqxeqy">
|
||
|
|
{locale === 'en'
|
||
|
|
? 'Back to News'
|
||
|
|
: locale === 'zh-TW'
|
||
|
|
? '返回新聞'
|
||
|
|
: '返回新闻'}
|
||
|
|
</span>
|
||
|
|
</Link>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Article Header */}
|
||
|
|
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white" data-oid="h:ik7pu">
|
||
|
|
<div className="max-w-4xl mx-auto" data-oid="j8v8ces">
|
||
|
|
<div className="mb-8" data-oid="p.xgl5-">
|
||
|
|
<div className="flex items-center space-x-4 mb-6" data-oid="zsrn6ji">
|
||
|
|
<span
|
||
|
|
className="px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full font-medium"
|
||
|
|
data-oid="wy0nqoy"
|
||
|
|
>
|
||
|
|
{article.category}
|
||
|
|
</span>
|
||
|
|
{article.featured && (
|
||
|
|
<span
|
||
|
|
className="px-3 py-1 bg-red-100 text-red-800 text-sm rounded-full font-medium"
|
||
|
|
data-oid="x-9lbop"
|
||
|
|
>
|
||
|
|
{locale === 'en'
|
||
|
|
? 'Featured'
|
||
|
|
: locale === 'zh-TW'
|
||
|
|
? '精選'
|
||
|
|
: '精选'}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h1
|
||
|
|
className="text-4xl md:text-5xl font-light leading-tight mb-6 text-gray-900"
|
||
|
|
data-oid="gd_tb.u"
|
||
|
|
>
|
||
|
|
{article.title}
|
||
|
|
</h1>
|
||
|
|
|
||
|
|
<div
|
||
|
|
className="flex flex-wrap items-center gap-6 text-gray-600 mb-6"
|
||
|
|
data-oid="h1xbibw"
|
||
|
|
>
|
||
|
|
<div className="flex items-center space-x-2" data-oid="hpuafgc">
|
||
|
|
<svg
|
||
|
|
className="w-5 h-5"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
data-oid="1dedyyy"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||
|
|
data-oid="iwixjjh"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
<span data-oid="4tznx3q">{article.date}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2" data-oid="65jmrbx">
|
||
|
|
<svg
|
||
|
|
className="w-5 h-5"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
data-oid="y9xj5gd"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||
|
|
data-oid="i1u38j6"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
<span data-oid="m76y57i">{article.author}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2" data-oid="844.7pi">
|
||
|
|
<svg
|
||
|
|
className="w-5 h-5"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
data-oid="re-164m"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||
|
|
data-oid="b0q9r7."
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
<span data-oid="49cu0of">{article.readTime}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p
|
||
|
|
className="text-xl text-gray-600 font-light leading-relaxed"
|
||
|
|
data-oid="b2er1ct"
|
||
|
|
>
|
||
|
|
{article.excerpt}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Tags */}
|
||
|
|
{article.tags.length > 0 && (
|
||
|
|
<div className="flex flex-wrap gap-2 mb-8" data-oid="i2rwhiz">
|
||
|
|
{article.tags.map((tag) => (
|
||
|
|
<span
|
||
|
|
key={tag}
|
||
|
|
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
|
||
|
|
data-oid="mo3ug:j"
|
||
|
|
>
|
||
|
|
#{tag}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Article Content */}
|
||
|
|
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white" data-oid="d02:vlw">
|
||
|
|
<div className="max-w-4xl mx-auto" data-oid=".8y4h8a">
|
||
|
|
<div className="prose prose-lg prose-gray max-w-none" data-oid="blhf5-s">
|
||
|
|
<ReactMarkdown
|
||
|
|
remarkPlugins={[remarkGfm]}
|
||
|
|
components={{
|
||
|
|
h1: ({ children }) => (
|
||
|
|
<h1
|
||
|
|
className="text-3xl font-light mb-6 text-gray-900 border-b border-gray-200 pb-4"
|
||
|
|
data-oid="k2f36kp"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</h1>
|
||
|
|
),
|
||
|
|
|
||
|
|
h2: ({ children }) => (
|
||
|
|
<h2
|
||
|
|
className="text-2xl font-light mb-4 mt-8 text-gray-900"
|
||
|
|
data-oid="us4v-ay"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</h2>
|
||
|
|
),
|
||
|
|
|
||
|
|
h3: ({ children }) => (
|
||
|
|
<h3
|
||
|
|
className="text-xl font-light mb-3 mt-6 text-gray-900"
|
||
|
|
data-oid="9h758:b"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</h3>
|
||
|
|
),
|
||
|
|
|
||
|
|
p: ({ children }) => (
|
||
|
|
<p
|
||
|
|
className="mb-4 leading-relaxed text-gray-700"
|
||
|
|
data-oid="v.5:d5y"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</p>
|
||
|
|
),
|
||
|
|
|
||
|
|
ul: ({ children }) => (
|
||
|
|
<ul
|
||
|
|
className="mb-4 space-y-2 list-disc list-inside text-gray-700"
|
||
|
|
data-oid="8tm.vpa"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</ul>
|
||
|
|
),
|
||
|
|
|
||
|
|
ol: ({ children }) => (
|
||
|
|
<ol
|
||
|
|
className="mb-4 space-y-2 list-decimal list-inside text-gray-700"
|
||
|
|
data-oid="2_dmi-o"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</ol>
|
||
|
|
),
|
||
|
|
|
||
|
|
li: ({ children }) => (
|
||
|
|
<li className="leading-relaxed" data-oid="p18xbtd">
|
||
|
|
{children}
|
||
|
|
</li>
|
||
|
|
),
|
||
|
|
|
||
|
|
blockquote: ({ children }) => (
|
||
|
|
<blockquote
|
||
|
|
className="border-l-4 border-blue-500 pl-4 py-2 mb-4 bg-blue-50 text-gray-700 italic"
|
||
|
|
data-oid="letfg8v"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</blockquote>
|
||
|
|
),
|
||
|
|
|
||
|
|
code: ({ children }) => (
|
||
|
|
<code
|
||
|
|
className="bg-gray-100 px-2 py-1 rounded text-sm font-mono text-gray-800"
|
||
|
|
data-oid=":rcw03n"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</code>
|
||
|
|
),
|
||
|
|
|
||
|
|
pre: ({ children }) => (
|
||
|
|
<pre
|
||
|
|
className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-4"
|
||
|
|
data-oid="kkqwbw_"
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</pre>
|
||
|
|
),
|
||
|
|
}}
|
||
|
|
data-oid="gwxptyo"
|
||
|
|
>
|
||
|
|
{article.content}
|
||
|
|
</ReactMarkdown>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Article Footer */}
|
||
|
|
<section className="py-8 px-4 sm:px-6 lg:px-8 bg-gray-50 border-t" data-oid="a4:0vxi">
|
||
|
|
<div className="max-w-4xl mx-auto" data-oid="gs4ey_i">
|
||
|
|
<div
|
||
|
|
className="flex flex-col md:flex-row justify-between items-start md:items-center space-y-4 md:space-y-0"
|
||
|
|
data-oid="utuumme"
|
||
|
|
>
|
||
|
|
<div data-oid="03ew7k3">
|
||
|
|
<p className="text-gray-600 text-sm" data-oid="40k1zhp">
|
||
|
|
{locale === 'en'
|
||
|
|
? 'Published on'
|
||
|
|
: locale === 'zh-TW'
|
||
|
|
? '發布於'
|
||
|
|
: '发布于'}{' '}
|
||
|
|
{article.date}
|
||
|
|
</p>
|
||
|
|
<p className="text-gray-600 text-sm" data-oid="qar67n3">
|
||
|
|
{locale === 'en' ? 'By' : locale === 'zh-TW' ? '作者:' : '作者:'}{' '}
|
||
|
|
{article.author}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex space-x-4" data-oid="8yznw49">
|
||
|
|
<button
|
||
|
|
className="text-blue-600 hover:text-blue-800 text-sm flex items-center space-x-1"
|
||
|
|
data-oid="gzw87qn"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
data-oid="ldlorm8"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
|
||
|
|
data-oid="l34b4tz"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
<span data-oid="ww8eb3z">
|
||
|
|
{locale === 'en'
|
||
|
|
? 'Share'
|
||
|
|
: locale === 'zh-TW'
|
||
|
|
? '分享'
|
||
|
|
: '分享'}
|
||
|
|
</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</Layout>
|
||
|
|
);
|
||
|
|
}
|