refactor: remove EmDash CMS, hardcode static page content

- Remove EmDash: uninstalled emdash + @emdash-cms/* packages
- Delete src/utils/site-identity.ts (only file importing emdash)
- Delete src/live.config.ts (emdash stub)
- Delete src/content/pages/ (6 MD files: home/about/services/contact/faq/portfolio)
- Remove 'pages' collection from src/content.config.ts
- Hardcode home content as constants in src/pages/index.astro
- Fix duplicate 'megaphone' key in icon-paths.ts (pre-existing bug blocking build)

Home page redesign:
- New hero policy: 'เพิ่มยอดขาย ลดต้นทุน ลดเวลา' (replaces stats claim)
- Remove stats section (no claims until validated)
- 12 problem cards in 4 service buckets (3 each), outline-badge Lucide icons
- New pull quote: 'กำไรที่มากขึ้นของลูกค้า'
- Fix portfolio preview bug (was loading blog collection, now loads portfolio)

About page:
- Sync hero with new policy
- New 'นโยบายของเรา' section in story
- New dark pull-quote band with mission statement

Hero component:
- Remove hard-coded trust strip (50+/40+/5+/100% stats)
- Remove hard-coded secondary CTA
- Replace with named slots so callers can opt in

CLAUDE.md: updated to reflect new architecture (no CMS, static-only)

Verified: npm run build clean (18 pages, 1.74s)
This commit is contained in:
Macky
2026-06-05 14:09:42 +07:00
parent 892c75b7a6
commit 3f1c0061c7
14 changed files with 290 additions and 5145 deletions

View File

@@ -5,16 +5,20 @@ import Footer from '../components/Footer.astro';
import Icon from '../components/Icon.astro';
import Hero from '../components/Hero.astro';
import { getCollection } from 'astro:content';
import { render } from 'astro:content';
import ServiceCard from '../components/ServiceCard.astro';
import PortfolioCard from '../components/PortfolioCard.astro';
const pages = await getCollection('pages');
const home = pages.find(p => p.slug === 'home');
const { Content } = home ? await render(home) : { Content: null };
// Hardcoded home page copy (previously in src/content/pages/home.md, now inlined)
const homeContent = {
badge: 'AI AGENCY ในประเทศไทย',
title: 'เพิ่มยอดขาย ลดต้นทุน ลดเวลา — ให้ธุรกิจของคุณ',
subtitle:
'นโยบายของเราคือสร้างระบบที่ทำให้ลูกค้ามีกำไรมากขึ้น — ด้วยเว็บไซต์ SEO AI Chatbot และ Marketing Automation ที่ออกแบบมาเพื่อ SME ไทยโดยเฉพาะ',
};
const services = await getCollection('services');
const blogPosts = await getCollection('blog');
const sortedPosts = blogPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const portfolio = await getCollection('portfolio');
const featuredPortfolio = portfolio.slice(0, 4);
// 4 mega-services (use existing service collection filtered)
const allServices = services.slice(0, 4);
@@ -24,42 +28,130 @@ const serviceImages: Record<string, string> = {
'marketing': '/images/services/marketing.jpg',
'webdev': '/images/services/webdev.jpg',
};
// 12 problem cards grouped by 4 service buckets (3 per bucket).
// Each card: serviceBucket, icon (Lucide), title, description, result.
const problemCards = [
// 🏢 Website — load/SEO/visibility
{
bucket: 'website',
icon: 'globe',
title: 'เว็บไซต์โหลดช้า ลูกค้าปิดก่อนเห็นสินค้า',
description: 'หน้าเว็บใช้เวลาโหลดเกิน 5 วินาที ลูกค้า 53% ปิดทิ้งทันที',
result: '→ เสียโอกาสขายตั้งแต่วินาทีแรก',
},
{
bucket: 'website',
icon: 'search',
title: 'อยู่หน้า 5 ของ Google หรือหายไปเลย',
description: 'ลูกค้าค้นหาแล้วไม่เจอเว็บคุณ เจอแต่คู่แข่ง',
result: '→ ต้องจ่ายค่าโฆษณาเพิ่มทุกเดือน',
},
{
bucket: 'website',
icon: 'shoppingCart',
title: 'เว็บมีอยู่ แต่ลูกค้าซื้อไม่ได้',
description: '<27>อร์มไม่ส่ง ตะกร้าค้าง ชำระเงินไม่ผ่าน',
result: '→ ยอดขายตกทั้งที่คนเข้าเว็บเยอะ',
},
// ⚙️ AI Automation — operations / efficiency
{
bucket: 'automation',
icon: 'message',
title: 'ทีมเซลล์ตอบแชตไม่ทัน ลูกค้าหายตอนกลางคืน',
description: 'ทีม 12 คนตอบไม่ไหว ลูกค้ารอ 5 นาทีแล้วไปซื้อที่อื่น',
result: '→ ยอดหายโดยไม่มีใครรู้ตัว',
},
{
bucket: 'automation',
icon: 'clipboard',
title: 'งานซ้ำ ๆ ใช้เวลาคนเป็นชั่วโมงทุกวัน',
description: 'คีย์ข้อมูล ทำใบเสนอราคา อัปเดตสต็อก ทุกอย่างทำมือ',
result: '→ ต้นทุนค่าแรงสูงขึ้นโดยไม่จำเป็น',
},
{
bucket: 'automation',
icon: 'cog',
title: 'ระบบแยกกัน ไม่คุยกัน',
description: 'CRM · ERP · ระบบบัญชี · หน้าร้าน ต่างคนต่างอยู่',
result: '→ ตัดสินใจช้า เพราะข้อมูลไม่เชื่อม',
},
// 📈 SEO + Content — traffic / leads
{
bucket: 'marketing',
icon: 'trendingDown',
title: 'ลงโฆษณา แต่ยอดขายไม่ขยับ',
description: 'คลิกเยอะ แต่ไม่มีใครซื้อ — ไม่มี Funnel ไม่มี Lead scoring',
result: '→ เงินหายไปกับคลิกที่ไม่มีคุณภาพ',
},
{
bucket: 'marketing',
icon: 'pen',
title: 'เขียนคอนเทนต์เองไม่ทัน',
description: 'อยากโพสต์สม่ำเสมอ แต่ทีมไม่มีเวลา',
result: '→ คอนเทนต์หยุดชะงัก ลูกค้าลืม',
},
{
bucket: 'marketing',
icon: 'megaphone',
title: 'ไม่รู้ว่าใครซื้อ ใครแค่ทักมาถามเล่น',
description: 'ไม่มีระบบติดตาม ไม่มี CRM ไม่มี Report',
result: '→ ลงทุนการตลาดแบบเดา',
},
// 🖥️ Tech Consult — infrastructure / scale
{
bucket: 'tech',
icon: 'server',
title: 'ไม่อยากจ้างทีม IT แต่ต้องมี Server',
description: 'ระบบหลังบ้านต้องทำงาน แต่จ้างประจำแพงเกิน',
result: '→ ต้นทุนคงที่สูงโดยไม่คุ้ม',
},
{
bucket: 'tech',
icon: 'shield',
title: 'กังวลเรื่อง PDPA / ข้อมูลรั่วไหล',
description: 'เก็บข้อมูลลูกค้าแต่ไม่รู้ว่าปลอดภัยแค่ไหน',
result: '→ เสี่ยงถูกฟ้องร้อง ถูกปรับ',
},
{
bucket: 'tech',
icon: 'monitor',
title: 'อยากใช้ AI แต่ไม่รู้จะเริ่มยังไง',
description: 'มี ChatGPT ใช้ แต่เอามาทำงานจริงในธุรกิจไม่เป็น',
result: '→ เสียโอกาสที่ AI จะช่วยลดงานได้',
},
];
const bucketLabels: Record<string, string> = {
website: 'Website',
automation: 'AI Automation',
marketing: 'SEO + Content',
tech: 'Tech Consult',
};
// Pre-group problem cards by bucket (4 groups, in canonical service order)
const problemGroupOrder = ['website', 'automation', 'marketing', 'tech'] as const;
const problemGroups = problemGroupOrder.map((bucket) => ({
bucket,
label: bucketLabels[bucket],
cards: problemCards.filter((c) => c.bucket === bucket),
}));
---
<Base title="MoreminiMore - รับทำเว็บไซต์ SEO AI Chatbot">
<Navigation />
<Hero
badge={home?.data.badge}
title={home?.data.title}
subtitle={home?.data.subtitle}
/>
badge={homeContent.badge}
title={homeContent.title}
subtitle={homeContent.subtitle}
>
<a slot="hero-cta-secondary" href="/portfolio" class="btn btn-outline-dark">
ดูผลงานจริง
</a>
</Hero>
<!-- STATS SECTION — yellow band (counters animate on view) -->
<section class="section section-stats reveal">
<div class="container">
<div class="stats-grid">
<div class="stat-item">
<span class="stat-number counter" data-from="0">50+</span>
<span class="stat-label">โปรเจกต์สำเร็จ</span>
</div>
<div class="stat-item">
<span class="stat-number counter" data-from="0">40+</span>
<span class="stat-label">ลูกค้าที่ไว้วางใจ</span>
</div>
<div class="stat-item">
<span class="stat-number counter" data-from="0">5+</span>
<span class="stat-label">ปีประสบการณ์</span>
</div>
<div class="stat-item">
<span class="stat-number counter" data-from="0">24/7</span>
<span class="stat-label">ให้บริการ</span>
</div>
</div>
</div>
</section>
<!-- PROBLEM SECTION — light, 3 cards (stagger-children) -->
<!-- PROBLEM SECTION — 12 pain cards, 3 per service bucket (light) -->
<section class="section section-soft">
<div class="container">
<div class="section-header reveal">
@@ -67,34 +159,28 @@ const serviceImages: Record<string, string> = {
<h2 class="section-title">
คุณกำลังเจอ <span class="highlight">แบบนี้อยู่</span> ใช่ไหม?
</h2>
<p class="section-desc">12 ปัญหาที่แบ่งตามบริการของเรา เลือกดูได้เลยว่าตรงกับคุณข้อไหน</p>
</div>
<div class="problem-grid stagger-children">
<div class="problem-card">
<div class="problem-icon">
<Icon name="message" />
{problemGroups.map((group) => (
<div class="problem-group reveal">
<h3 class="problem-group-title">
<span class="problem-group-tag">{group.label}</span>
</h3>
<div class="problem-grid">
{group.cards.map((card) => (
<div class="problem-card">
<div class="problem-icon-badge">
<Icon name={card.icon} size={28} color="dark" />
</div>
<h4 class="problem-title">{card.title}</h4>
<p class="problem-desc">{card.description}</p>
<span class="problem-result">{card.result}</span>
</div>
))}
</div>
<h3 class="problem-title">ลูกค้าทัก LINE เข้ามา แต่ตอบไม่ทัน</h3>
<p class="problem-desc">ทีมเซลล์ 12 คน ตอบแชตไม่ไหว ลูกค้ารอ 5 นาทีแล้วไปซื้อที่อื่น</p>
<span class="problem-result">→ ยอดหายก่อนที่คุณจะเห็น</span>
</div>
<div class="problem-card">
<div class="problem-icon">
<Icon name="trendingDown" />
</div>
<h3 class="problem-title">ลงโฆษณา แต่ยอดขายไม่ขยับ</h3>
<p class="problem-desc">ไม่มีระบบ Lead ไม่มี Funnel ไม่รู้ว่าใครซื้อ ใครแค่ทักมาถามเล่น</p>
<span class="problem-result">→ เงินหายไปกับคลิกที่ไม่มีคุณภาพ</span>
</div>
<div class="problem-card">
<div class="problem-icon">
<Icon name="globe" />
</div>
<h3 class="problem-title">เว็บไซต์มีอยู่ แต่ไม่มีใครเจอใน Google</h3>
<p class="problem-desc">อันดับหน้า 5 ไม่มีใครเปิด ขณะที่คู่แข่งติดหน้าแรกจาก SEO อย่างเดียว</p>
<span class="problem-result">→ เสียเงินฟรีทุกเดือน</span>
</div>
</div>
))}
<p class="problem-closing reveal">
<span class="highlight">ทุกปัญหาข้างต้นแก้ได้ด้วยระบบเดียว</span> — ดูว่าเราทำยังไง
@@ -139,14 +225,14 @@ const serviceImages: Record<string, string> = {
<div class="container">
<blockquote class="pull-quote">
<p class="quote-text">
"เราไม่ได้ขายเว็บไซต์ — เราสร้างระบบที่<span class="highlight">ทำเงินให้ธุรกิจคุณ</span>"
"เป้าหมายของเราคือ <span class="highlight">กำไรที่มากขึ้นของลูกค้า</span>"
</p>
<cite class="quote-author">— มอร์มินิมอร์, ปรัชญาการทำงาน</cite>
<cite class="quote-author">— มอร์มินิมอร์, ปณิธานการทำงาน</cite>
</blockquote>
</div>
</section>
<!-- PORTFOLIO PREVIEW — 3 latest on soft -->
<!-- PORTFOLIO PREVIEW — 4 featured on soft -->
<section class="section section-soft">
<div class="container">
<div class="section-header reveal">
@@ -154,45 +240,27 @@ const serviceImages: Record<string, string> = {
<h2 class="section-title">
ลูกค้าจริง <span class="highlight">ผลลัพธ์จริง</span>
</h2>
<p class="section-desc">9 โปรเจกต์ที่เราภาคภูมิใจ — ตั้งแต่ร้านค้าออนไลน์ ไปจนถึงโรงงานฉีดพลาสติก</p>
<p class="section-desc">ตัวอย่างผลงานที่เราภาคภูมิใจ — คลิกดูเว็บจริงได้เลย</p>
</div>
<div class="blog-editorial reveal">
{sortedPosts[0] && (
<a href={`/blog/${sortedPosts[0].id}`} class="blog-featured">
<div class="blog-image">
<img src={sortedPosts[0].data.image} alt={sortedPosts[0].data.title} loading="lazy" />
<span class="blog-badge">บทความล่าสุด</span>
</div>
<div class="blog-content">
<span class="blog-category">{sortedPosts[0].data.category}</span>
<h3 class="blog-title">{sortedPosts[0].data.title}</h3>
<p class="blog-excerpt">{sortedPosts[0].data.excerpt}</p>
<span class="read-more">อ่านต่อ →</span>
</div>
</a>
)}
<div class="blog-sidebar">
{sortedPosts.slice(1, 4).map((post) => (
<a href={`/blog/${post.id}`} class="blog-card-mini">
<div class="blog-card-image">
<img src={post.data.image} alt={post.data.title} loading="lazy" />
</div>
<div class="blog-card-content">
<span class="blog-category-small">{post.data.category}</span>
<h4 class="blog-title-small">{post.data.title}</h4>
<span class="blog-date-small">
{new Date(post.data.date).toLocaleDateString('th-TH', { month: 'short', day: 'numeric' })}
</span>
</div>
</a>
))}
</div>
<div class="portfolio-preview-grid stagger-children">
{featuredPortfolio.map((item) => (
<PortfolioCard
name={item.data.name}
url={item.data.url}
category={item.data.category}
category_label={item.data.category_label}
industry={item.data.industry}
thumbnail={item.data.thumbnail}
description={item.data.description}
what_we_did={item.data.what_we_did}
result={item.data.result}
/>
))}
</div>
<div class="section-cta">
<a href="/blog" class="btn btn-dark btn-lg">บทความทั้งหมด →</a>
<a href="/portfolio" class="btn btn-dark btn-lg">ดูผลงานทั้งหมด →</a>
</div>
</div>
</section>
@@ -223,39 +291,6 @@ const serviceImages: Record<string, string> = {
</Base>
<style>
/* ============================================
STATS — yellow band
============================================ */
.section-stats {
background: var(--color-primary);
padding: 60px 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-number {
display: block;
font-family: var(--font-display);
font-size: clamp(48px, 7vw, 80px);
font-weight: 900;
color: var(--color-black);
line-height: 1;
}
.stat-label {
display: block;
font-size: 14px;
color: rgba(0, 0, 0, 0.7);
margin-top: 8px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
}
/* ============================================
SECTION HEADER (light)
============================================ */
@@ -305,49 +340,96 @@ const serviceImages: Record<string, string> = {
}
/* ============================================
PROBLEM CARDS
PROBLEM CARDS — 12 cards, 3 per service bucket
============================================ */
.section-soft { background: var(--color-bg-alt); }
.problem-group {
margin-bottom: 56px;
}
.problem-group:last-of-type {
margin-bottom: 0;
}
.problem-group-title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.problem-group-tag {
display: inline-block;
font-family: var(--font-display);
font-size: 13px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--color-black);
background: var(--color-primary);
padding: 6px 14px;
border-radius: var(--radius-sm);
}
.problem-group-title::after {
content: '';
flex: 1;
height: 2px;
background: var(--color-primary);
opacity: 0.4;
}
.problem-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
gap: 20px;
}
.problem-card {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
padding: 32px;
padding: 28px 24px;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.problem-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary);
}
.problem-icon { margin-bottom: 16px; }
.problem-icon-badge {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--color-primary);
border: 2px solid var(--color-black);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.problem-title {
font-family: var(--font-display);
font-size: 20px;
font-size: 17px;
font-weight: 800;
color: var(--color-black);
margin-bottom: 12px;
margin-bottom: 10px;
line-height: 1.35;
}
.problem-desc {
font-size: 15px;
font-size: 14px;
color: var(--color-gray-600);
line-height: 1.6;
margin-bottom: 16px;
margin-bottom: 14px;
flex: 1;
}
.problem-result {
display: block;
font-size: 14px;
font-size: 13px;
font-weight: 700;
color: var(--color-primary-dark);
}
.problem-closing {
text-align: center;
margin-top: 40px;
margin-top: 48px;
font-size: 18px;
color: var(--color-gray-700);
}
@@ -454,12 +536,12 @@ const serviceImages: Record<string, string> = {
}
.section-dark-quote .pull-quote {
text-align: center;
max-width: 900px;
max-width: 1000px;
margin: 0 auto;
}
.quote-text {
font-family: var(--font-display);
font-size: clamp(28px, 4vw, 48px);
font-size: clamp(28px, 4.5vw, 56px);
font-weight: 800;
line-height: 1.3;
color: var(--color-white);
@@ -477,170 +559,39 @@ const serviceImages: Record<string, string> = {
}
/* ============================================
BLOG EDITORIAL PREVIEW
PORTFOLIO PREVIEW GRID — 2x2 on desktop
============================================ */
.blog-editorial {
.portfolio-preview-grid {
display: grid;
grid-template-columns: 1.5fr 1fr;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.blog-featured {
display: flex;
flex-direction: column;
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
overflow: hidden;
transition: all 0.3s ease;
}
.blog-featured:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary);
}
.blog-featured .blog-image {
position: relative;
aspect-ratio: 16/10;
overflow: hidden;
}
.blog-featured .blog-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.blog-badge {
position: absolute;
top: 16px;
left: 16px;
background: var(--color-primary);
color: var(--color-black);
padding: 6px 14px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.blog-featured .blog-content {
padding: 32px;
}
.blog-category {
display: inline-block;
background: var(--color-bg-soft);
color: var(--color-gray-600);
padding: 4px 12px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
}
.blog-title {
font-family: var(--font-display);
font-size: 22px;
font-weight: 800;
color: var(--color-black);
margin-bottom: 12px;
line-height: 1.3;
}
.blog-excerpt {
font-size: 14px;
color: var(--color-gray-600);
line-height: 1.6;
margin-bottom: 16px;
}
.read-more {
font-size: 14px;
font-weight: 700;
color: var(--color-black);
text-transform: uppercase;
letter-spacing: 1px;
}
.blog-sidebar {
display: flex;
flex-direction: column;
gap: 16px;
}
.blog-card-mini {
display: flex;
gap: 16px;
padding: 12px;
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
transition: all 0.3s ease;
}
.blog-card-mini:hover {
transform: translateX(4px);
border-color: var(--color-primary);
}
.blog-card-image {
width: 80px;
height: 80px;
flex-shrink: 0;
border-radius: var(--radius-md);
overflow: hidden;
background: var(--color-bg-soft);
}
.blog-card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.blog-card-content {
display: flex;
flex-direction: column;
justify-content: center;
}
.blog-category-small {
font-size: 10px;
color: var(--color-gray-500);
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 700;
}
.blog-title-small {
font-size: 14px;
font-weight: 700;
color: var(--color-black);
line-height: 1.3;
margin: 4px 0;
}
.blog-date-small {
font-size: 11px;
color: var(--color-gray-500);
}
/* ============================================
FINAL CTA (yellow)
FINAL CTA
============================================ */
.section-yellow { background: var(--color-primary); }
.cta-section .cta-content {
text-align: center;
max-width: 700px;
margin: 0 auto;
}
.cta-section .cta-title {
.cta-content { text-align: center; max-width: 700px; margin: 0 auto; }
.cta-title {
font-family: var(--font-display);
font-size: clamp(28px, 4vw, 48px);
font-size: clamp(28px, 4vw, 44px);
font-weight: 900;
color: var(--color-black);
margin-bottom: 16px;
}
.cta-section .cta-desc {
.cta-desc {
font-size: 18px;
color: rgba(0, 0, 0, 0.7);
margin-bottom: 32px;
line-height: 1.6;
}
.cta-section .cta-actions {
.cta-actions {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 24px;
}
.cta-section .cta-reassurance {
.cta-reassurance {
margin-top: 24px;
font-size: 14px;
color: rgba(0, 0, 0, 0.6);
}
@@ -649,13 +600,13 @@ const serviceImages: Record<string, string> = {
RESPONSIVE
============================================ */
@media (max-width: 1024px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.problem-grid { grid-template-columns: 1fr; }
.problem-grid { grid-template-columns: repeat(2, 1fr); }
.services-mega-grid { grid-template-columns: 1fr; }
.blog-editorial { grid-template-columns: 1fr; }
.portfolio-preview-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.stats-grid { grid-template-columns: 1fr 1fr; gap: 24px; }
.problem-grid { grid-template-columns: 1fr; }
.portfolio-preview-grid { grid-template-columns: 1fr; }
.cta-actions { flex-direction: column; }
.cta-actions .btn { width: 100%; justify-content: center; }
}