feat(frontend): update pages, components and branding
Refresh Astro frontend implementation including new pages (Portfolio, Teams, Services), components, and styling updates.
This commit is contained in:
@@ -1,66 +1,356 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
/**
|
||||
* 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'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// Placeholder slugs - would fetch from CMS
|
||||
const slugs = [
|
||||
'2-zhao-yao-kong-xiao-fei-zhe-de-xin',
|
||||
'2022-jie-qing-xing-xiao-quan-gong-lue',
|
||||
// Add all from sitemap
|
||||
];
|
||||
// Get slug from params
|
||||
const { slug } = Astro.params
|
||||
|
||||
return slugs.map(slug => ({
|
||||
params: { slug },
|
||||
props: { slug }
|
||||
}));
|
||||
// Validate slug
|
||||
if (!slug) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
// Fetch post by slug
|
||||
const post = await fetchPostBySlug(slug)
|
||||
|
||||
// Placeholder content - would fetch from CMS
|
||||
const post = {
|
||||
title: 'Sample Post Title',
|
||||
date: 'January 20, 2022',
|
||||
content: 'Sample content...'
|
||||
};
|
||||
// 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>
|
||||
<section class="post-section">
|
||||
<div class="container">
|
||||
<a href="/news" class="back-link">回到文章列表</a>
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<p class="post-date">文章發布日期:{post.date}</p>
|
||||
<div class="post-content prose prose-custom max-w-none">
|
||||
<p>{post.content}</p>
|
||||
<!-- More content would be rendered here with markdown -->
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- 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>
|
||||
</section>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<style>
|
||||
.post-section {
|
||||
padding: 40px 0;
|
||||
<!-- 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;
|
||||
}
|
||||
.back-link {
|
||||
|
||||
.article-category {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #007bff;
|
||||
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;
|
||||
}
|
||||
.post-date {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.back-button:hover {
|
||||
border-color: var(--color-enchunblue, #23608c);
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
background: rgba(35, 96, 140, 0.05);
|
||||
}
|
||||
.post-content {
|
||||
/* Prose styles handle typography */
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.article-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@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>
|
||||
|
||||
Reference in New Issue
Block a user