iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Modern Web

30 天製作工作室 SaaS 產品 (前端篇)系列 第 24

Day 24: 30天打造SaaS產品前端篇-Landing Page 設計與實作

  • 分享至 

  • xImage
  •  

前情提要

經過 Day 21-23 的測試三部曲,我們已經建立了完整的測試生態系統。今天我們開啟新的篇章:打造一個專業的 Landing Page,作為 Kyo System 企業級微服務生態系的門面。我們將深入探討現代 Landing Page 的設計原則、動畫效果、效能優化,以及轉換率優化策略。

Landing Page 的核心要素

一個成功的 SaaS Landing Page 需要具備以下要素:

/**
 * Landing Page 成功要素分析
 *
 * 1. Hero Section (主頁橫幅)
 *    ✅ 3 秒內傳達核心價值
 *    ✅ 清晰的 CTA (Call-to-Action)
 *    ✅ 視覺吸引力(動畫、圖片)
 *
 * 2. Value Proposition (價值主張)
 *    ✅ 解決什麼問題
 *    ✅ 為什麼選擇我們
 *    ✅ 量化成果(數據支持)
 *
 * 3. Features (功能展示)
 *    ✅ 3-6 個核心功能
 *    ✅ 視覺化呈現
 *    ✅ 清晰的分類
 *
 * 4. Social Proof (社交證明)
 *    ✅ 客戶評價
 *    ✅ 使用數據
 *    ✅ 合作夥伴 Logo
 *
 * 5. Pricing (定價)
 *    ✅ 清晰透明
 *    ✅ 多層級方案
 *    ✅ 突出推薦方案
 *
 * 6. FAQ (常見問題)
 *    ✅ 預先回答疑慮
 *    ✅ 降低轉換障礙
 *
 * 7. Final CTA (最後行動呼籲)
 *    ✅ 強化訊息
 *    ✅ 降低風險(免費試用)
 *
 * 轉換率基準:
 * - 優秀: 5%+
 * - 良好: 2-5%
 * - 一般: 1-2%
 * - 需改進: <1%
 */

技術架構與工具選擇

// src/pages/Landing/types.ts
/**
 * Landing Page 技術棧
 *
 * UI 框架: React 18 + TypeScript
 * 樣式方案: Mantine UI + Tailwind CSS (optional)
 * 動畫庫: Framer Motion
 * Icons: Tabler Icons (Mantine 內建)
 * 表單處理: React Hook Form + Zod
 * SEO: React Helmet Async
 * Analytics: Google Analytics 4 / PostHog
 */

export interface LandingPageSection {
  id: string;
  title: string;
  visible: boolean;
}

export interface CTAButton {
  text: string;
  variant: 'primary' | 'secondary';
  action: () => void;
  tracking?: {
    event: string;
    category: string;
  };
}

export interface Feature {
  id: string;
  icon: React.ComponentType<{ size?: number }>;
  title: string;
  description: string;
  details?: string[];
}

export interface Testimonial {
  id: string;
  name: string;
  role: string;
  company: string;
  avatar?: string;
  content: string;
  rating: number;
}

export interface PricingPlan {
  id: string;
  name: string;
  price: number;
  currency: string;
  interval: 'month' | 'year';
  description: string;
  features: string[];
  highlighted?: boolean;
  ctaText: string;
  ctaAction: () => void;
}

Hero Section 實作

// src/pages/Landing/sections/HeroSection.tsx
import { Container, Title, Text, Button, Group, Box, Image } from '@mantine/core';
import { motion } from 'framer-motion';
import { IconArrowRight, IconBrandGithub } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';

const MotionBox = motion(Box);
const MotionTitle = motion(Title);
const MotionText = motion(Text);

export function HeroSection() {
  const navigate = useNavigate();

  // 動畫變體
  const containerVariants = {
    hidden: { opacity: 0 },
    visible: {
      opacity: 1,
      transition: {
        staggerChildren: 0.2,
      },
    },
  };

  const itemVariants = {
    hidden: { opacity: 0, y: 20 },
    visible: {
      opacity: 1,
      y: 0,
      transition: {
        duration: 0.6,
        ease: [0.6, -0.05, 0.01, 0.99],
      },
    },
  };

  const floatingVariants = {
    initial: { y: 0 },
    animate: {
      y: [-10, 10, -10],
      transition: {
        duration: 6,
        repeat: Infinity,
        ease: 'easeInOut',
      },
    },
  };

  return (
    <Box
      component="section"
      sx={(theme) => ({
        position: 'relative',
        minHeight: 'calc(100vh - 60px)',
        display: 'flex',
        alignItems: 'center',
        background: `linear-gradient(135deg, ${theme.colors.violet[9]} 0%, ${theme.colors.blue[9]} 100%)`,
        overflow: 'hidden',

        // 背景裝飾
        '&::before': {
          content: '""',
          position: 'absolute',
          top: '-50%',
          right: '-20%',
          width: '800px',
          height: '800px',
          borderRadius: '50%',
          background: `radial-gradient(circle, ${theme.fn.rgba(theme.white, 0.1)} 0%, transparent 70%)`,
          pointerEvents: 'none',
        },

        '&::after': {
          content: '""',
          position: 'absolute',
          bottom: '-30%',
          left: '-10%',
          width: '600px',
          height: '600px',
          borderRadius: '50%',
          background: `radial-gradient(circle, ${theme.fn.rgba(theme.white, 0.08)} 0%, transparent 70%)`,
          pointerEvents: 'none',
        },
      })}
    >
      <Container size="xl" sx={{ position: 'relative', zIndex: 1 }}>
        <MotionBox
          variants={containerVariants}
          initial="hidden"
          animate="visible"
          style={{
            display: 'grid',
            gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
            gap: '3rem',
            alignItems: 'center',
          }}
        >
          {/* 左側文字內容 */}
          <Box>
            <MotionTitle
              variants={itemVariants}
              order={1}
              size="h1"
              sx={(theme) => ({
                color: theme.white,
                fontSize: '3.5rem',
                fontWeight: 800,
                lineHeight: 1.2,
                marginBottom: theme.spacing.md,

                '@media (max-width: 768px)': {
                  fontSize: '2.5rem',
                },
              })}
            >
              從 OTP 驗證開始
              <br />
              打造你的 SaaS 基礎建設
            </MotionTitle>

            <MotionText
              variants={itemVariants}
              size="xl"
              sx={(theme) => ({
                color: theme.fn.rgba(theme.white, 0.9),
                marginBottom: theme.spacing.xl,
                lineHeight: 1.6,
              })}
            >
              Kyo System 是一套模組化的企業級微服務生態系,提供開箱即用的 SaaS 基礎建設。
              <br />
              <strong>首發產品 Kyo OTP - 專業的驗證碼服務,讓你專注於核心業務</strong>
            </MotionText>

            {/* 關鍵數據展示 */}
            <MotionBox variants={itemVariants}>
              <Group spacing="xl" mb="xl">
                <Box>
                  <Text size="xl" weight={700} sx={{ color: 'white' }}>
                    99.9%
                  </Text>
                  <Text size="sm" sx={(theme) => ({ color: theme.fn.rgba(theme.white, 0.7) })}>
                    系統可用性
                  </Text>
                </Box>
                <Box>
                  <Text size="xl" weight={700} sx={{ color: 'white' }}>
                    &lt;100ms
                  </Text>
                  <Text size="sm" sx={(theme) => ({ color: theme.fn.rgba(theme.white, 0.7) })}>
                    平均回應時間
                  </Text>
                </Box>
                <Box>
                  <Text size="xl" weight={700} sx={{ color: 'white' }}>
                    1M+
                  </Text>
                  <Text size="sm" sx={(theme) => ({ color: theme.fn.rgba(theme.white, 0.7) })}>
                    每日發送量
                  </Text>
                </Box>
              </Group>
            </MotionBox>

            {/* CTA 按鈕 */}
            <MotionBox variants={itemVariants}>
              <Group spacing="md">
                <Button
                  size="lg"
                  radius="md"
                  rightIcon={<IconArrowRight size={20} />}
                  sx={(theme) => ({
                    background: theme.white,
                    color: theme.colors.violet[9],
                    '&:hover': {
                      background: theme.fn.rgba(theme.white, 0.9),
                      transform: 'translateY(-2px)',
                    },
                    transition: 'all 0.3s ease',
                    boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
                  })}
                  onClick={() => navigate('/signup')}
                >
                  免費開始使用
                </Button>

                <Button
                  size="lg"
                  radius="md"
                  variant="outline"
                  leftIcon={<IconBrandGithub size={20} />}
                  sx={(theme) => ({
                    color: theme.white,
                    borderColor: theme.white,
                    '&:hover': {
                      background: theme.fn.rgba(theme.white, 0.1),
                      transform: 'translateY(-2px)',
                    },
                    transition: 'all 0.3s ease',
                  })}
                  onClick={() => window.open('https://github.com/kyong-saas', '_blank')}
                >
                  查看文件
                </Button>
              </Group>

              <Text
                size="sm"
                mt="md"
                sx={(theme) => ({
                  color: theme.fn.rgba(theme.white, 0.6),
                })}
              >
                ✨ 無需信用卡 · 每月 1,000 則免費額度
              </Text>
            </MotionBox>
          </Box>

          {/* 右側視覺元素 */}
          <MotionBox
            variants={floatingVariants}
            initial="initial"
            animate="animate"
            sx={{
              position: 'relative',
              display: 'flex',
              justifyContent: 'center',
              alignItems: 'center',
            }}
          >
            {/* 這裡可以放產品截圖、動畫示意圖等 */}
            <Box
              sx={(theme) => ({
                width: '100%',
                maxWidth: '500px',
                aspectRatio: '4/3',
                background: theme.fn.rgba(theme.white, 0.1),
                borderRadius: theme.radius.lg,
                border: `2px solid ${theme.fn.rgba(theme.white, 0.2)}`,
                backdropFilter: 'blur(10px)',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
              })}
            >
              {/* 產品截圖或動畫放這裡 */}
              <DashboardPreview />
            </Box>
          </MotionBox>
        </MotionBox>
      </Container>

      {/* 向下滾動提示 */}
      <MotionBox
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ delay: 1.5 }}
        sx={{
          position: 'absolute',
          bottom: '2rem',
          left: '50%',
          transform: 'translateX(-50%)',
        }}
      >
        <motion.div
          animate={{ y: [0, 10, 0] }}
          transition={{ duration: 1.5, repeat: Infinity }}
        >
          <Text
            size="sm"
            sx={(theme) => ({
              color: theme.fn.rgba(theme.white, 0.6),
              textAlign: 'center',
            })}
          >
            向下滾動了解更多
          </Text>
        </motion.div>
      </MotionBox>
    </Box>
  );
}

/**
 * Dashboard 預覽元件
 */
function DashboardPreview() {
  return (
    <Box p="xl" w="100%">
      <motion.div
        initial={{ opacity: 0, scale: 0.9 }}
        animate={{ opacity: 1, scale: 1 }}
        transition={{ delay: 0.5, duration: 0.8 }}
      >
        <Box
          sx={(theme) => ({
            background: theme.white,
            borderRadius: theme.radius.md,
            padding: theme.spacing.md,
            boxShadow: theme.shadows.xl,
          })}
        >
          {/* 模擬儀表板介面 */}
          <Group spacing="xs" mb="md">
            <Box
              sx={{ width: 12, height: 12, borderRadius: '50%', background: '#FF5F56' }}
            />
            <Box
              sx={{ width: 12, height: 12, borderRadius: '50%', background: '#FFBD2E' }}
            />
            <Box
              sx={{ width: 12, height: 12, borderRadius: '50%', background: '#27C93F' }}
            />
          </Group>

          <Box
            sx={(theme) => ({
              height: '300px',
              background: theme.fn.gradient({ from: 'violet', to: 'blue', deg: 135 }),
              borderRadius: theme.radius.md,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            })}
          >
            <Text size="lg" weight={700} sx={{ color: 'white' }}>
              OTP Dashboard
            </Text>
          </Box>
        </Box>
      </motion.div>
    </Box>
  );
}

Features Section 實作

// src/pages/Landing/sections/FeaturesSection.tsx
import { Container, Title, Text, SimpleGrid, Box, ThemeIcon, Badge } from '@mantine/core';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import {
  IconShield,
  IconBolt,
  IconUsers,
  IconChartLine,
  IconCloud,
  IconLock,
} from '@tabler/icons-react';

const MotionBox = motion(Box);

const features = [
  {
    icon: IconShield,
    title: 'Kyo OTP - 驗證碼服務',
    description: '首發產品,提供企業級的一次性密碼驗證服務',
    details: ['簡訊/Email OTP', '多通道整合', '高送達率'],
    color: 'violet',
  },
  {
    icon: IconUsers,
    title: '多租戶架構',
    description: '完整的租戶隔離,支援 SaaS 多客戶場景',
    details: ['資料隔離', '獨立配額', '租戶管理'],
    color: 'cyan',
  },
  {
    icon: IconCloud,
    title: '微服務架構',
    description: '模組化設計,每個服務可獨立使用或整合',
    details: ['服務解耦', '獨立擴展', '容錯隔離'],
    color: 'blue',
  },
  {
    icon: IconChartLine,
    title: '即時監控與分析',
    description: '完整的可觀測性,掌握系統運行狀態',
    details: ['即時儀表板', '效能指標', '告警通知'],
    color: 'teal',
  },
  {
    icon: IconBolt,
    title: 'RESTful API 優先',
    description: 'oRPC 標準化 API,快速整合到現有系統',
    details: ['OpenAPI 規範', 'SDK 支援', 'Webhook 回調'],
    color: 'indigo',
  },
  {
    icon: IconLock,
    title: 'AWS 雲端原生',
    description: '99.9% SLA 保證,基於 AWS 最佳實踐',
    details: ['自動擴展', '多區域部署', '災難恢復'],
    color: 'grape',
  },
];

export function FeaturesSection() {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: '-100px' });

  const containerVariants = {
    hidden: { opacity: 0 },
    visible: {
      opacity: 1,
      transition: {
        staggerChildren: 0.1,
      },
    },
  };

  const itemVariants = {
    hidden: { opacity: 0, y: 20 },
    visible: {
      opacity: 1,
      y: 0,
      transition: {
        duration: 0.5,
      },
    },
  };

  return (
    <Box
      component="section"
      ref={ref}
      py={80}
      sx={(theme) => ({
        background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
      })}
    >
      <Container size="xl">
        <MotionBox
          initial={{ opacity: 0, y: 20 }}
          animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
          transition={{ duration: 0.6 }}
          mb={50}
          sx={{ textAlign: 'center' }}
        >
          <Badge size="lg" variant="gradient" gradient={{ from: 'violet', to: 'blue' }} mb="md">
            強大功能
          </Badge>
          <Title order={2} size="h1" mb="md">
            為什麼選擇 Kyo System?
          </Title>
          <Text size="lg" color="dimmed" maw={600} mx="auto">
            模組化的企業級微服務生態系,從 OTP 驗證開始,逐步建構完整的 SaaS 基礎建設
          </Text>
        </MotionBox>

        <MotionBox
          variants={containerVariants}
          initial="hidden"
          animate={isInView ? 'visible' : 'hidden'}
        >
          <SimpleGrid
            cols={3}
            spacing="xl"
            breakpoints={[
              { maxWidth: 'md', cols: 2 },
              { maxWidth: 'sm', cols: 1 },
            ]}
          >
            {features.map((feature, index) => (
              <FeatureCard key={index} {...feature} variants={itemVariants} />
            ))}
          </SimpleGrid>
        </MotionBox>
      </Container>
    </Box>
  );
}

interface FeatureCardProps {
  icon: React.ComponentType<{ size?: number }>;
  title: string;
  description: string;
  details: string[];
  color: string;
  variants: any;
}

function FeatureCard({ icon: Icon, title, description, details, color, variants }: FeatureCardProps) {
  return (
    <MotionBox
      variants={variants}
      whileHover={{ y: -5 }}
      sx={(theme) => ({
        padding: theme.spacing.xl,
        borderRadius: theme.radius.lg,
        border: `1px solid ${
          theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
        }`,
        background: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white,
        transition: 'all 0.3s ease',
        cursor: 'pointer',

        '&:hover': {
          boxShadow: theme.shadows.lg,
          borderColor: theme.colors[color][5],
        },
      })}
    >
      <ThemeIcon
        size={60}
        radius="md"
        variant="gradient"
        gradient={{ from: color, to: `${color}.7` }}
        mb="md"
      >
        <Icon size={30} />
      </ThemeIcon>

      <Title order={3} size="h3" mb="sm">
        {title}
      </Title>

      <Text color="dimmed" mb="md">
        {description}
      </Text>

      <Box>
        {details.map((detail, idx) => (
          <Text
            key={idx}
            size="sm"
            color="dimmed"
            sx={(theme) => ({
              display: 'flex',
              alignItems: 'center',
              marginBottom: theme.spacing.xs,

              '&::before': {
                content: '"✓"',
                display: 'inline-block',
                marginRight: theme.spacing.xs,
                color: theme.colors[color][6],
                fontWeight: 700,
              },
            })}
          >
            {detail}
          </Text>
        ))}
      </Box>
    </MotionBox>
  );
}

Pricing Section 實作

// src/pages/Landing/sections/PricingSection.tsx
import { Container, Title, Text, SimpleGrid, Box, Button, Badge, List } from '@mantine/core';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { IconCheck, IconArrowRight } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';

const MotionBox = motion(Box);

const pricingPlans = [
  {
    name: 'Starter',
    price: 0,
    interval: 'month',
    description: '適合個人開發者與小型專案',
    features: [
      '每月 1,000 則免費額度',
      '基本 API 存取',
      '郵件支援',
      '7 天資料保留',
      '基礎統計報表',
    ],
    highlighted: false,
    ctaText: '免費開始',
  },
  {
    name: 'Professional',
    price: 99,
    interval: 'month',
    description: '適合成長中的團隊與企業',
    features: [
      '每月 50,000 則額度',
      '完整 API 功能',
      '優先郵件與線上支援',
      '30 天資料保留',
      '進階分析與報表',
      '自訂 Webhook',
      '多租戶管理',
    ],
    highlighted: true,
    ctaText: '立即升級',
    badge: '最受歡迎',
  },
  {
    name: 'Enterprise',
    price: null,
    interval: 'month',
    description: '適合大型企業與特殊需求',
    features: [
      '無限制發送量',
      '專屬 API 端點',
      '24/7 技術支援',
      '無限資料保留',
      '客製化報表',
      'SLA 保證',
      '專屬客戶經理',
      '私有部署選項',
    ],
    highlighted: false,
    ctaText: '聯絡銷售',
  },
];

export function PricingSection() {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: '-100px' });
  const navigate = useNavigate();

  return (
    <Box
      component="section"
      ref={ref}
      py={80}
      sx={(theme) => ({
        background: theme.fn.gradient({ from: 'violet.9', to: 'blue.9', deg: 135 }),
      })}
    >
      <Container size="xl">
        <MotionBox
          initial={{ opacity: 0, y: 20 }}
          animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
          transition={{ duration: 0.6 }}
          mb={50}
          sx={{ textAlign: 'center' }}
        >
          <Title order={2} size="h1" mb="md" sx={{ color: 'white' }}>
            簡單透明的定價
          </Title>
          <Text size="lg" sx={(theme) => ({ color: theme.fn.rgba(theme.white, 0.8) })} maw={600} mx="auto">
            選擇最適合你的方案,隨時可以升級或降級
          </Text>
        </MotionBox>

        <SimpleGrid
          cols={3}
          spacing="xl"
          breakpoints={[
            { maxWidth: 'md', cols: 2 },
            { maxWidth: 'sm', cols: 1 },
          ]}
        >
          {pricingPlans.map((plan, index) => (
            <PricingCard
              key={index}
              {...plan}
              onCTAClick={() => {
                if (plan.price === null) {
                  window.location.href = 'mailto:sales@kyong.com';
                } else {
                  navigate('/signup');
                }
              }}
              delay={index * 0.1}
              isInView={isInView}
            />
          ))}
        </SimpleGrid>

        <MotionBox
          initial={{ opacity: 0 }}
          animate={isInView ? { opacity: 1 } : { opacity: 0 }}
          transition={{ delay: 0.6 }}
          mt={50}
        >
          <Text size="sm" sx={(theme) => ({ color: theme.fn.rgba(theme.white, 0.6), textAlign: 'center' })}>
            ✨ 所有方案皆包含 SSL 加密、自動備份、以及基礎 DDoS 防護
          </Text>
        </MotionBox>
      </Container>
    </Box>
  );
}

interface PricingCardProps {
  name: string;
  price: number | null;
  interval: string;
  description: string;
  features: string[];
  highlighted: boolean;
  ctaText: string;
  badge?: string;
  onCTAClick: () => void;
  delay: number;
  isInView: boolean;
}

function PricingCard({
  name,
  price,
  interval,
  description,
  features,
  highlighted,
  ctaText,
  badge,
  onCTAClick,
  delay,
  isInView,
}: PricingCardProps) {
  return (
    <MotionBox
      initial={{ opacity: 0, y: 20 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
      transition={{ duration: 0.5, delay }}
      whileHover={{ y: -10 }}
      sx={(theme) => ({
        padding: theme.spacing.xl,
        borderRadius: theme.radius.lg,
        background: theme.white,
        boxShadow: highlighted ? '0 20px 60px rgba(0,0,0,0.3)' : theme.shadows.md,
        border: highlighted ? `3px solid ${theme.colors.yellow[5]}` : 'none',
        position: 'relative',
        transition: 'all 0.3s ease',
        transform: highlighted ? 'scale(1.05)' : 'scale(1)',

        '&:hover': {
          boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
        },
      })}
    >
      {badge && (
        <Badge
          size="lg"
          variant="gradient"
          gradient={{ from: 'yellow', to: 'orange' }}
          sx={{
            position: 'absolute',
            top: -12,
            right: 20,
          }}
        >
          {badge}
        </Badge>
      )}

      <Title order={3} size="h3" mb="xs">
        {name}
      </Title>

      <Text color="dimmed" size="sm" mb="xl">
        {description}
      </Text>

      <Box mb="xl">
        {price === null ? (
          <Title order={2} size="h2">
            聯絡我們
          </Title>
        ) : (
          <>
            <Text
              component="span"
              sx={{ fontSize: '3rem', fontWeight: 700, lineHeight: 1 }}
            >
              ${price}
            </Text>
            <Text component="span" color="dimmed" size="lg" ml="xs">
              / {interval === 'month' ? '月' : '年'}
            </Text>
          </>
        )}
      </Box>

      <List
        spacing="sm"
        mb="xl"
        icon={
          <Box
            sx={(theme) => ({
              width: 20,
              height: 20,
              borderRadius: '50%',
              background: theme.fn.gradient({ from: 'violet', to: 'blue' }),
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            })}
          >
            <IconCheck size={14} color="white" />
          </Box>
        }
      >
        {features.map((feature, idx) => (
          <List.Item key={idx}>
            <Text size="sm">{feature}</Text>
          </List.Item>
        ))}
      </List>

      <Button
        fullWidth
        size="lg"
        variant={highlighted ? 'gradient' : 'outline'}
        gradient={highlighted ? { from: 'violet', to: 'blue' } : undefined}
        rightIcon={<IconArrowRight size={18} />}
        onClick={onCTAClick}
        sx={(theme) => ({
          '&:hover': {
            transform: 'translateY(-2px)',
          },
          transition: 'all 0.3s ease',
        })}
      >
        {ctaText}
      </Button>
    </MotionBox>
  );
}

SEO 優化

// src/pages/Landing/LandingPage.tsx
import { Helmet } from 'react-helmet-async';
import { HeroSection } from './sections/HeroSection';
import { FeaturesSection } from './sections/FeaturesSection';
import { PricingSection } from './sections/PricingSection';
import { TestimonialsSection } from './sections/TestimonialsSection';
import { FAQSection } from './sections/FAQSection';
import { CTASection } from './sections/CTASection';

export function LandingPage() {
  // 結構化資料 (JSON-LD)
  const structuredData = {
    '@context': 'https://schema.org',
    '@type': 'SoftwareApplication',
    name: 'Kyo System',
    applicationCategory: 'BusinessApplication',
    description: '企業級微服務生態系,提供模組化的 SaaS 基礎建設',
    offers: {
      '@type': 'Offer',
      price: '0',
      priceCurrency: 'USD',
    },
    aggregateRating: {
      '@type': 'AggregateRating',
      ratingValue: '4.8',
      ratingCount: '127',
    },
  };

  return (
    <>
      <Helmet>
        {/* 基本 SEO */}
        <title>Kyo System - 企業級微服務生態系 | 從 OTP 驗證開始</title>
        <meta
          name="description"
          content="Kyo System 提供模組化的企業級微服務生態系。首發產品 Kyo OTP - 專業驗證碼服務,支援多租戶、微服務架構、AWS 雲端原生。99.9% SLA 保證。"
        />
        <meta
          name="keywords"
          content="Kyo System, 微服務, SaaS, OTP, 一次性密碼, 驗證碼, 多租戶, AWS, 雲端原生"
        />

        {/* Open Graph */}
        <meta property="og:title" content="Kyo System - 企業級微服務生態系 | 從 OTP 驗證開始" />
        <meta
          property="og:description"
          content="模組化的企業級微服務生態系,首發產品 Kyo OTP 提供專業的驗證碼服務"
        />
        <meta property="og:type" content="website" />
        <meta property="og:url" content="https://kyong.com" />
        <meta property="og:image" content="https://kyong.com/og-image.png" />

        {/* Twitter Card */}
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:title" content="Kyo System - 企業級微服務生態系 | 從 OTP 驗證開始" />
        <meta
          name="twitter:description"
          content="模組化的企業級微服務生態系,首發產品 Kyo OTP 提供專業的驗證碼服務"
        />
        <meta name="twitter:image" content="https://kyong.com/twitter-image.png" />

        {/* 結構化資料 */}
        <script type="application/ld+json">{JSON.stringify(structuredData)}</script>

        {/* Canonical URL */}
        <link rel="canonical" href="https://kyong.com" />
      </Helmet>

      <main>
        <HeroSection />
        <FeaturesSection />
        <PricingSection />
        <TestimonialsSection />
        <FAQSection />
        <CTASection />
      </main>
    </>
  );
}

效能優化策略

// src/pages/Landing/optimizations.ts
/**
 * Landing Page 效能優化策略
 *
 * 1. 程式碼分割 (Code Splitting)
 *    - 路由層級分割
 *    - 元件層級懶加載
 *    - 第三方庫按需載入
 *
 * 2. 圖片優化
 *    - WebP 格式
 *    - 響應式圖片 (srcset)
 *    - 懶加載 (Lazy Loading)
 *    - CDN 託管
 *
 * 3. 資源預載入
 *    - DNS Prefetch
 *    - Preconnect
 *    - Prefetch 關鍵資源
 *
 * 4. 快取策略
 *    - Service Worker
 *    - HTTP 快取頭
 *    - LocalStorage 快取
 *
 * 5. 關鍵渲染路徑優化
 *    - 內聯關鍵 CSS
 *    - 延遲載入非關鍵 JS
 *    - 減少 Render-blocking 資源
 *
 * 6. 字型優化
 *    - 字型子集化
 *    - font-display: swap
 *    - 預載入字型
 */

// 懶加載圖片
import { useEffect, useRef, useState } from 'react';

export function useLazyImage(src: string) {
  const [imageSrc, setImageSrc] = useState<string | null>(null);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setImageSrc(src);
            observer.disconnect();
          }
        });
      },
      { rootMargin: '50px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, [src]);

  return { imageSrc, imgRef };
}

// 預載入關鍵資源
export function preloadCriticalAssets() {
  // 預載入字型
  const fontLink = document.createElement('link');
  fontLink.rel = 'preload';
  fontLink.as = 'font';
  fontLink.type = 'font/woff2';
  fontLink.href = '/fonts/inter-var.woff2';
  fontLink.crossOrigin = 'anonymous';
  document.head.appendChild(fontLink);

  // 預連接到 CDN
  const preconnect = document.createElement('link');
  preconnect.rel = 'preconnect';
  preconnect.href = 'https://cdn.kyong.com';
  document.head.appendChild(preconnect);
}

// 效能監控
export function reportWebVitals(metric: any) {
  // 發送到分析服務
  if (window.gtag) {
    window.gtag('event', metric.name, {
      event_category: 'Web Vitals',
      value: Math.round(metric.value),
      event_label: metric.id,
      non_interaction: true,
    });
  }

  // 或發送到自己的分析端點
  fetch('/api/analytics/web-vitals', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: metric.name,
      value: metric.value,
      id: metric.id,
      navigationType: metric.navigationType,
    }),
  });
}

A/B 測試框架

// src/lib/ab-testing.ts
import { useState, useEffect } from 'react';

export interface Experiment {
  id: string;
  name: string;
  variants: Array<{
    id: string;
    name: string;
    weight: number; // 權重 (0-100)
  }>;
}

export interface ExperimentResult {
  experimentId: string;
  variantId: string;
}

/**
 * A/B 測試 Hook
 */
export function useExperiment(experimentId: string): string | null {
  const [variantId, setVariantId] = useState<string | null>(null);

  useEffect(() => {
    // 檢查是否已經分配過變體
    const savedVariant = localStorage.getItem(`experiment_${experimentId}`);

    if (savedVariant) {
      setVariantId(savedVariant);
      return;
    }

    // 從伺服器取得實驗配置
    fetch(`/api/experiments/${experimentId}`)
      .then((res) => res.json())
      .then((experiment: Experiment) => {
        // 加權隨機分配
        const variant = selectVariant(experiment.variants);
        setVariantId(variant.id);

        // 儲存分配結果
        localStorage.setItem(`experiment_${experimentId}`, variant.id);

        // 追蹤分配事件
        trackExperiment(experimentId, variant.id);
      });
  }, [experimentId]);

  return variantId;
}

/**
 * 加權隨機選擇變體
 */
function selectVariant(variants: Array<{ id: string; weight: number }>) {
  const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
  let random = Math.random() * totalWeight;

  for (const variant of variants) {
    random -= variant.weight;
    if (random <= 0) {
      return variant;
    }
  }

  return variants[0]; // fallback
}

/**
 * 追蹤實驗分配
 */
function trackExperiment(experimentId: string, variantId: string) {
  if (window.gtag) {
    window.gtag('event', 'experiment_assignment', {
      experiment_id: experimentId,
      variant_id: variantId,
    });
  }

  // 發送到自己的分析端點
  fetch('/api/analytics/experiments', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      experimentId,
      variantId,
      timestamp: new Date().toISOString(),
    }),
  });
}

/**
 * 追蹤轉換事件
 */
export function trackConversion(experimentId: string, eventName: string, value?: number) {
  const variantId = localStorage.getItem(`experiment_${experimentId}`);

  if (!variantId) return;

  if (window.gtag) {
    window.gtag('event', eventName, {
      experiment_id: experimentId,
      variant_id: variantId,
      value,
    });
  }

  fetch('/api/analytics/conversions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      experimentId,
      variantId,
      eventName,
      value,
      timestamp: new Date().toISOString(),
    }),
  });
}

// 使用範例
export function HeroSectionWithABTest() {
  const ctaVariant = useExperiment('hero_cta_text');

  const ctaText = {
    control: '免費開始使用',
    variant_a: '立即免費試用',
    variant_b: '開始你的旅程',
  }[ctaVariant || 'control'];

  const handleCTAClick = () => {
    trackConversion('hero_cta_text', 'signup_click');
    // ... 導航到註冊頁面
  };

  return (
    <Button onClick={handleCTAClick}>
      {ctaText}
    </Button>
  );
}

轉換率優化 (CRO) 技巧

/**
 * 轉換率優化最佳實踐
 *
 * 1. 清晰的價值主張
 *    ✅ 3 秒內讓訪客理解產品價值
 *    ✅ 使用數據支持(99.9% 可用性)
 *    ✅ 突出差異化優勢
 *
 * 2. 減少摩擦
 *    ✅ 簡化註冊流程(單頁表單)
 *    ✅ 社交登入選項
 *    ✅ 無需信用卡試用
 *    ✅ 自動填充
 *
 * 3. 建立信任
 *    ✅ 客戶評價與案例
 *    ✅ 安全認證標章
 *    ✅ 透明定價
 *    ✅ 隱私政策連結
 *
 * 4. 緊迫感與稀缺性
 *    ⚠️ 適度使用,避免過度
 *    ✅ 限時優惠
 *    ✅ 剩餘名額顯示
 *
 * 5. 多重 CTA
 *    ✅ Hero Section CTA
 *    ✅ Features 後的 CTA
 *    ✅ Pricing 中的 CTA
 *    ✅ 頁尾 CTA
 *
 * 6. 手機優先
 *    ✅ 響應式設計
 *    ✅ 觸控友善按鈕(最小 44x44px)
 *    ✅ 快速載入(<3s)
 *
 * 7. 追蹤與優化
 *    ✅ 設定轉換目標
 *    ✅ 熱圖分析(Hotjar)
 *    ✅ 使用者錄影
 *    ✅ A/B 測試
 */

// 轉換漏斗追蹤
export function setupConversionFunnel() {
  // 頁面載入
  trackFunnelStep('landing_page_view');

  // 滾動深度
  let maxScrollDepth = 0;
  window.addEventListener('scroll', () => {
    const scrollDepth = Math.round(
      (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
    );

    if (scrollDepth > maxScrollDepth) {
      maxScrollDepth = scrollDepth;

      if (scrollDepth >= 25 && maxScrollDepth < 50) {
        trackFunnelStep('scroll_25');
      } else if (scrollDepth >= 50 && maxScrollDepth < 75) {
        trackFunnelStep('scroll_50');
      } else if (scrollDepth >= 75) {
        trackFunnelStep('scroll_75');
      }
    }
  });

  // CTA 點擊
  document.querySelectorAll('[data-cta]').forEach((button) => {
    button.addEventListener('click', () => {
      const ctaName = button.getAttribute('data-cta');
      trackFunnelStep(`cta_click_${ctaName}`);
    });
  });
}

function trackFunnelStep(step: string) {
  if (window.gtag) {
    window.gtag('event', 'funnel_step', {
      step_name: step,
      timestamp: new Date().toISOString(),
    });
  }
}

今日總結

我們今天完成了 Landing Page 的設計與實作:

核心成就

  1. Hero Section: 吸引眼球的主頁橫幅設計,突出 Kyo System 生態系定位
  2. Features Section: 清晰呈現微服務架構與 Kyo OTP 首發產品
  3. Pricing Section: 透明的定價策略
  4. SEO 優化: 完整的 meta tags 與結構化資料
  5. 效能優化: 圖片懶加載、程式碼分割
  6. A/B 測試: 資料驅動的優化框架
  7. CRO 策略: 提升轉換率的最佳實踐

技術深度分析

Framer Motion vs CSS 動畫:

  • Framer Motion: 聲明式 API、更好的控制、支援手勢
  • CSS 動畫: 更高效能、但功能有限
  • 💡 建議:簡單動畫用 CSS,複雜互動用 Framer Motion

主頁橫幅載入優化:

  • 目標 LCP (Largest Contentful Paint) < 2.5s
  • 內聯關鍵 CSS(~14KB)
  • 預載入主頁橫幅圖片
  • 延遲載入非主頁橫幅元件

A/B 測試最佳實踐:

  • 一次只測試一個變量
  • 需要統計顯著性(至少 95% 信心水準)
  • 測試週期至少 2 週
  • 記錄所有實驗結果

Landing Page 檢查清單

  • ✅ 清晰的價值主張(3 秒法則)
  • ✅ 強烈的 CTA 按鈕
  • ✅ 社交證明(客戶評價)
  • ✅ 透明定價
  • ✅ 手機響應式設計
  • ✅ 載入速度 <3 秒
  • ✅ SEO 優化完整
  • ✅ 追蹤分析設定
  • ✅ A/B 測試框架
  • ✅ 無障礙設計 (A11y)

https://ithelp.ithome.com.tw/upload/images/20251008/20140358UFBYOX1ePw.pnghttps://ithelp.ithome.com.tw/upload/images/20251008/20140358OrD6LDWIly.pnghttps://ithelp.ithome.com.tw/upload/images/20251008/20140358kVhc2NUV3B.png


上一篇
Day 23: 30天打造SaaS產品前端篇-測試覆蓋率報告與效能優化
系列文
30 天製作工作室 SaaS 產品 (前端篇)24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言