Refresh Astro frontend implementation including new pages (Portfolio, Teams, Services), components, and styling updates.
357 lines
8.1 KiB
Plaintext
357 lines
8.1 KiB
Plaintext
---
|
|
/**
|
|
* Blog Article Detail Page - 行銷放大鏡文章詳情
|
|
* URL: /xing-xiao-fang-da-jing/[slug]
|
|
* Fetches from Payload CMS and renders with Lexical content
|
|
*/
|
|
import Layout from '../../layouts/Layout.astro'
|
|
import RelatedPosts from '../../components/blog/RelatedPosts.astro'
|
|
import { fetchPostBySlug, fetchRelatedPosts, formatDate } from '../../lib/api/blog'
|
|
import { serializeLexical } from '../../lib/serializeLexical'
|
|
|
|
// Get slug from params
|
|
const { slug } = Astro.params
|
|
|
|
// Validate slug
|
|
if (!slug) {
|
|
return Astro.redirect('/404')
|
|
}
|
|
|
|
// Fetch post by slug
|
|
const post = await fetchPostBySlug(slug)
|
|
|
|
// Handle 404 for non-existent or unpublished posts
|
|
if (!post) {
|
|
return Astro.redirect('/404')
|
|
}
|
|
|
|
// Fetch related posts
|
|
const categorySlugs = post.categories?.map(c => c.slug) || []
|
|
const relatedPosts = await fetchRelatedPosts(post.id, categorySlugs, 4)
|
|
|
|
// Format date
|
|
const displayDate = formatDate(post.publishedAt)
|
|
|
|
// Get primary category
|
|
const category = post.categories?.[0]
|
|
|
|
// Serialize Lexical content to HTML (must await the async function)
|
|
const contentHtml = await serializeLexical(post.content)
|
|
|
|
// SEO metadata
|
|
const metaTitle = post.meta?.title || post.title
|
|
const metaDescription = post.meta?.description || post.excerpt || ''
|
|
const metaImage = post.meta?.image?.url || post.ogImage?.url || post.heroImage?.url || ''
|
|
---
|
|
|
|
<Layout title={metaTitle} description={metaDescription}>
|
|
<article class="article-detail">
|
|
<!-- Article Header -->
|
|
<header class="article-header">
|
|
<div class="container">
|
|
<!-- Category Badge -->
|
|
{
|
|
category && (
|
|
<span
|
|
class="article-category"
|
|
style={`background-color: ${category.backgroundColor || 'var(--color-enchunblue)'}; color: ${category.textColor || '#fff'}`}
|
|
>
|
|
{category.title}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
<!-- Article Title -->
|
|
<h1 class="article-title">
|
|
{post.title}
|
|
</h1>
|
|
|
|
<!-- Published Date -->
|
|
{
|
|
displayDate && (
|
|
<time class="article-date" datetime={post.publishedAt || post.createdAt}>
|
|
{displayDate}
|
|
</time>
|
|
)
|
|
}
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Hero Image -->
|
|
{
|
|
post.heroImage?.url && (
|
|
<div class="article-hero-image">
|
|
<img
|
|
src={post.heroImage.url}
|
|
alt={post.heroImage.alt || post.title}
|
|
loading="eager"
|
|
width="1200"
|
|
height="675"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
<!-- Article Content -->
|
|
<div class="article-content">
|
|
<div class="container">
|
|
<div class="content-wrapper">
|
|
<!-- Render rich text content with Lexical to HTML conversion -->
|
|
<div class="prose" set:html={contentHtml} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Back to List Button -->
|
|
<div class="article-actions">
|
|
<div class="container">
|
|
<a href="/news" class="back-button">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
|
|
</svg>
|
|
回到文章列表
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Related Posts Section -->
|
|
<RelatedPosts posts={relatedPosts} />
|
|
</Layout>
|
|
|
|
<!-- Open Graph Tags -->
|
|
<script define:vars={{ metaImage, metaTitle, metaDescription }}>
|
|
if (typeof document !== 'undefined') {
|
|
// Update OG image
|
|
const ogImage = document.querySelector('meta[property="og:image"]')
|
|
if (ogImage && metaImage) {
|
|
ogImage.setAttribute('content', metaImage)
|
|
}
|
|
|
|
// Update OG title
|
|
const ogTitle = document.querySelector('meta[property="og:title"]')
|
|
if (ogTitle) {
|
|
ogTitle.setAttribute('content', metaTitle)
|
|
}
|
|
|
|
// Update OG description
|
|
const ogDesc = document.querySelector('meta[property="og:description"]')
|
|
if (ogDesc) {
|
|
ogDesc.setAttribute('content', metaDescription)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
/* Article Header */
|
|
.article-header {
|
|
padding: 60px 20px 40px;
|
|
text-align: center;
|
|
background-color: #ffffff;
|
|
}
|
|
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 0 20px;
|
|
}
|
|
|
|
.article-category {
|
|
display: inline-block;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 20px;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.article-title {
|
|
font-family: "Noto Sans TC", sans-serif;
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
color: var(--color-dark-blue, #1a1a1a);
|
|
line-height: 1.3;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.article-date {
|
|
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
|
font-size: 1rem;
|
|
color: var(--color-gray-500, #999);
|
|
}
|
|
|
|
/* Hero Image */
|
|
.article-hero-image {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
aspect-ratio: 16 / 9;
|
|
overflow: hidden;
|
|
background: var(--color-gray-100, #f5f5f5);
|
|
}
|
|
|
|
.article-hero-image img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
/* Article Content */
|
|
.article-content {
|
|
padding: 60px 20px;
|
|
background-color: #ffffff;
|
|
}
|
|
|
|
.content-wrapper {
|
|
max-width: 720px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Prose styles for rich text content */
|
|
.prose {
|
|
font-family: "Noto Sans TC", sans-serif;
|
|
color: var(--color-gray-700, #333);
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.prose :global(h1),
|
|
.prose :global(h2),
|
|
.prose :global(h3),
|
|
.prose :global(h4) {
|
|
font-weight: 700;
|
|
color: var(--color-dark-blue, #1a1a1a);
|
|
margin-top: 2rem;
|
|
margin-bottom: 1rem;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.prose :global(h1) { font-size: 2rem; }
|
|
.prose :global(h2) { font-size: 1.75rem; }
|
|
.prose :global(h3) { font-size: 1.5rem; }
|
|
.prose :global(h4) { font-size: 1.25rem; }
|
|
|
|
.prose :global(p) {
|
|
margin-bottom: 1.25rem;
|
|
font-size: 1.0625rem;
|
|
}
|
|
|
|
.prose :global(a) {
|
|
color: var(--color-enchunblue, #23608c);
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
|
|
.prose :global(a:hover) {
|
|
color: var(--color-enchunblue-hover, #1a4d6e);
|
|
}
|
|
|
|
.prose :global(img) {
|
|
border-radius: 8px;
|
|
margin: 2rem 0;
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.prose :global(ul),
|
|
.prose :global(ol) {
|
|
padding-left: 1.5rem;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.prose :global(li) {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.prose :global(blockquote) {
|
|
border-left: 4px solid var(--color-enchunblue, #23608c);
|
|
padding-left: 1rem;
|
|
margin: 2rem 0;
|
|
font-style: italic;
|
|
color: var(--color-gray-600, #666);
|
|
}
|
|
|
|
.prose :global(code) {
|
|
background: var(--color-gray-100, #f5f5f5);
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.prose :global(pre) {
|
|
background: var(--color-dark-blue, #1a1a1a);
|
|
color: #fff;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
overflow-x: auto;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.prose :global(pre code) {
|
|
background: transparent;
|
|
color: inherit;
|
|
}
|
|
|
|
/* Article Actions */
|
|
.article-actions {
|
|
padding: 20px;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.back-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 24px;
|
|
background: #ffffff;
|
|
border: 2px solid var(--color-gray-300, #e0e0e0);
|
|
border-radius: 8px;
|
|
color: var(--color-gray-700, #555);
|
|
font-family: "Noto Sans TC", sans-serif;
|
|
font-size: 0.9375rem;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
transition: all 0.25s ease;
|
|
}
|
|
|
|
.back-button:hover {
|
|
border-color: var(--color-enchunblue, #23608c);
|
|
color: var(--color-enchunblue, #23608c);
|
|
background: rgba(35, 96, 140, 0.05);
|
|
}
|
|
|
|
/* Responsive Adjustments */
|
|
@media (max-width: 991px) {
|
|
.article-title {
|
|
font-size: 2rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.article-header {
|
|
padding: 40px 16px 30px;
|
|
}
|
|
|
|
.article-title {
|
|
font-size: 1.75rem;
|
|
}
|
|
|
|
.article-hero-image {
|
|
aspect-ratio: 4 / 3;
|
|
}
|
|
|
|
.article-content {
|
|
padding: 40px 16px;
|
|
}
|
|
|
|
.content-wrapper {
|
|
padding: 0;
|
|
}
|
|
|
|
.prose :global(h1) { font-size: 1.75rem; }
|
|
.prose :global(h2) { font-size: 1.5rem; }
|
|
.prose :global(h3) { font-size: 1.25rem; }
|
|
.prose :global(p) {
|
|
font-size: 1rem;
|
|
}
|
|
}
|
|
</style>
|