297 lines
13 KiB
TypeScript
297 lines
13 KiB
TypeScript
import { notFound } from 'next/navigation';
|
||
import ReactMarkdown from 'react-markdown';
|
||
import remarkGfm from 'remark-gfm';
|
||
import { getTranslations, Locale, getNavigationPaths, defaultLocale } 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: {
|
||
id: string;
|
||
};
|
||
}
|
||
|
||
export async function generateStaticParams() {
|
||
// 默认语言的文章ID
|
||
const articles = ['ai-transformation-2024', 'cloud-security-best-practices', 'company-expansion-2024'];
|
||
return articles.map((id) => ({ id }));
|
||
}
|
||
|
||
export async function generateMetadata({ params }: NewsDetailPageProps) {
|
||
const { id } = params;
|
||
const locale = defaultLocale; // 使用默认语言
|
||
|
||
// 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 { id } = params;
|
||
const locale = defaultLocale; // 使用默认语言
|
||
|
||
// 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}>
|
||
{/* Breadcrumb */}
|
||
<section className="py-4 px-4 sm:px-6 lg:px-8 bg-gray-50 border-b">
|
||
<div className="max-w-4xl mx-auto">
|
||
<nav className="flex items-center space-x-2 text-sm text-gray-600">
|
||
<Link
|
||
href={navigationPaths.find((p) => p.key === 'home')?.path || '/'}
|
||
className="hover:text-blue-600 transition-colors"
|
||
>
|
||
{common.navigation.home}
|
||
</Link>
|
||
<span>/</span>
|
||
<Link
|
||
href={navigationPaths.find((p) => p.key === 'news')?.path || '/news'}
|
||
className="hover:text-blue-600 transition-colors"
|
||
>
|
||
{common.navigation.news}
|
||
</Link>
|
||
<span>/</span>
|
||
<span className="text-gray-900">{article.title}</span>
|
||
</nav>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Language Switcher */}
|
||
<section className="py-4 px-4 sm:px-6 lg:px-8 bg-white border-b">
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-2">
|
||
<span className="text-sm text-gray-600">可用语言:</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"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||
</svg>
|
||
<span>返回新闻</span>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Article Header */}
|
||
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white">
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className="mb-8">
|
||
<div className="flex items-center space-x-4 mb-6">
|
||
<span className="px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full font-medium">
|
||
{article.category}
|
||
</span>
|
||
{article.featured && (
|
||
<span className="px-3 py-1 bg-red-100 text-red-800 text-sm rounded-full font-medium">
|
||
精选
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<h1 className="text-4xl md:text-5xl font-light leading-tight mb-6 text-gray-900">
|
||
{article.title}
|
||
</h1>
|
||
|
||
<div className="flex flex-wrap items-center gap-6 text-gray-600 mb-6">
|
||
<div className="flex items-center space-x-2">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<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" />
|
||
</svg>
|
||
<span>{article.date}</span>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<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" />
|
||
</svg>
|
||
<span>{article.author}</span>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<span>{article.readTime}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<p className="text-xl text-gray-600 font-light leading-relaxed">
|
||
{article.excerpt}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Tags */}
|
||
{article.tags.length > 0 && (
|
||
<div className="flex flex-wrap gap-2 mb-8">
|
||
{article.tags.map((tag) => (
|
||
<span
|
||
key={tag}
|
||
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
|
||
>
|
||
#{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Article Content */}
|
||
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white">
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className="prose prose-lg prose-gray max-w-none">
|
||
<ReactMarkdown
|
||
remarkPlugins={[remarkGfm]}
|
||
components={{
|
||
h1: ({ children }) => (
|
||
<h1 className="text-3xl font-light mb-6 text-gray-900 border-b border-gray-200 pb-4">
|
||
{children}
|
||
</h1>
|
||
),
|
||
h2: ({ children }) => (
|
||
<h2 className="text-2xl font-light mb-4 text-gray-900 mt-8">
|
||
{children}
|
||
</h2>
|
||
),
|
||
h3: ({ children }) => (
|
||
<h3 className="text-xl font-light mb-3 text-gray-900 mt-6">
|
||
{children}
|
||
</h3>
|
||
),
|
||
p: ({ children }) => (
|
||
<p className="mb-4 text-gray-700 leading-relaxed">
|
||
{children}
|
||
</p>
|
||
),
|
||
ul: ({ children }) => (
|
||
<ul className="mb-4 pl-6 space-y-2">
|
||
{children}
|
||
</ul>
|
||
),
|
||
ol: ({ children }) => (
|
||
<ol className="mb-4 pl-6 space-y-2 list-decimal">
|
||
{children}
|
||
</ol>
|
||
),
|
||
li: ({ children }) => (
|
||
<li className="text-gray-700 leading-relaxed">
|
||
{children}
|
||
</li>
|
||
),
|
||
blockquote: ({ children }) => (
|
||
<blockquote className="border-l-4 border-blue-600 pl-6 my-6 italic text-gray-600">
|
||
{children}
|
||
</blockquote>
|
||
),
|
||
code: ({ children }) => (
|
||
<code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono">
|
||
{children}
|
||
</code>
|
||
),
|
||
pre: ({ children }) => (
|
||
<pre className="bg-gray-100 p-4 rounded-lg overflow-x-auto mb-4">
|
||
{children}
|
||
</pre>
|
||
),
|
||
}}
|
||
>
|
||
{article.content}
|
||
</ReactMarkdown>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Related Articles */}
|
||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50">
|
||
<div className="max-w-4xl mx-auto">
|
||
<h2 className="text-2xl font-light mb-8 text-gray-900">相关文章</h2>
|
||
<div className="grid md:grid-cols-2 gap-8">
|
||
{/* 这里可以添加相关文章的逻辑 */}
|
||
<div className="bg-white rounded-lg p-6 shadow-sm">
|
||
<h3 className="text-lg font-light mb-2 text-gray-900">更多新闻即将发布</h3>
|
||
<p className="text-gray-600 text-sm">
|
||
请持续关注我们的最新动态和行业见解。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</Layout>
|
||
);
|
||
}
|