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:
2026-02-11 11:50:42 +08:00
parent be7fc902fb
commit 9c2181f743
49 changed files with 9699 additions and 899 deletions

View File

@@ -1,31 +1,38 @@
---
import Layout from '../layouts/Layout.astro';
/**
* About Page - 關於恩群數位
* 展示公司特色、服務優勢和與其他公司的差異
*/
import Layout from '../layouts/Layout.astro'
import AboutHero from '../sections/AboutHero.astro'
import FeatureSection from '../sections/FeatureSection.astro'
import ComparisonSection from '../sections/ComparisonSection.astro'
import CTASection from '../sections/CTASection.astro'
// Metadata for SEO
const title = '關於恩群數位 | 專業數位行銷服務團隊'
const description = '恩群數位行銷成立於2018年提供全方位數位行銷服務。我們在地化優先、數據驅動是您最可信赖的數位行銷夥伴。'
---
<Layout>
<section class="about-section">
<div class="container">
<h1>關於恩群</h1>
<div class="prose prose-custom max-w-none">
<p>恩群數位行銷有限公司成立於2018年專注於數位行銷服務。</p>
<p>我們擁有豐富的廣告行銷操作經驗,提供全方位行銷解決方案。</p>
<!-- Add more content from HTML -->
</div>
</div>
</section>
<Layout title={title} description={description}>
<!-- Hero Section -->
<AboutHero />
<!-- Service Features Section -->
<FeatureSection />
<!-- Comparison Section -->
<ComparisonSection />
<!-- CTA Section -->
<CTASection
homeData={{
ctaSection: {
headline: '準備好開始新的旅程了嗎',
description: '讓我們一起為您的品牌打造獨特的數位行銷策略',
buttonText: '聯絡我們',
buttonLink: '/contact-us',
},
}}
/>
</Layout>
<style>
.about-section {
padding: 40px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
</style>

View File

@@ -1,85 +1,625 @@
---
import Layout from '../layouts/Layout.astro';
/**
* Contact Page - 聯絡我們頁面
* Pixel-perfect implementation based on Webflow design
* Includes form validation, submission handling, and responsive layout
*/
import Layout from '../layouts/Layout.astro'
// Metadata for SEO
const title = '聯絡我們 | 恩群數位行銷'
const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我們的專業團隊將竭誠為您服務。電話: 02 5570 0527Email: enchuntaiwan@gmail.com'
---
<Layout>
<section class="contact-section">
<div class="container">
<h1>聯絡我們</h1>
<form id="contact-form">
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" required />
<Layout title={title} description={description}>
<section class="contact-section" id="contact">
<div class="contactus_wrapper">
<!-- Contact Form Side -->
<div class="contact_form_wrapper">
<h1 class="contact_head">聯絡我們</h1>
<p class="contact_parafraph">
有任何問題嗎?歡迎聯絡我們,我們將竭誠為您服務。
</p>
<p class="contact_reminder">
* 標註欄位為必填
</p>
<!-- Contact Form -->
<form id="contact-form" class="contact_form" novalidate>
<!-- Success Message -->
<div id="form-success" class="w-form-done" style="display: none;">
感謝您的留言!我們會盡快回覆您。
</div>
<!-- Error Message -->
<div id="form-error" class="w-form-fail" style="display: none;">
送出失敗,請稍後再試或直接來電。
</div>
<!-- Form Fields -->
<div class="contact-form-grid">
<!-- Name Field -->
<div class="contact_field_wrapper">
<label for="Name" class="contact_field_name">
姓名 <span>*</span>
</label>
<input
type="text"
id="Name"
name="Name"
class="input_field"
required
minlength="2"
maxlength="256"
placeholder="請輸入您的姓名"
/>
<span class="error-message" id="Name-error"></span>
</div>
<!-- Phone Field -->
<div class="contact_field_wrapper">
<label for="Phone" class="contact_field_name">
聯絡電話 <span>*</span>
</label>
<input
type="tel"
id="Phone"
name="Phone"
class="input_field"
required
placeholder="請輸入您的電話號碼"
/>
<span class="error-message" id="Phone-error"></span>
</div>
</div>
<!-- Email Field -->
<div class="contact_field_wrapper">
<label for="Email" class="contact_field_name">
Email <span>*</span>
</label>
<input
type="email"
id="Email"
name="Email"
class="input_field"
required
placeholder="請輸入您的 Email"
/>
<span class="error-message" id="Email-error"></span>
</div>
<!-- Message Field -->
<div class="contact_field_wrapper">
<label for="Message" class="contact_field_name">
聯絡訊息 <span>*</span>
</label>
<textarea
id="Message"
name="Message"
class="input_field"
minlength="10"
maxlength="5000"
required
placeholder="請輸入您的訊息(至少 10 個字元)"
></textarea>
<span class="error-message" id="Message-error"></span>
</div>
<!-- Submit Button -->
<button type="submit" class="submit-button" id="submit-btn">
<span class="button-text">送出訊息</span>
<span class="button-loading" style="display: none;">送出中...</span>
</button>
</form>
</div>
<!-- Contact Image Side -->
<div class="contact-image">
<div class="image-wrapper">
<img
src="/placeholder-contact.jpg"
alt="聯絡恩群數位"
width="600"
height="400"
/>
</div>
<div class="form-group">
<label for="email">電子郵件</label>
<input type="email" id="email" name="email" required />
<!-- Contact Info Card -->
<div class="contact-info-card">
<h3>聯絡資訊</h3>
<div class="info-item">
<svg class="info-icon" viewBox="0 0 24 24"><path fill="currentColor" d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47-1.14.75-1.02zM2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02L2.05 21.05zM19.95 2.95c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36.11-.74-.47-1.14-.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53z"/></svg>
<span>諮詢電話: 02 5570 0527</span>
</div>
<div class="info-item">
<svg class="info-icon" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
<span>Email: <a href="mailto:enchuntaiwan@gmail.com">enchuntaiwan@gmail.com</a></span>
</div>
</div>
<div class="form-group">
<label for="message">訊息</label>
<textarea id="message" name="message" required></textarea>
</div>
<button type="submit">送出</button>
</form>
<div class="contact-info">
<p>諮詢電話: 02 5570 0527</p>
<p>電子郵件: <a href="mailto:enchuntaiwan@gmail.com">enchuntaiwan@gmail.com</a></p>
</div>
</div>
</section>
</Layout>
<script>
// Basic form handler - would integrate with Cloudflare Worker
document.getElementById('contact-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
// Submit to Cloudflare Worker
alert('Form submitted (placeholder)');
});
</script>
<style>
/* Contact Section Styles - Pixel-perfect from Webflow */
.contact-section {
padding: 40px 0;
padding: 4rem 0;
background: var(--color-background);
scroll-margin-top: 80px;
}
.container {
max-width: 800px;
.contactus_wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input, textarea {
/* Form Side */
.contact_form_wrapper {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
height: 150px;
/* Headings */
.contact_head {
font-size: 2.5rem;
font-weight: 700;
line-height: 1.2;
color: var(--color-text-primary);
margin-bottom: 1rem;
}
button {
background: #007bff;
.contact_parafraph {
font-size: 1.125rem;
font-weight: 400;
line-height: 1.6;
color: var(--color-gray-600);
margin-bottom: 0.5rem;
}
.contact_reminder {
font-size: 0.875rem;
font-style: italic;
color: var(--color-text-muted);
margin-bottom: 2rem;
}
/* Form Container */
.contact_form {
padding: 2rem;
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
/* Form Grid */
.contact-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
/* Field Wrapper */
.contact_field_wrapper {
display: flex;
flex-direction: column;
margin-bottom: 1.5rem;
}
.contact_field_wrapper:last-child {
margin-bottom: 2rem;
}
/* Field Label */
.contact_field_name {
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
display: block;
}
.contact_field_name span {
color: var(--color-primary);
}
/* Input Fields */
.input_field {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-size: 1rem;
line-height: 1.5;
background: var(--color-background);
transition: all var(--transition-fast);
font-family: inherit;
color: var(--color-text-primary);
}
.input_field:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(56, 152, 236, 0.1);
transform: translateY(-1px);
}
.input_field::placeholder {
color: var(--color-text-muted);
}
.input_field.error {
border-color: #dc3545 !important;
background-color: #fff5f5;
}
#Message {
min-height: 120px;
resize: vertical;
}
/* Error Message */
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
display: none;
}
.error-message:not(:empty) {
display: block;
}
/* Submit Button */
.submit-button {
background: var(--color-primary);
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
border-radius: var(--radius);
padding: 0.875rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.contact-info {
margin-top: 40px;
transition: all var(--transition-fast);
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 200px;
width: 100%;
}
</style>
.submit-button:hover:not(:disabled) {
background: var(--color-primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.submit-button:active:not(:disabled) {
transform: translateY(0);
}
.submit-button:disabled {
background: var(--color-gray-500);
cursor: not-allowed;
opacity: 0.8;
}
/* Form Success/Error Messages */
.w-form-done,
.w-form-fail {
padding: 1rem 1.5rem;
border-radius: var(--radius);
margin-top: 1rem;
text-align: center;
font-weight: 500;
animation: slideUp 0.3s ease-out;
}
.w-form-done {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.w-form-fail {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Image Side */
.contact-image {
display: flex;
flex-direction: column;
gap: 2rem;
justify-content: center;
align-items: center;
}
.image-wrapper {
width: 100%;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-md);
background: var(--color-gray-100);
aspect-ratio: 3/2;
}
.image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Contact Info Card */
.contact-info-card {
width: 100%;
padding: 1.5rem;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.contact-info-card h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 1rem;
}
.info-item {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
font-size: 0.95rem;
color: var(--color-text-secondary);
}
.info-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
color: var(--color-primary);
}
.info-item a {
color: var(--color-primary);
text-decoration: none;
}
.info-item a:hover {
text-decoration: underline;
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.contactus_wrapper {
grid-template-columns: 1fr;
gap: 2rem;
}
.contact_head {
font-size: 2rem;
}
}
@media (max-width: 767px) {
.contact-section {
padding: 2rem 0;
}
.contact_form {
padding: 1.5rem;
}
.contact-form-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.submit-button {
min-width: 160px;
}
}
@media (max-width: 479px) {
.contact_form {
padding: 1rem;
}
.contact_head {
font-size: 1.5rem;
}
}
</style>
<script>
// Form validation and submission handler
function initContactForm() {
const form = document.getElementById('contact-form') as HTMLFormElement
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement
const successMsg = document.getElementById('form-success') as HTMLElement
const errorMsg = document.getElementById('form-error') as HTMLElement
if (!form) return
// Validation patterns
const patterns = {
Name: /^[\u4e00-\u9fa5a-zA-Z\s]{2,256}$/,
Phone: /^[0-9\-\s\+]{6,20}$/,
Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
Message: /^.{10,5000}$/
}
// Validation function
function validateField(input: HTMLInputElement | HTMLTextAreaElement): boolean {
const name = input.name
const value = input.value.trim()
const errorSpan = document.getElementById(`${name}-error`) as HTMLElement
if (!input.hasAttribute('required') && !value) {
clearError(input, errorSpan)
return true
}
let isValid = true
let errorMessage = ''
// Required check
if (input.hasAttribute('required') && !value) {
isValid = false
errorMessage = '此欄位為必填'
}
// Pattern validation
else if (patterns[name as keyof typeof patterns] && !patterns[name as keyof typeof patterns].test(value)) {
isValid = false
switch (name) {
case 'Name':
errorMessage = '請輸入有效的姓名(至少 2 個字元)'
break
case 'Phone':
errorMessage = '請輸入有效的電話號碼'
break
case 'Email':
errorMessage = '請輸入有效的 Email 格式'
break
case 'Message':
errorMessage = '訊息至少需要 10 個字元'
break
}
}
// Show/hide error
if (!isValid) {
input.classList.add('error')
if (errorSpan) {
errorSpan.textContent = errorMessage
errorSpan.style.display = 'block'
}
} else {
clearError(input, errorSpan)
}
return isValid
}
function clearError(input: HTMLInputElement | HTMLTextAreaElement, errorSpan: HTMLElement) {
input.classList.remove('error')
if (errorSpan) {
errorSpan.textContent = ''
errorSpan.style.display = 'none'
}
}
// Real-time validation on blur
form.querySelectorAll('input, textarea').forEach((field) => {
field.addEventListener('blur', () => {
validateField(field as HTMLInputElement | HTMLTextAreaElement)
})
field.addEventListener('input', () => {
const input = field as HTMLInputElement | HTMLTextAreaElement
if (input.classList.contains('error')) {
validateField(input)
}
})
})
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault()
// Validate all fields
const inputs = form.querySelectorAll('input, textarea') as NodeListOf<HTMLInputElement | HTMLTextAreaElement>
let isFormValid = true
inputs.forEach((input) => {
if (!validateField(input)) {
isFormValid = false
}
})
if (!isFormValid) {
// Scroll to first error
const firstError = form.querySelector('.input_field.error') as HTMLElement
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
return
}
// Show loading state
submitBtn.disabled = true
const buttonText = submitBtn.querySelector('.button-text') as HTMLElement
const buttonLoading = submitBtn.querySelector('.button-loading') as HTMLElement
if (buttonText) buttonText.style.display = 'none'
if (buttonLoading) buttonLoading.style.display = 'inline'
// Hide previous messages
successMsg.style.display = 'none'
errorMsg.style.display = 'none'
// Collect form data
const formData = new FormData(form)
const data = {
name: formData.get('Name'),
phone: formData.get('Phone'),
email: formData.get('Email'),
message: formData.get('Message')
}
try {
// Submit to backend (via API proxy)
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
if (response.ok) {
// Success
successMsg.style.display = 'block'
form.reset()
window.scrollTo({ top: 0, behavior: 'smooth' })
} else {
throw new Error('Submission failed')
}
} catch (error) {
console.error('Form submission error:', error)
errorMsg.style.display = 'block'
} finally {
// Reset button state
submitBtn.disabled = false
if (buttonText) buttonText.style.display = 'inline'
if (buttonLoading) buttonLoading.style.display = 'none'
}
})
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initContactForm)
if (document.readyState !== 'loading') {
initContactForm()
}
</script>

View File

@@ -1,66 +1,56 @@
---
import Layout from "../layouts/Layout.astro";
import VideoHero from "../components/videoHero.astro";
/**
* Homepage - Main landing page
* Fetches data from Payload CMS and renders all sections
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../layouts/Layout.astro'
import HeroSection from '../sections/HeroSection.astro'
import PainpointSection from '../sections/PainpointSection.astro'
import StatisticsSection from '../sections/StatisticsSection.astro'
// ServiceFeatures - REMOVED: Services should only appear in Footer, not as a main section
// import ServiceFeatures from '../sections/ServiceFeatures.astro'
import ClientCasesSection from '../sections/ClientCasesSection.astro'
import PortfolioPreview from '../sections/PortfolioPreview.astro'
import CTASection from '../sections/CTASection.astro'
import { getHomepageData } from '../lib/api/home'
import type { HomeData, PortfolioItem } from '../lib/api/home'
// Fetch data from Payload CMS
let homeData: HomeData | null = null
let portfolioItems: PortfolioItem[] = []
try {
const data = await getHomepageData()
homeData = data.home
portfolioItems = data.portfolioItems || []
} catch (error) {
console.error('Failed to fetch homepage data:', error)
}
// Metadata for SEO
const title = '恩群數位行銷 | 專業數位行銷服務'
const description = '恩群數位行銷提供專業的 Google Ads、社群代操、論壇行銷、網站設計等全方位數位行銷服務協助您的品牌在數位時代脫穎而出。'
---
<Layout>
<!-- Hero Section -->
<VideoHero
desktopVideo="/video/enchun-hero-background-video.mp4"
mobileVideo="/video/enchun-hero-background-video.webm"
logo="/enchun-logo-full.svg"
header="創造企業更多發展的可能性\n是我們的使命"
subheader="Its our destiny to create possibilities for your business."
/>
<Layout title={title} description={description}>
<!-- Hero Section -->
<HeroSection homeData={homeData} />
<!-- Services Section -->
<section class="py-16 bg-surface">
<div class="max-w-6xl mx-auto px-4">
<h2 class="text-3xl font-bold text-center text-text mb-12">
我們的服務
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-semibold text-primary mb-4">
Google Ads
</h3>
<p class="text-text">
專業的Google廣告投放服務幫助您的品牌觸及目標客戶。
</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-semibold text-primary mb-4">
社群行銷
</h3>
<p class="text-text">
全方位社群媒體經營,從內容策劃到數據分析,一站式服務。
</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-semibold text-primary mb-4">
網站設計
</h3>
<p class="text-text">
現代化響應式網站設計,提升品牌形象和用戶體驗。
</p>
</div>
</div>
</div>
</section>
<!-- Painpoint Section (您的困擾) -->
<PainpointSection homeData={homeData} />
<!-- About Section -->
<section class="py-16">
<div class="max-w-6xl mx-auto px-4 text-center">
<h2 class="text-3xl font-bold text-text mb-8">關於恩群</h2>
<p class="text-lg text-text max-w-3xl mx-auto">
恩群數位行銷團隊擁有豐富的數位行銷經驗,我們相信在地化優先、高投資轉換率、數據優先、關係優於銷售。
每一個客戶都是我們重視的夥伴,我們珍惜與客戶的合作關係。
</p>
<a
href="/about-enchun"
class="inline-block mt-6 bg-primary text-white px-6 py-3 rounded-lg font-semibold hover:bg-primary/90 transition-colors"
>了解更多</a
>
</div>
</section>
<!-- Statistics Section (數據統計) -->
<StatisticsSection homeData={homeData} />
<!-- Service Features - REMOVED: Services should only appear in Footer, not as a main section -->
<!-- Client Cases Section (客戶案例) -->
<ClientCasesSection homeData={homeData} />
<!-- Portfolio Preview Section -->
<PortfolioPreview homeData={homeData} portfolioItems={portfolioItems} />
<!-- CTA Section -->
<CTASection homeData={homeData} />
</Layout>

View File

@@ -1,43 +1,24 @@
---
import Layout from '../layouts/Layout.astro';
/**
* Marketing Solutions Page - 行銷方案頁面
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../layouts/Layout.astro'
import SolutionsHero from '../sections/SolutionsHero.astro'
import ServicesList from '../sections/ServicesList.astro'
// Metadata for SEO
const title = '行銷解決方案 | 恩群數位行銷'
const description = '恩群數位行銷提供全方位的數位行銷服務,包括 Google Ads、社群代操、論壇行銷、網紅行銷、網站設計等協助您的品牌在數位時代脫穎而出。'
---
<Layout>
<section class="solutions-section">
<div class="container">
<h1>行銷方案</h1>
<p>我們提供多樣化的行銷方案,幫助您的品牌深入人心。</p>
<ul>
<li>Google 商家關鍵字</li>
<li>Google Ads</li>
<li>社群代操</li>
<li>論壇行銷</li>
<li>網紅行銷</li>
<li>形象影片</li>
</ul>
</div>
</section>
<Layout title={title} description={description}>
<!-- Hero Section -->
<SolutionsHero
title="行銷解決方案"
subtitle="提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出"
/>
<!-- Services List -->
<ServicesList />
</Layout>
<style>
.solutions-section {
padding: 40px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 10px 0;
border-bottom: 1px solid #eee;
}
</style>

View File

@@ -1,61 +1,289 @@
---
import Layout from '../layouts/Layout.astro';
/**
* News/Blog Listing Page - 行銷放大鏡
* URL: /news
* Displays all published blog posts from Payload CMS
*/
import Layout from '../layouts/Layout.astro'
import ArticleCard from '../components/blog/ArticleCard.astro'
import CategoryFilter from '../components/blog/CategoryFilter.astro'
import { fetchPosts, fetchCategories } from '../lib/api/blog'
// Placeholder for blog posts - would fetch from CMS
const posts = [
{ slug: 'en-qun-shu-wei-zui-xin-gong-gao', title: '恩群數位最新公告', date: '2023-01-01' },
{ slug: 'google-xiao-xue-tang', title: 'Google小學堂', date: '2023-01-02' },
// Add more
];
// Metadata for SEO
const title = '行銷放大鏡 | 恩群數位行銷'
const description = '閱讀恩群數位的專業行銷文章掌握最新的數位行銷趨勢、社群經營技巧、Google 廣告策略等實用內容。'
// Pagination settings
const PAGE_SIZE = 12
// Get query parameters
const url = Astro.url
const pageParam = url.searchParams.get('page')
const page = Math.max(1, parseInt(pageParam || '1'))
// Fetch posts from Payload CMS
const postsData = await fetchPosts(page, PAGE_SIZE)
const posts = postsData.docs
const totalPages = postsData.totalPages
const hasPreviousPage = postsData.hasPreviousPage
const hasNextPage = postsData.hasNextPage
// Fetch categories
const categories = await fetchCategories()
// Filter out categories without slugs and the legacy "文章分類" container
const validCategories = categories.filter(c =>
c.slug && c.slug !== 'wen-zhang-fen-lei'
)
---
<Layout>
<section class="news-section">
<Layout title={title} description={description}>
<!-- Blog Hero Section -->
<section class="blog-hero" aria-labelledby="blog-heading">
<div class="container">
<h1>行銷放大鏡</h1>
<div class="posts-grid">
{posts.map(post => (
<article class="post-card">
<h2><a href={`/wen-zhang-fen-lei/${post.slug}`}>{post.title}</a></h2>
<p>{post.date}</p>
</article>
))}
<div class="section_header_w_line">
<div class="divider_line"></div>
<div class="header_subtitle">
<h2 id="blog-heading" class="header_subtitle_head">行銷放大鏡</h2>
<p class="header_subtitle_paragraph">Marketing Blog</p>
</div>
<div class="divider_line"></div>
</div>
</div>
</section>
<!-- Category Filter -->
<section class="filter-section">
<div class="container">
<CategoryFilter categories={validCategories} />
</div>
</section>
<!-- Blog Posts Grid -->
<section class="blog-section" aria-label="文章列表">
<div class="container">
{
posts.length > 0 ? (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-10 w-full max-w-[1200px] mx-auto">
{posts.map((post) => (
<ArticleCard post={post} />
))}
</div>
) : (
<div class="empty-state">
<p class="empty-text">暫無文章</p>
</div>
)
}
<!-- Pagination -->
{
totalPages > 1 && (
<nav class="pagination" aria-label="分頁導航">
<div class="pagination-container">
{
hasPreviousPage && (
<a
href={`?page=${page - 1}`}
class="pagination-link pagination-link-prev"
aria-label="上一頁"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
</svg>
上一頁
</a>
)
}
<div class="pagination-info">
<span class="current-page">{page}</span>
<span class="page-separator">/</span>
<span class="total-pages">{totalPages}</span>
</div>
{
hasNextPage && (
<a
href={`?page=${page + 1}`}
class="pagination-link pagination-link-next"
aria-label="下一頁"
>
下一頁
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</svg>
</a>
)
}
</div>
</nav>
)
}
</div>
</section>
</Layout>
<style>
.news-section {
padding: 40px 0;
/* Blog Hero Section */
.blog-hero {
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
.section_header_w_line {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.header_subtitle {
text-align: center;
margin-bottom: 30px;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
.header_subtitle_head {
color: var(--color-enchunblue, #23608c);
font-family: "Noto Sans TC", sans-serif;
font-weight: 700;
font-size: 2rem;
margin-bottom: 8px;
}
.post-card {
border: 1px solid #ddd;
padding: 20px;
.header_subtitle_paragraph {
color: var(--color-gray-600, #666);
font-family: "Quicksand", "Noto Sans TC", sans-serif;
font-weight: 400;
font-size: 1rem;
}
.divider_line {
width: 40px;
height: 2px;
background-color: var(--color-enchunblue, #23608c);
}
/* Filter Section */
.filter-section {
background-color: #ffffff;
padding-bottom: 20px;
}
/* Blog Section */
.blog-section {
padding: 40px 20px 60px;
background-color: #f8f9fa;
min-height: 60vh;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-text {
font-size: 1.125rem;
color: var(--color-gray-500, #999);
}
/* Pagination */
.pagination {
padding-top: 20px;
}
.pagination-container {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
}
.pagination-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #ffffff;
border: 2px solid var(--color-gray-300, #e0e0e0);
border-radius: 8px;
}
.post-card h2 {
margin-top: 0;
}
.post-card a {
color: var(--color-gray-700, #555);
font-family: "Noto Sans TC", sans-serif;
font-size: 0.9375rem;
font-weight: 500;
text-decoration: none;
color: #333;
transition: all 0.25s ease;
}
.post-card a:hover {
color: #007bff;
.pagination-link:hover {
border-color: var(--color-enchunblue, #23608c);
color: var(--color-enchunblue, #23608c);
background: rgba(35, 96, 140, 0.05);
}
</style>
.pagination-info {
display: flex;
align-items: center;
gap: 8px;
font-family: "Quicksand", sans-serif;
font-size: 1rem;
color: var(--color-gray-600, #666);
}
.current-page {
font-weight: 700;
color: var(--color-enchunblue, #23608c);
}
.page-separator {
color: var(--color-gray-400, #ccc);
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.header_subtitle_head {
font-size: 1.75rem;
}
}
@media (max-width: 767px) {
.blog-hero {
padding: 40px 16px;
}
.section_header_w_line {
flex-wrap: wrap;
gap: 12px;
}
.divider_line {
width: 30px;
}
.blog-section {
padding: 30px 16px 50px;
}
.pagination-container {
gap: 16px;
}
.pagination-link {
padding: 8px 16px;
font-size: 0.875rem;
}
.header_subtitle_head {
font-size: 1.5rem;
}
.header_subtitle_paragraph {
font-size: 0.9375rem;
}
}
</style>

View File

@@ -1,28 +1,181 @@
---
import Layout from '../layouts/Layout.astro';
/**
* Teams Page - 恩群大本營
* 展示工作環境、公司故事和員工福利
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../layouts/Layout.astro'
import TeamsHero from '../sections/TeamsHero.astro'
import EnvironmentSlider from '../sections/EnvironmentSlider.astro'
import CompanyStory from '../sections/CompanyStory.astro'
import BenefitsSection from '../sections/BenefitsSection.astro'
// Metadata for SEO
const title = '恩群大本營 | 恩群數位行銷'
const description = '加入恩群數位的團隊,享受優質的工作環境與完善的員工福利。我們重視個人的特質發揮,歡迎樂於學習、善於建立關係的你加入我們。'
---
<Layout>
<section class="teams-section">
<div class="container">
<h1>恩群大本營</h1>
<p>認識我們的團隊成員。</p>
<!-- Team members would be listed here -->
<Layout title={title} description={description}>
<!-- Hero Section -->
<TeamsHero />
<!-- Environment Slider Section -->
<EnvironmentSlider />
<!-- Company Story Section -->
<CompanyStory />
<!-- Benefits Section -->
<BenefitsSection />
<!-- CTA Section -->
<section class="section-call4action" aria-labelledby="cta-heading">
<div class="container w-container">
<div class="c4a-grid">
<div class="c4a-content">
<h3 id="cta-heading" class="career-c4a-heading">
以人的成長為優先<br />
創造人的最大價值
</h3>
<p class="career-c4a-paragraph">
在恩群數位裡我們重視個人的特質能夠完全發揮,只要你樂於學習、善於跟人建立關係,並且重要的是你有一個善良的心,恩群數位歡迎你的加入
</p>
</div>
<a
href="https://www.104.com.tw/company/1a2x6bkoaj?jobsource=joblist_r_cust"
target="_blank"
rel="noopener noreferrer"
class="c4a-button"
>
立刻申請面試
</a>
<div class="c4a-bg"></div>
</div>
</div>
</section>
</Layout>
<style>
.teams-section {
padding: 40px 0;
/* CTA Section Styles - Pixel-perfect from Webflow */
.section-call4action {
padding: 80px 20px;
text-align: center;
background-color: var(--color-gray-100, #f8f9fa);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
.w-container {
max-width: 1200px;
margin: 0 auto;
}
</style>
.c4a-grid {
display: grid;
grid-template-columns: 1fr auto;
gap: 32px;
align-items: center;
position: relative;
}
.c4a-content {
text-align: left;
}
.career-c4a-heading {
font-family: "Noto Sans TC", sans-serif;
font-weight: 600;
font-size: 1.75rem;
color: var(--color-tarawera, #23608c);
margin-bottom: 16px;
line-height: 1.4;
}
.career-c4a-paragraph {
font-family: "Noto Sans TC", sans-serif;
font-weight: 400;
font-size: 1rem;
color: var(--color-gray-600);
line-height: 1.6;
max-width: 500px;
}
.c4a-button {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--color-enchunblue);
color: white;
padding: 16px 32px;
border-radius: var(--radius, 8px);
font-weight: 600;
font-size: 1rem;
text-decoration: none;
transition: all var(--transition-base, 0.3s ease);
white-space: nowrap;
}
.c4a-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
background-color: var(--color-enchunblue-hover, #1a4d6e);
}
.c4a-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(35, 96, 140, 0.05) 0%, rgba(35, 96, 140, 0.02) 100%);
border-radius: var(--radius-lg, 12px);
z-index: -1;
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.section-call4action {
padding: 60px 16px;
}
.c4a-grid {
grid-template-columns: 1fr;
text-align: center;
}
.c4a-content {
text-align: center;
}
.career-c4a-paragraph {
max-width: 100%;
}
.c4a-button {
width: 100%;
max-width: 300px;
}
}
@media (max-width: 767px) {
.section-call4action {
padding: 40px 16px;
}
.career-c4a-heading {
font-size: 1.5rem;
}
.career-c4a-paragraph {
font-size: 0.95rem;
}
.c4a-button {
padding: 14px 24px;
font-size: 0.95rem;
}
}
</style>

View File

@@ -0,0 +1,345 @@
---
/**
* Portfolio Detail Page - 作品詳情頁
* Displays full project information with live website link
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../../layouts/Layout.astro'
import { fetchPortfolioBySlug, getWebsiteTypeLabel } from '../../lib/api/portfolio'
// Get slug from params
const { slug } = Astro.params
// Validate slug
if (!slug) {
return Astro.redirect('/404')
}
// Fetch portfolio item
const portfolio = await fetchPortfolioBySlug(slug)
// Handle 404 for non-existent portfolio
if (!portfolio) {
return Astro.redirect('/404')
}
// Get website type label
const typeLabel = getWebsiteTypeLabel(portfolio.websiteType)
// Get tags
const tags = portfolio.tags?.map(t => t.tag) || []
// SEO metadata
const title = `${portfolio.title} | 恩群數位案例分享`
const description = portfolio.description || `瀏覽 ${portfolio.title} 專案詳情`
---
<Layout title={title} description={description}>
<article class="portfolio-detail">
<div class="container">
<!-- Back Link -->
<a href="/portfolio" class="back-link">
<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>
<!-- Project Header -->
<header class="project-header">
<div class="project-meta">
<span class="project-type-badge">{typeLabel}</span>
</div>
<h1 class="project-title">{portfolio.title}</h1>
{
portfolio.description && (
<p class="project-description">{portfolio.description}</p>
)
}
<!-- Tags -->
{
tags.length > 0 && (
<div class="project-tags">
{tags.map((tag) => (
<span class="tag">{tag}</span>
))}
</div>
)
}
</header>
<!-- Hero Image -->
{
portfolio.image?.url && (
<div class="project-hero-image">
<img
src={portfolio.image.url}
alt={portfolio.image.alt || portfolio.title}
loading="eager"
width="1200"
height="675"
/>
</div>
)
}
<!-- Project Content -->
<div class="project-content">
<div class="content-section">
<h2>專案介紹</h2>
<p>
此專案展示了我們在{typeLabel}領域的專業能力。
我們致力於為客戶打造符合品牌定位、使用體驗優良的數位產品。
</p>
</div>
<div class="content-section">
<h2>專案連結</h2>
{
portfolio.url ? (
<a
href={portfolio.url}
target="_blank"
rel="noopener noreferrer"
class="btn-primary"
>
前往網站
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
</svg>
</a>
) : (
<p class="text-muted">此專案暫無公開連結</p>
)
}
</div>
</div>
<!-- CTA Section -->
<div class="detail-cta">
<h3>喜歡這個作品嗎?</h3>
<p>讓我們一起為您的品牌打造獨特的數位體驗</p>
<a href="/contact-us" class="cta-button">
聯絡我們
</a>
</div>
</div>
</article>
</Layout>
<style>
/* Portfolio Detail */
.portfolio-detail {
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
/* Back Link */
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 2rem;
color: var(--color-enchunblue, #23608c);
text-decoration: none;
font-weight: 500;
transition: color 0.25s ease;
}
.back-link:hover {
color: var(--color-enchunblue-hover, #1a4d6e);
}
/* Project Header */
.project-header {
text-align: center;
margin-bottom: 3rem;
}
.project-meta {
margin-bottom: 1rem;
}
.project-type-badge {
display: inline-block;
background: var(--color-enchunblue, #23608c);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
.project-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--color-dark-blue, #1a1a1a);
margin-bottom: 1rem;
line-height: 1.3;
}
.project-description {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.125rem;
color: var(--color-gray-600, #666);
max-width: 700px;
margin: 0 auto 1.5rem;
line-height: 1.7;
}
.project-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
}
.tag {
background: var(--color-gray-100, #f5f5f5);
color: var(--color-gray-700, #4a5568);
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
/* Hero Image */
.project-hero-image {
margin-bottom: 3rem;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.project-hero-image img {
width: 100%;
height: auto;
display: block;
}
/* Project Content */
.project-content {
display: grid;
gap: 2.5rem;
margin-bottom: 3rem;
}
.content-section h2 {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.5rem;
font-weight: 600;
color: var(--color-dark-blue, #1a1a1a);
margin-bottom: 1rem;
}
.content-section p {
color: var(--color-gray-600, #666);
line-height: 1.8;
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--color-enchunblue, #23608c);
color: white;
padding: 0.875rem 1.75rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: var(--color-enchunblue-hover, #1a4d6e);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
}
.text-muted {
color: var(--color-gray-500, #999);
}
/* Detail CTA */
.detail-cta {
text-align: center;
padding: 3rem 2rem;
background: var(--color-gray-50, #f9fafb);
border-radius: 16px;
}
.detail-cta h3 {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.5rem;
font-weight: 600;
color: var(--color-enchunblue, #23608c);
margin-bottom: 0.75rem;
}
.detail-cta p {
color: var(--color-gray-600, #666);
margin-bottom: 1.5rem;
}
.cta-button {
display: inline-block;
background: var(--color-enchunblue, #23608c);
color: white;
padding: 14px 28px;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
}
.cta-button:hover {
background: var(--color-enchunblue-hover, #1a4d6e);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.portfolio-detail {
padding: 50px 20px;
}
.project-title {
font-size: 2rem;
}
}
@media (max-width: 767px) {
.portfolio-detail {
padding: 40px 16px;
}
.project-title {
font-size: 1.75rem;
}
.project-description {
font-size: 1rem;
}
.content-section h2 {
font-size: 1.25rem;
}
.detail-cta {
padding: 2rem 1.5rem;
}
.detail-cta h3 {
font-size: 1.25rem;
}
}
</style>

View File

@@ -0,0 +1,244 @@
---
/**
* Portfolio Listing Page - 案例分享列表頁
* Displays all portfolio items in a 2-column grid
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../../layouts/Layout.astro'
import PortfolioCard from '../../components/PortfolioCard.astro'
import { fetchPortfolios } from '../../lib/api/portfolio'
// Metadata for SEO
const title = '案例分享 | 恩群數位行銷'
const description = '瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。'
// Fetch portfolios from Payload CMS
const portfoliosData = await fetchPortfolios(1, 100)
const portfolios = portfoliosData.docs
---
<Layout title={title} description={description}>
<!-- Portfolio Header -->
<section class="portfolio-header" aria-labelledby="portfolio-heading">
<div class="container">
<div class="section_header_w_line">
<div class="divider_line"></div>
<div class="header_subtitle">
<h2 id="portfolio-heading" class="header_subtitle_head">案例分享</h2>
<p class="header_subtitle_paragraph">Our Works</p>
</div>
<div class="divider_line"></div>
</div>
</div>
</section>
<!-- Portfolio Grid -->
<section class="portfolio-grid-section" aria-label="作品列表">
<div class="container">
{
portfolios.length > 0 ? (
<ul class="portfolio-grid">
{
portfolios.map((item) => (
<PortfolioCard
item={{
slug: item.slug,
title: item.title,
description: item.description || '',
image: item.image?.url,
tags: item.tags?.map(t => t.tag) || [],
externalUrl: item.url || undefined,
}}
/>
))
}
</ul>
) : (
<div class="empty-state">
<p class="empty-text">暫無作品資料</p>
</div>
)
}
</div>
</section>
<!-- CTA Section -->
<section class="portfolio-cta" aria-labelledby="cta-heading">
<div class="container">
<h2 id="cta-heading" class="cta-title">
有興趣與我們合作嗎?
</h2>
<p class="cta-description">
讓我們一起為您的品牌打造獨特的數位體驗
</p>
<a href="/contact-us" class="cta-button">
聯絡我們
</a>
</div>
</section>
</Layout>
<style>
/* Portfolio Header */
.portfolio-header {
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.section_header_w_line {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.header_subtitle {
text-align: center;
}
.header_subtitle_head {
color: var(--color-enchunblue, #23608c);
font-family: "Noto Sans TC", sans-serif;
font-weight: 700;
font-size: 2rem;
margin-bottom: 8px;
}
.header_subtitle_paragraph {
color: var(--color-gray-600, #666);
font-family: "Quicksand", "Noto Sans TC", sans-serif;
font-weight: 400;
font-size: 1rem;
}
.divider_line {
width: 40px;
height: 2px;
background-color: var(--color-enchunblue, #23608c);
}
/* Grid Section */
.portfolio-grid-section {
padding: 0 20px 60px;
background-color: #f8f9fa;
}
.portfolio-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
max-width: 1200px;
margin: 0 auto;
padding: 0;
list-style: none;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-text {
font-size: 1.125rem;
color: var(--color-gray-500, #999);
}
/* CTA Section */
.portfolio-cta {
text-align: center;
padding: 80px 20px;
background-color: #ffffff;
}
.cta-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.75rem;
font-weight: 600;
color: var(--color-enchunblue, #23608c);
margin-bottom: 16px;
}
.cta-description {
font-size: 1rem;
color: var(--color-gray-600, #666);
margin-bottom: 32px;
}
.cta-button {
display: inline-block;
background-color: var(--color-enchunblue, #23608c);
color: white;
padding: 16px 32px;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
background-color: var(--color-enchunblue-hover, #1a4d6e);
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.portfolio-header {
padding: 50px 20px;
}
.header_subtitle_head {
font-size: 1.75rem;
}
.portfolio-grid {
gap: 16px;
}
}
@media (max-width: 767px) {
.portfolio-header {
padding: 40px 16px;
}
.header_subtitle_head {
font-size: 1.5rem;
}
.header_subtitle_paragraph {
font-size: 0.9375rem;
}
.portfolio-grid-section {
padding: 0 16px 40px;
}
.portfolio-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.portfolio-cta {
padding: 60px 16px;
}
.cta-title {
font-size: 1.5rem;
}
.cta-description {
font-size: 0.9375rem;
}
.cta-button {
padding: 14px 24px;
font-size: 0.9375rem;
}
}
</style>

View File

@@ -1,52 +1,442 @@
---
import Layout from '../../layouts/Layout.astro';
/**
* Portfolio Detail Page - 案例詳情頁
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../../layouts/Layout.astro'
export async function getStaticPaths() {
// Portfolio slugs from sitemap
const slugs = [
'web-design-project-2',
'web-design-project-3',
'web-design-project-4',
'web-design-project-5'
];
// Get portfolio items
const portfolioItems: Record<string, {
title: string
client?: string
date?: string
category?: string
description: string
images?: string[]
externalUrl?: string
}> = {
'corporate-website-1': {
title: '企業官網設計案例',
client: '知名製造業公司',
date: '2024年1月',
category: '企業官網',
description: `
<p>這是一個為知名製造業公司打造的現代化企業官網專案。客戶希望建立一個能夠有效展示產品系列、公司形象以及最新消息的專業網站。</p>
return slugs.map(slug => ({
params: { slug },
props: { slug }
}));
<h3>專案背景</h3>
<p>客戶原有的網站設計過時,難以在手機設備上正常瀏覽,且內容管理不夠靈活。他們需要一個響應式設計的現代化網站,能夠自動適應各種設備,並方便內容團隊更新。</p>
<h3>解決方案</h3>
<p>我們採用了最新的網頁技術,打造了一個完全響應式的企業官網。網站架構清晰,導航直觀,產品展示頁面設計精美,同時整合了新聞發佈功能,讓客戶能夠快速分享公司動態。</p>
<h3>專案成果</h3>
<p>網站上線後,客戶反映使用者停留時間增加了 40%,詢問量也顯著提升。響應式設計讓行動用戶也能獲得良好的瀏覽體驗,整體滿意度非常高。</p>
`,
images: ['/placeholder-portfolio-1.jpg'],
},
'ecommerce-site-1': {
title: '電商平台建置',
client: '精品品牌',
date: '2024年2月',
category: '電商網站',
description: `
<p>這是一個為精品品牌打造的 B2C 電商網站專案。客戶需要一個功能完整、設計精緻的線上購物平台。</p>
<h3>專案背景</h3>
<p>客戶希望拓展線上銷售渠道,建立一個能夠完整呈現品牌調性的電商網站。需求包含會員系統、購物車、金流整合、庫存管理等完整功能。</p>
<h3>解決方案</h3>
<p>我們設計了一個簡潔優雅的電商平台,整合了完整的購物流程。從商品瀏覽、加入購物車、結帳到訂單追蹤,整個流程流暢無縫。同時整合了主流金流與物流服務。</p>
<h3>專案成果</h3>
<p>網站上線後首月即達到預期銷售目標,客戶反應購物體驗流暢,設計風格也獲得用戶高度評價。</p>
`,
images: ['/placeholder-portfolio-2.jpg'],
},
'brand-website-1': {
title: '品牌形象網站',
client: '新創品牌',
date: '2024年3月',
category: '品牌網站',
description: `
<p>這是一個為新創品牌打造的以視覺故事為核心的品牌網站專案。</p>
<h3>專案背景</h3>
<p>客戶是一個新創品牌,需要一個能夠有效傳達品牌故事、價值理念的網站。他們希望網站不只是資訊展示,更能成為品牌與用戶情感連結的橋樑。</p>
<h3>解決方案</h3>
<p>我們以視覺故事為設計核心,運用大型視覺元素、動畫效果和互動設計,打造了一個充滿故事性的品牌網站。每一個區塊都精心設計,引導用戶深入了解品牌。</p>
<h3>專案成果</h3>
<p>網站成功建立了強烈的品牌印象,訪客平均停留時間超過 3 分鐘,品牌認知度顯著提升。</p>
`,
images: ['/placeholder-portfolio-3.jpg'],
},
'landing-page-1': {
title: '活動行銷頁面',
client: '活動主辦單位',
date: '2024年4月',
category: '活動頁面',
description: `
<p>這是一個為大型活動設計的高轉換率行銷頁面專案。</p>
<h3>專案背景</h3>
<p>客戶需要一個能夠有效吸引報名、轉換率高的活動頁面。頁面需要能夠清楚傳達活動資訊、吸引目光,並引導用戶完成報名流程。</p>
<h3>解決方案</h3>
<p>我們設計了一個視覺衝擊力強的活動頁面CTA 按鈕配置經過精心規劃,報名流程簡化到最少步驟。同時加入倒數計時、名額顯示等促進轉換的元素。</p>
<h3>專案成果</h3>
<p>頁面上線後,報名轉換率達到 15%,遠超過預期目標,活動順利達到滿檔狀態。</p>
`,
images: ['/placeholder-portfolio-4.jpg'],
},
}
const { slug } = Astro.props;
export async function getStaticPaths() {
const slugs = Object.keys(portfolioItems)
// Placeholder content
const project = {
title: 'Web Design Project',
description: 'Project description...',
images: []
};
return slugs.map((slug) => ({
params: { slug },
props: { slug }
}))
}
const { slug } = Astro.props
const project = portfolioItems[slug] || {
title: '作品詳情',
description: '<p>暫無內容</p>',
images: ['/placeholder-portfolio.jpg'],
}
// Metadata for SEO
const title = `${project.title} | 恩群數位案例`
const description = project.description.replace(/<[^>]*>/g, '').slice(0, 160)
---
<Layout>
<section class="project-section">
<Layout title={title} description={description}>
<!-- Breadcrumb -->
<nav class="breadcrumb" aria-label="麵包屑導航">
<div class="container">
<h1>{project.title}</h1>
<p>{project.description}</p>
<!-- Images and details -->
<ol class="breadcrumb-list">
<li><a href="/">首頁</a></li>
<li><a href="/website-portfolio">案例分享</a></li>
<li aria-current="page">{project.title}</li>
</ol>
</div>
</section>
</nav>
<!-- Portfolio Detail -->
<article class="portfolio-detail">
<div class="container">
<!-- Header -->
<header class="portfolio-detail-header">
<h1 class="portfolio-detail-title">{project.title}</h1>
<div class="portfolio-detail-meta">
{
project.client && (
<span class="meta-item">
<strong>客戶:</strong>{project.client}
</span>
)
}
{
project.date && (
<span class="meta-item">
<strong>日期:</strong>{project.date}
</span>
)
}
{
project.category && (
<span class="meta-item">
<strong>類別:</strong>{project.category}
</span>
)
}
</div>
</header>
<!-- Featured Image -->
{
project.images && project.images.length > 0 && (
<div class="portfolio-detail-image-wrapper">
<img
src={project.images[0]}
alt={project.title}
class="portfolio-detail-image"
loading="eager"
width="1000"
height="562"
/>
</div>
)
}
<!-- Description -->
<div class="portfolio-detail-content">
<div class="description-wrapper" set:html={project.description} />
</div>
<!-- CTA -->
<div class="portfolio-detail-cta">
<h3 class="cta-heading">有專案想要討論嗎?</h3>
<p class="cta-subheading">我們很樂意聽聽您的需求</p>
<a href="/contact-us" class="cta-button">
聯絡我們
</a>
</div>
<!-- Related Projects -->
<div class="related-projects">
<h3 class="related-title">更多案例</h3>
<a href="/website-portfolio" class="view-all-link">
查看全部案例 →
</a>
</div>
</div>
</article>
</Layout>
<style>
.project-section {
padding: 40px 0;
/* Portfolio Detail Styles - Pixel-perfect from Webflow */
/* Breadcrumb */
.breadcrumb {
padding: 16px 20px;
background-color: #f8f9fa;
border-bottom: 1px solid #e5e7eb;
}
.breadcrumb-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 1200px;
margin: 0 auto;
padding: 0;
list-style: none;
font-size: 0.875rem;
color: var(--color-gray-600);
}
.breadcrumb-list a {
color: var(--color-enchunblue);
text-decoration: none;
}
.breadcrumb-list a:hover {
text-decoration: underline;
}
.breadcrumb-list li:not(:last-child)::after {
content: '/';
margin-left: 8px;
color: var(--color-gray-400);
}
/* Detail Section */
.portfolio-detail {
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
/* Header */
.portfolio-detail-header {
margin-bottom: 40px;
text-align: center;
margin-bottom: 30px;
}
</style>
.portfolio-detail-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--color-tarawera, #2d3748);
margin-bottom: 24px;
line-height: 1.2;
}
.portfolio-detail-meta {
display: flex;
justify-content: center;
gap: 24px;
font-size: 0.875rem;
color: var(--color-gray-600);
flex-wrap: wrap;
}
.meta-item strong {
color: var(--color-text-primary);
}
/* Featured Image */
.portfolio-detail-image-wrapper {
margin-bottom: 40px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.portfolio-detail-image {
width: 100%;
height: auto;
display: block;
}
/* Content */
.portfolio-detail-content {
margin-bottom: 60px;
}
.description-wrapper {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.125rem;
line-height: 1.8;
color: var(--color-text-primary);
}
.description-wrapper :global(p) {
margin-bottom: 20px;
}
.description-wrapper :global(h3) {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-enchunblue);
margin-top: 32px;
margin-bottom: 16px;
}
/* CTA Section */
.portfolio-detail-cta {
text-align: center;
padding: 60px;
background: linear-gradient(135deg, rgba(35, 96, 140, 0.05) 0%, rgba(35, 96, 140, 0.02) 100%);
border-radius: 12px;
margin-bottom: 60px;
}
.cta-heading {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.75rem;
font-weight: 600;
color: var(--color-enchunblue);
margin-bottom: 8px;
}
.cta-subheading {
font-size: 1rem;
color: var(--color-gray-600);
margin-bottom: 24px;
}
.cta-button {
display: inline-block;
background-color: var(--color-enchunblue);
color: white;
padding: 16px 32px;
border-radius: var(--radius, 8px);
font-weight: 600;
text-decoration: none;
transition: all var(--transition-base, 0.3s ease);
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
background-color: var(--color-enchunblue-hover, #1a4d6e);
}
/* Related Projects */
.related-projects {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 32px;
border-top: 1px solid var(--color-border, #e5e7eb);
}
.related-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
}
.view-all-link {
color: var(--color-enchunblue);
text-decoration: none;
font-weight: 500;
transition: color var(--transition-fast, 0.2s ease);
}
.view-all-link:hover {
color: var(--color-enchunblue-hover, #1a4d6e);
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.portfolio-detail {
padding: 50px 16px;
}
.portfolio-detail-title {
font-size: 2rem;
}
.portfolio-detail-cta {
padding: 40px;
}
.cta-heading {
font-size: 1.5rem;
}
}
@media (max-width: 767px) {
.breadcrumb {
padding: 12px 16px;
}
.breadcrumb-list {
font-size: 0.8125rem;
}
.portfolio-detail {
padding: 40px 16px;
}
.portfolio-detail-title {
font-size: 1.75rem;
}
.portfolio-detail-meta {
flex-direction: column;
gap: 8px;
}
.portfolio-detail-cta {
padding: 32px 20px;
}
.related-projects {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.description-wrapper {
font-size: 1rem;
}
.description-wrapper :global(h3) {
font-size: 1.25rem;
}
}
</style>

View File

@@ -1,57 +1,225 @@
---
import Layout from '../layouts/Layout.astro';
/**
* Portfolio Listing Page - 案例分享列表頁
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../layouts/Layout.astro'
import PortfolioCard from '../components/PortfolioCard.astro'
// Placeholder portfolios
const portfolios = [
{ slug: 'web-design-project-2', title: 'Project 2', description: 'Description...' },
// Add more
];
// Metadata for SEO
const title = '案例分享 | 恩群數位行銷'
const description = '瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。'
// Portfolio items - can be fetched from Payload CMS
const portfolioItems = [
{
slug: 'corporate-website-1',
title: '企業官網設計案例',
description: '為知名製造業打造的現代化企業官網,整合產品展示與新聞發佈功能。',
image: '/placeholder-portfolio-1.jpg',
tags: ['企業官網', '響應式設計'],
},
{
slug: 'ecommerce-site-1',
title: '電商平台建置',
description: 'B2C 電商網站,包含會員系統、購物車、金流整合等完整功能。',
image: '/placeholder-portfolio-2.jpg',
tags: ['電商網站', '金流整合'],
},
{
slug: 'brand-website-1',
title: '品牌形象網站',
description: '以視覺故事為核心的品牌網站,展現品牌獨特價值與理念。',
image: '/placeholder-portfolio-3.jpg',
tags: ['品牌網站', '視覺設計'],
},
{
slug: 'landing-page-1',
title: '活動行銷頁面',
description: '高轉換率的活動頁面設計,有效的 CTA 配置與使用者體驗規劃。',
image: '/placeholder-portfolio-4.jpg',
tags: ['活動頁面', '行銷'],
},
]
---
<Layout>
<section class="portfolio-section">
<Layout title={title} description={description}>
<!-- Portfolio Header -->
<section class="portfolio-header" aria-labelledby="portfolio-heading">
<div class="container">
<h1>網站設計作品</h1>
<div class="portfolio-grid">
{portfolios.map(item => (
<div class="portfolio-item">
<h2><a href={`/webdesign-profolio/${item.slug}`}>{item.title}</a></h2>
<p>{item.description}</p>
</div>
))}
</div>
<h1 id="portfolio-heading" class="portfolio-title">案例分享</h1>
<p class="portfolio-subtitle">Selected Works</p>
<div class="divider-line"></div>
<div class="divider-line"></div>
</div>
</section>
<!-- Portfolio Grid -->
<section class="portfolio-grid-section" aria-label="作品列表">
<ul class="portfolio-grid">
{
portfolioItems.map((item) => (
<PortfolioCard item={item} />
))
}
</ul>
</section>
<!-- CTA Section -->
<section class="portfolio-cta" aria-labelledby="cta-heading">
<div class="container">
<h2 id="cta-heading" class="cta-title">
有興趣與我們合作嗎?
</h2>
<p class="cta-description">
讓我們一起為您的品牌打造獨特的數位體驗
</p>
<a href="/contact-us" class="cta-button">
聯絡我們
</a>
</div>
</section>
</Layout>
<style>
.portfolio-section {
padding: 40px 0;
/* Portfolio Page Styles - Pixel-perfect from Webflow */
/* Header Section */
.portfolio-header {
text-align: center;
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
.portfolio-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--color-enchunblue);
margin-bottom: 8px;
}
.portfolio-subtitle {
font-family: "Quicksand", "Noto Sans TC", sans-serif;
font-size: 1.125rem;
color: var(--color-gray-700);
margin-bottom: 24px;
}
.divider-line {
width: 100px;
height: 2px;
background-color: var(--color-enchunblue);
margin: 0 auto;
}
.divider-line:last-child {
width: 60px;
margin-top: 4px;
}
/* Grid Section */
.portfolio-grid-section {
padding: 0 20px 60px;
background-color: #f8f9fa;
}
.portfolio-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-template-columns: repeat(2, 1fr);
gap: 20px;
max-width: 1200px;
margin: 0 auto;
padding: 0;
list-style: none;
}
.portfolio-item {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
/* CTA Section */
.portfolio-cta {
text-align: center;
padding: 80px 20px;
background-color: #ffffff;
}
.portfolio-item a {
.cta-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.75rem;
font-weight: 600;
color: var(--color-enchunblue);
margin-bottom: 16px;
}
.cta-description {
font-size: 1rem;
color: var(--color-gray-600);
margin-bottom: 32px;
}
.cta-button {
display: inline-block;
background-color: var(--color-enchunblue);
color: white;
padding: 16px 32px;
border-radius: var(--radius, 8px);
font-weight: 600;
text-decoration: none;
color: #333;
transition: all var(--transition-base, 0.3s ease);
}
.portfolio-item a:hover {
color: #007bff;
.cta-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
background-color: var(--color-enchunblue-hover, #1a4d6e);
}
</style>
/* Responsive Adjustments */
@media (max-width: 991px) {
.portfolio-header {
padding: 50px 20px;
}
.portfolio-title {
font-size: 2rem;
}
.portfolio-grid {
gap: 16px;
}
}
@media (max-width: 767px) {
.portfolio-header {
padding: 40px 16px;
}
.portfolio-title {
font-size: 1.75rem;
}
.portfolio-subtitle {
font-size: 1rem;
}
.portfolio-grid-section {
padding: 0 16px 40px;
}
.portfolio-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.portfolio-cta {
padding: 60px 16px;
}
.cta-title {
font-size: 1.5rem;
}
}
</style>

View File

@@ -1,79 +1,233 @@
---
import Layout from '../../layouts/Layout.astro';
/**
* Category Page - 文章分類
* URL: /wen-zhang-fen-lei/[slug]
* Displays posts filtered by category from Payload CMS
*/
import Layout from '../../layouts/Layout.astro'
import ArticleCard from '../../components/blog/ArticleCard.astro'
import { fetchCategoryBySlug, fetchPosts, formatDate } from '../../lib/api/blog'
export async function getStaticPaths() {
// Category slugs
const slugs = [
'en-qun-shu-wei-zui-xin-gong-gao',
'xing-xiao-shi-shi-zui-qian-xian',
'meta-xiao-xue-tang',
'google-xiao-xue-tang'
];
const { slug } = Astro.params
return slugs.map(slug => ({
params: { slug },
props: { slug }
}));
// Fetch category by slug
const category = await fetchCategoryBySlug(slug)
// Handle 404 for non-existent category
if (!category) {
return Astro.redirect('/404')
}
const { slug } = Astro.props;
// Fetch posts for this category (limit 100 for category pages)
const PAGE_SIZE = 100
const postsData = await fetchPosts(1, PAGE_SIZE, slug)
const posts = postsData.docs
// Placeholder - would fetch category and posts from CMS
const category = {
name: 'Category Name',
posts: [
{ slug: 'post1', title: 'Post 1', date: '2023-01-01' }
]
};
// Format date function
const formatDateTW = (dateStr: string | null | undefined): string => {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('zh-TW', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
---
<Layout>
<section class="category-section">
<Layout title={category.title}>
<!-- Category Header -->
<section class="category-hero" aria-labelledby="category-heading">
<div class="container">
<h1>{category.name}</h1>
<div class="posts-list">
{category.posts.map(post => (
<article class="post-item">
<h2><a href={`/xing-xiao-fang-da-jing/${post.slug}`}>{post.title}</a></h2>
<p>{post.date}</p>
</article>
))}
</div>
<a href="/news" class="back-link">← 回到文章列表</a>
<h1 id="category-heading" class="category-title">{category.title}</h1>
{
category.nameEn && (
<p class="category-subtitle">{category.nameEn}</p>
)
}
</div>
</section>
<!-- Category Posts -->
<section class="category-posts">
<div class="container">
{
posts.length > 0 ? (
<div class="posts-grid">
{posts.map((post) => (
<article class="post-item">
<a href={`/xing-xiao-fang-da-jing/${post.slug}`} class="post-link">
{
post.heroImage?.url && (
<div class="post-image">
<img
src={post.heroImage.url}
alt={post.heroImage.alt || post.title}
loading="lazy"
/>
</div>
)
}
<div class="post-content">
<h3 class="post-title">{post.title}</h3>
<time class="post-date">{formatDateTW(post.publishedAt)}</time>
{
post.excerpt && (
<p class="post-excerpt">{post.excerpt.slice(0, 120)}...</p>
)
}
</div>
</a>
</article>
))}
</div>
) : (
<div class="empty-state">
<p>此分類暫無文章</p>
</div>
)
}
</div>
</section>
</Layout>
<style>
.category-section {
padding: 40px 0;
.category-hero {
padding: 60px 20px 40px;
text-align: center;
background-color: #ffffff;
}
.container {
max-width: 1000px;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
.posts-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.post-item {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
}
.post-item h2 {
margin-top: 0;
}
.post-item a {
.back-link {
display: inline-block;
margin-bottom: 20px;
color: var(--color-enchunblue, #23608c);
text-decoration: none;
color: #333;
font-weight: 500;
}
.post-item a:hover {
color: #007bff;
.category-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 2rem;
font-weight: 700;
color: var(--color-dark-blue, #1a1a1a);
margin-bottom: 8px;
}
</style>
.category-subtitle {
font-family: "Quicksand", "Noto Sans TC", sans-serif;
font-size: 1rem;
color: var(--color-gray-600, #666);
}
.category-posts {
padding: 40px 20px 60px;
background-color: #f8f9fa;
min-height: 60vh;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.post-item {
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s ease;
}
.post-item:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.post-link {
display: block;
text-decoration: none;
color: inherit;
}
.post-image {
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--color-gray-100, #f5f5f5);
}
.post-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.post-content {
padding: 20px;
}
.post-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-dark-blue, #1a1a1a);
margin-bottom: 12px;
line-height: 1.4;
}
.post-date {
font-size: 0.875rem;
color: var(--color-gray-500, #999);
margin-bottom: 12px;
}
.post-excerpt {
font-size: 0.9375rem;
color: var(--color-gray-600, #666);
line-height: 1.6;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: var(--color-gray-500, #999);
}
/* Responsive */
@media (max-width: 991px) {
.posts-grid {
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.category-title {
font-size: 1.75rem;
}
}
@media (max-width: 767px) {
.category-hero {
padding: 40px 16px 30px;
}
.category-title {
font-size: 1.5rem;
}
.posts-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.category-posts {
padding: 30px 16px 50px;
}
}
</style>

View File

@@ -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>