import Link from 'next/link'; import Image from 'next/image'; import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; import { allPosts } from 'contentlayer2/generated'; import { getPostBySlug, getRelatedPosts, getPostNeighbors } from '@/lib/posts'; import { siteConfig } from '@/lib/config'; import { ReadingProgress } from '@/components/reading-progress'; import { ScrollReveal } from '@/components/scroll-reveal'; import { PostLayout } from '@/components/post-layout'; import { PostCard } from '@/components/post-card'; import { PostStorylineNav } from '@/components/post-storyline-nav'; import { SectionDivider } from '@/components/section-divider'; import { FooterCue } from '@/components/footer-cue'; import { JsonLd } from '@/components/json-ld'; export function generateStaticParams() { return allPosts.map((post) => ({ slug: post.slug || post.flattenedPath })); } interface Props { params: Promise<{ slug: string }>; } export async function generateMetadata({ params }: Props): Promise { const { slug } = await params; const post = getPostBySlug(slug); if (!post) return {}; const ogImageUrl = new URL('/api/og', process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'); ogImageUrl.searchParams.set('title', post.title); if (post.description) { ogImageUrl.searchParams.set('description', post.description); } if (post.tags && post.tags.length > 0) { ogImageUrl.searchParams.set('tags', post.tags.slice(0, 3).join(',')); } return { title: post.title, description: post.description || post.title, openGraph: { title: post.title, description: post.description || post.title, type: 'article', publishedTime: post.published_at, authors: post.authors, tags: post.tags, images: [ { url: ogImageUrl.toString(), width: 1200, height: 630, alt: post.title, }, ], }, twitter: { card: 'summary_large_image', title: post.title, description: post.description || post.title, images: [ogImageUrl.toString()], }, }; } export default async function BlogPostPage({ params }: Props) { const { slug } = await params; const post = getPostBySlug(slug); if (!post) return notFound(); const relatedPosts = getRelatedPosts(post, 3); const neighbors = getPostNeighbors(post); const hasToc = / 0) { ogImageUrl.searchParams.set('tags', post.tags.slice(0, 3).join(',')); } // Get image URL - prefer feature_image, fallback to OG image const imageUrl = post.feature_image ? `${siteConfig.url}${post.feature_image.replace('../assets', '/assets')}` : ogImageUrl.toString(); // BlogPosting Schema const blogPostingSchema = { '@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.title, description: post.description || post.custom_excerpt || post.title, image: imageUrl, datePublished: post.published_at, dateModified: post.updated_at || post.published_at, author: { '@type': 'Person', name: post.authors?.[0] || siteConfig.author, url: siteConfig.url, }, publisher: { '@type': 'Organization', name: siteConfig.name, logo: { '@type': 'ImageObject', url: `${siteConfig.url}${siteConfig.avatar}`, }, }, mainEntityOfPage: { '@type': 'WebPage', '@id': postUrl, }, ...(post.tags && post.tags.length > 0 && { keywords: post.tags.join(', '), articleSection: post.tags[0], }), inLanguage: siteConfig.defaultLocale, url: postUrl, }; // BreadcrumbList Schema const breadcrumbSchema = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: '首頁', item: siteConfig.url, }, { '@type': 'ListItem', position: 2, name: '所有文章', item: `${siteConfig.url}/blog`, }, { '@type': 'ListItem', position: 3, name: post.title, item: postUrl, }, ], }; return ( <>
{/* Main content area for Pagefind indexing */}
{post.published_at && (

{new Date(post.published_at).toLocaleDateString( siteConfig.defaultLocale )}

)}

{post.title}

{post.tags && (
{post.tags.map((t) => ( #{t} ))}
)}
{post.feature_image && (
{post.title}
)}
{/* Exclude navigation and related posts from search indexing */}
{relatedPosts.length > 0 && (

相關文章

為你挑選相似主題

{relatedPosts.map((related) => ( ))}
)}
); }