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>
|
|||
|
|
);
|
|||
|
|
}
|