Skip to content

自定义首页开发

VitePress 默认主题提供了开箱即用的首页配置,但当需求超出 frontmatter 配置能力时,就需要开发自定义首页。本文从插槽扩展到完全自定义,逐步讲解首页定制方案。

方案选择

方案灵活度复杂度适用场景
frontmatter 配置标准首页,快速搭建
布局插槽扩展⭐⭐⭐⭐⭐在默认首页基础上添加内容
完全自定义首页⭐⭐⭐⭐⭐⭐⭐⭐⭐完全控制首页布局和交互

方案一:frontmatter 配置

这是最简单的方式,通过 index.md 的 frontmatter 配置首页:

yaml
---
layout: home

hero:
  name: VitePress
  text: 快速建站
  tagline: 基于 Vite 和 Vue 的静态站点生成器
  image:
    src: /logo.svg
    alt: VitePress
  actions:
    - theme: brand
      text: 快速开始
      link: /guide/getting-started
    - theme: alt
      text: GitHub
      link: https://github.com/vuejs/vitepress

features:
  - icon: 
    title: Vite 驱动
    details: 享受 Vite 的即时热更新和极速构建
  - icon: 🖖
    title: Vue 驱动
    details: 在 Markdown 中使用 Vue 组件和语法
  - icon: 🛠️
    title: 功能丰富
    details: 支持搜索、i18n、自定义主题等
---

Hero 配置项

字段类型说明
namestring主标题上方的大字
textstring主标题
taglinestring副标题
image.srcstringHero 图片路径
image.altstring图片 alt 文本
actionsarray操作按钮列表

Features 配置项

字段类型说明
iconstring图标(Emoji 或组件名)
titlestring特性标题
detailsstring特性描述
linkstring点击跳转链接
linkTextstring链接文本

限制

frontmatter 配置的首页布局是固定的:Hero 区域 + Features 网格。如需其他布局(如合作伙伴 Logo 墙、统计数据、时间线等),需要使用方案二或方案三。

方案二:布局插槽扩展

在默认首页基础上,通过插槽插入自定义内容。

可用首页插槽

text
┌──────────────────────────────────────┐
│ layout-top                           │ ← 全宽顶部
├──────────────────────────────────────┤
│                                      │
│         home-hero-before             │ ← Hero 前
│    ┌──────────────────────┐          │
│    │     Hero 区域         │          │
│    └──────────────────────┘          │
│         home-hero-after              │ ← Hero 后
│                                      │
│         home-features-before         │ ← Features 前
│    ┌──────────────────────┐          │
│    │   Features 网格       │          │
│    └──────────────────────┘          │
│         home-features-after          │ ← Features 后
│                                      │
├──────────────────────────────────────┤
│ layout-bottom                        │ ← 全宽底部
└──────────────────────────────────────┘

示例:添加统计数据模块

vue
<!-- .vitepress/theme/components/HomeStats.vue -->
<script setup lang="ts">
const stats = [
  { label: 'GitHub Stars', value: '12K+', icon: '⭐' },
  { label: 'NPM 周下载', value: '50K+', icon: '📦' },
  { label: '贡献者', value: '300+', icon: '👥' },
  { label: '插件数', value: '100+', icon: '🔌' }
]
</script>

<template>
  <div class="home-stats">
    <div v-for="stat in stats" :key="stat.label" class="stat-item">
      <span class="stat-icon">{{ stat.icon }}</span>
      <span class="stat-value">{{ stat.value }}</span>
      <span class="stat-label">{{ stat.label }}</span>
    </div>
  </div>
</template>

<style scoped>
.home-stats {
  display: flex;
  justify-content: center;
  gap: 48px;
  padding: 48px 24px;
  max-width: 1152px;
  margin: 0 auto;
}

.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.stat-icon {
  font-size: 32px;
}

.stat-value {
  font-size: 36px;
  font-weight: 700;
  color: var(--vp-c-brand);
}

.stat-label {
  font-size: 14px;
  color: var(--vp-c-text-2);
}

@media (max-width: 768px) {
  .home-stats {
    flex-wrap: wrap;
    gap: 24px;
  }

  .stat-item {
    width: 40%;
  }

  .stat-value {
    font-size: 28px;
  }
}
</style>

示例:添加合作伙伴 Logo 墙

vue
<!-- .vitepress/theme/components/HomeSponsors.vue -->
<script setup lang="ts">
interface Sponsor {
  name: string
  logo: string
  url: string
  tier: 'platinum' | 'gold' | 'silver'
}

const sponsors: Sponsor[] = [
  { name: 'Vue.js', logo: '/images/sponsors/vue.svg', url: 'https://vuejs.org', tier: 'platinum' },
  { name: 'Vite', logo: '/images/sponsors/vite.svg', url: 'https://vitejs.dev', tier: 'platinum' },
  { name: 'Nuxt', logo: '/images/sponsors/nuxt.svg', url: 'https://nuxt.com', tier: 'gold' },
  { name: 'Pinia', logo: '/images/sponsors/pinia.svg', url: 'https://pinia.vuejs.org', tier: 'silver' }
]
</script>

<template>
  <section class="home-sponsors">
    <h2 class="section-title">合作伙伴</h2>

    <div v-for="tier in ['platinum', 'gold', 'silver']" :key="tier" class="sponsor-tier">
      <h3 class="tier-label">{{ { platinum: '铂金', gold: '金牌', silver: '银牌' }[tier] }}</h3>
      <div class="sponsor-grid" :class="`tier-${tier}`">
        <a
          v-for="sponsor in sponsors.filter(s => s.tier === tier)"
          :key="sponsor.name"
          :href="sponsor.url"
          target="_blank"
          rel="noopener"
          class="sponsor-card"
        >
          <img :src="sponsor.logo" :alt="sponsor.name" />
          <span>{{ sponsor.name }}</span>
        </a>
      </div>
    </div>
  </section>
</template>

<style scoped>
.home-sponsors {
  padding: 64px 24px;
  text-align: center;
  max-width: 1152px;
  margin: 0 auto;
}

.section-title {
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 48px;
  color: var(--vp-c-text-1);
}

.tier-label {
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--vp-c-text-3);
  margin-bottom: 16px;
}

.sponsor-grid {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 32px;
}

.sponsor-card {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 24px;
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  transition: border-color 0.25s, box-shadow 0.25s;
  color: var(--vp-c-text-2);
  text-decoration: none;
}

.sponsor-card:hover {
  border-color: var(--vp-c-brand);
  box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.15);
}

.sponsor-card img {
  height: 24px;
}

.tier-platinum .sponsor-card {
  padding: 20px 32px;
}

.tier-platinum .sponsor-card img {
  height: 36px;
}
</style>

注册插槽

vue
<!-- .vitepress/theme/Layout.vue -->
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import HomeStats from './components/HomeStats.vue'
import HomeSponsors from './components/HomeSponsors.vue'

const { Layout } = DefaultTheme
</script>

<template>
  <Layout>
    <template #home-hero-after>
      <HomeStats />
    </template>
    <template #home-features-after>
      <HomeSponsors />
    </template>
  </Layout>
</template>

方案三:完全自定义首页

当默认首页布局完全无法满足需求时,可以创建完全自定义的首页。

步骤一:创建首页组件

vue
<!-- .vitepress/theme/components/CustomHome.vue -->
<script setup lang="ts">
import { onMounted, ref } from 'vue'

// 打字机效果
const typedText = ref('')
const fullText = '快速构建现代文档站点'
let index = 0

onMounted(() => {
  const timer = setInterval(() => {
    typedText.value = fullText.slice(0, index + 1)
    index++
    if (index >= fullText.length) {
      clearInterval(timer)
    }
  }, 100)
})

// 特性列表
const features = [
  {
    icon: '⚡',
    title: '极速构建',
    description: '基于 Vite 的即时热更新,毫秒级响应',
    link: '/basics/configuration'
  },
  {
    icon: '🖖',
    title: 'Vue 驱动',
    description: '在 Markdown 中无缝使用 Vue 组件和组合式 API',
    link: '/advanced/using-vue'
  },
  {
    icon: '🎨',
    title: '主题定制',
    description: '完整的主题系统,支持 CSS 变量和布局插槽',
    link: '/theme/default-theme'
  },
  {
    icon: '🔍',
    title: '全文搜索',
    description: '内置本地搜索,或集成 Algolia 云搜索',
    link: '/advanced/search-comparison'
  },
  {
    icon: '🌍',
    title: '国际化',
    description: '原生支持多语言站点,轻松面向全球用户',
    link: '/advanced/i18n'
  },
  {
    icon: '🚀',
    title: '一键部署',
    description: '支持 GitHub Pages、Vercel、Netlify 等多平台',
    link: '/advanced/deployment-platforms'
  }
]
</script>

<template>
  <div class="custom-home">
    <!-- Hero 区域 -->
    <section class="hero">
      <div class="hero-bg">
        <div class="hero-gradient" />
      </div>
      <div class="hero-content">
        <h1 class="hero-title">
          <span class="hero-name">VitePress</span>
          <span class="hero-typed">{{ typedText }}<span class="cursor">|</span></span>
        </h1>
        <p class="hero-description">
          Vite + Vue 驱动的静态站点生成器,专为技术文档打造
        </p>
        <div class="hero-actions">
          <a href="/guide/getting-started" class="btn btn-brand">快速开始</a>
          <a href="/guide/what-is-vitepress" class="btn btn-alt">了解更多</a>
        </div>
      </div>
    </section>

    <!-- 特性网格 -->
    <section class="features">
      <div class="features-grid">
        <a
          v-for="feature in features"
          :key="feature.title"
          :href="feature.link"
          class="feature-card"
        >
          <span class="feature-icon">{{ feature.icon }}</span>
          <h3 class="feature-title">{{ feature.title }}</h3>
          <p class="feature-desc">{{ feature.description }}</p>
        </a>
      </div>
    </section>

    <!-- 代码示例区域 -->
    <section class="code-demo">
      <h2 class="section-title">简洁的配置,强大的功能</h2>
      <div class="code-container">
        <div class="code-label">.vitepress/config.mts</div>
        <pre class="code-block"><code>import { defineConfig } from 'vitepress'

export default defineConfig({
  title: '我的文档',
  description: '基于 VitePress 构建',
  themeConfig: {
    nav: [...],
    sidebar: {...}
  }
})</code></pre>
      </div>
    </section>
  </div>
</template>

<style scoped>
/* Hero 区域 */
.hero {
  position: relative;
  padding: 96px 24px 64px;
  text-align: center;
  overflow: hidden;
}

.hero-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.hero-gradient {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse at 50% 0%,
    rgba(var(--vp-c-brand-rgb), 0.15) 0%,
    transparent 60%
  );
}

.hero-content {
  position: relative;
  z-index: 1;
  max-width: 800px;
  margin: 0 auto;
}

.hero-title {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 24px;
}

.hero-name {
  font-size: 64px;
  font-weight: 800;
  background: linear-gradient(
    135deg,
    var(--vp-c-brand) 0%,
    var(--vp-c-brand-light) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  letter-spacing: -0.02em;
}

.hero-typed {
  font-size: 28px;
  font-weight: 500;
  color: var(--vp-c-text-2);
}

.cursor {
  animation: blink 1s step-end infinite;
}

@keyframes blink {
  50% { opacity: 0; }
}

.hero-description {
  font-size: 18px;
  color: var(--vp-c-text-2);
  margin-bottom: 40px;
  line-height: 1.6;
}

.hero-actions {
  display: flex;
  justify-content: center;
  gap: 16px;
}

/* 按钮 */
.btn {
  display: inline-flex;
  align-items: center;
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  text-decoration: none;
  transition: all 0.25s;
}

.btn-brand {
  background: var(--vp-c-brand);
  color: var(--vp-c-white);
}

.btn-brand:hover {
  background: var(--vp-c-brand-dark);
}

.btn-alt {
  border: 1px solid var(--vp-c-divider);
  color: var(--vp-c-text-1);
  background: var(--vp-c-bg-soft);
}

.btn-alt:hover {
  border-color: var(--vp-c-brand);
  color: var(--vp-c-brand);
}

/* 特性网格 */
.features {
  padding: 64px 24px;
  max-width: 1152px;
  margin: 0 auto;
}

.features-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}

.feature-card {
  padding: 24px;
  border: 1px solid var(--vp-c-divider);
  border-radius: 12px;
  text-decoration: none;
  transition: all 0.25s;
}

.feature-card:hover {
  border-color: var(--vp-c-brand);
  box-shadow: 0 4px 16px rgba(var(--vp-c-brand-rgb), 0.1);
  transform: translateY(-2px);
}

.feature-icon {
  font-size: 32px;
  margin-bottom: 12px;
  display: block;
}

.feature-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--vp-c-text-1);
  margin-bottom: 8px;
}

.feature-desc {
  font-size: 14px;
  color: var(--vp-c-text-2);
  line-height: 1.6;
}

/* 代码演示区域 */
.code-demo {
  padding: 64px 24px;
  text-align: center;
  max-width: 800px;
  margin: 0 auto;
}

.section-title {
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 32px;
  color: var(--vp-c-text-1);
}

.code-container {
  border: 1px solid var(--vp-c-divider);
  border-radius: 12px;
  overflow: hidden;
  text-align: left;
}

.code-label {
  padding: 8px 16px;
  font-size: 12px;
  color: var(--vp-c-text-3);
  border-bottom: 1px solid var(--vp-c-divider);
  background: var(--vp-c-bg-soft);
}

.code-block {
  padding: 16px;
  margin: 0;
  font-family: var(--vp-font-family-mono);
  font-size: 14px;
  line-height: 1.6;
  color: var(--vp-c-text-2);
  background: var(--vp-c-bg-soft);
  overflow-x: auto;
}

/* 响应式 */
@media (max-width: 768px) {
  .hero {
    padding: 64px 16px 48px;
  }

  .hero-name {
    font-size: 40px;
  }

  .hero-typed {
    font-size: 20px;
  }

  .features-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
  }
}

@media (max-width: 480px) {
  .features-grid {
    grid-template-columns: 1fr;
  }

  .hero-actions {
    flex-direction: column;
    align-items: center;
  }
}
</style>

步骤二:在首页中使用自定义组件

有两种方式使用自定义首页组件:

方式一:通过 layout + 插槽(推荐)

yaml
---
layout: home
---
vue
<!-- .vitepress/theme/Layout.vue -->
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import CustomHome from './components/CustomHome.vue'
import { useData } from 'vitepress'

const { Layout } = DefaultTheme
const { frontmatter } = useData()
</script>

<template>
  <Layout>
    <template #home-hero-before>
      <CustomHome v-if="frontmatter.customHome" />
    </template>
  </Layout>
</template>
yaml
---
layout: home
customHome: true
---

方式二:完全替换布局

vue
<!-- .vitepress/theme/Layout.vue -->
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import CustomHome from './components/CustomHome.vue'
import { useData } from 'vitepress'

const { Layout } = DefaultTheme
const { frontmatter } = useData()
</script>

<template>
  <CustomHome v-if="frontmatter.layout === 'custom-home'" />
  <Layout v-else />
</template>
yaml
---
layout: custom-home
---

注意

方式二会导致默认首页的所有样式和脚本都不加载。确保自定义组件包含所有必要的内容。

高级模块开发

动画数字计数器

vue
<!-- .vitepress/theme/components/AnimatedCounter.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

interface Props {
  target: number
  duration?: number
  suffix?: string
  prefix?: string
}

const props = withDefaults(defineProps<Props>(), {
  duration: 2000,
  suffix: '',
  prefix: ''
})

const current = ref(0)
let animationFrame: number | null = null

function animate() {
  const start = performance.now()

  function update(now: number) {
    const elapsed = now - start
    const progress = Math.min(elapsed / props.duration, 1)
    // easeOutExpo
    const eased = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress)
    current.value = Math.floor(eased * props.target)

    if (progress < 1) {
      animationFrame = requestAnimationFrame(update)
    }
  }

  animationFrame = requestAnimationFrame(update)
}

onMounted(() => {
  // 使用 IntersectionObserver 在可见时触发
  const observer = new IntersectionObserver(
    (entries) => {
      if (entries[0].isIntersecting) {
        animate()
        observer.disconnect()
      }
    },
    { threshold: 0.5 }
  )
  // 观察父元素
  observer.observe(document.querySelector('.counter-container')!)
  onUnmounted(() => observer.disconnect())
})

onUnmounted(() => {
  if (animationFrame) cancelAnimationFrame(animationFrame)
})
</script>

<template>
  <span class="animated-counter">
    {{ prefix }}{{ current.toLocaleString() }}{{ suffix }}
  </span>
</template>

<style scoped>
.animated-counter {
  font-variant-numeric: tabular-nums;
}
</style>

轮播推荐语

vue
<!-- .vitepress/theme/components/TestimonialCarousel.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

interface Testimonial {
  quote: string
  author: string
  role: string
  avatar: string
}

const testimonials: Testimonial[] = [
  {
    quote: 'VitePress 让我们的文档编写效率提升了 3 倍。',
    author: '张三',
    role: '前端架构师 @ 某大厂',
    avatar: '/images/avatars/zhang.png'
  },
  {
    quote: 'Markdown + Vue 组件的组合非常强大,文档也能有交互。',
    author: '李四',
    role: '技术作家 @ 开源社区',
    avatar: '/images/avatars/li.png'
  },
  {
    quote: '从 VuePress 迁移过来后,构建速度提升了 10 倍。',
    author: '王五',
    role: '全栈工程师 @ 创业公司',
    avatar: '/images/avatars/wang.png'
  }
]

const currentIndex = ref(0)
let timer: ReturnType<typeof setInterval> | null = null

function next() {
  currentIndex.value = (currentIndex.value + 1) % testimonials.length
}

function prev() {
  currentIndex.value = (currentIndex.value - 1 + testimonials.length) % testimonials.length
}

onMounted(() => {
  timer = setInterval(next, 5000)
})

onUnmounted(() => {
  if (timer) clearInterval(timer)
})
</script>

<template>
  <section class="testimonials">
    <h2 class="section-title">用户评价</h2>
    <div class="carousel">
      <button class="carousel-btn prev" @click="prev">‹</button>

      <Transition name="fade" mode="out-in">
        <div :key="currentIndex" class="testimonial-card">
          <blockquote class="testimonial-quote">
            "{{ testimonials[currentIndex].quote }}"
          </blockquote>
          <div class="testimonial-author">
            <img
              :src="testimonials[currentIndex].avatar"
              :alt="testimonials[currentIndex].author"
              class="author-avatar"
            />
            <div>
              <div class="author-name">{{ testimonials[currentIndex].author }}</div>
              <div class="author-role">{{ testimonials[currentIndex].role }}</div>
            </div>
          </div>
        </div>
      </Transition>

      <button class="carousel-btn next" @click="next">›</button>
    </div>

    <div class="carousel-dots">
      <button
        v-for="(_, i) in testimonials"
        :key="i"
        class="dot"
        :class="{ active: i === currentIndex }"
        @click="currentIndex = i"
      />
    </div>
  </section>
</template>

<style scoped>
.testimonials {
  padding: 64px 24px;
  text-align: center;
  max-width: 800px;
  margin: 0 auto;
}

.section-title {
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 48px;
}

.carousel {
  display: flex;
  align-items: center;
  gap: 16px;
}

.carousel-btn {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--vp-c-divider);
  background: var(--vp-c-bg-soft);
  color: var(--vp-c-text-2);
  font-size: 20px;
  cursor: pointer;
  transition: all 0.25s;
}

.carousel-btn:hover {
  border-color: var(--vp-c-brand);
  color: var(--vp-c-brand);
}

.testimonial-card {
  flex: 1;
  padding: 32px;
}

.testimonial-quote {
  font-size: 20px;
  font-style: italic;
  color: var(--vp-c-text-1);
  margin-bottom: 24px;
  line-height: 1.6;
}

.testimonial-author {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

.author-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
}

.author-name {
  font-weight: 600;
  color: var(--vp-c-text-1);
}

.author-role {
  font-size: 14px;
  color: var(--vp-c-text-3);
}

.carousel-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 24px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: none;
  background: var(--vp-c-divider);
  cursor: pointer;
  transition: all 0.25s;
}

.dot.active {
  background: var(--vp-c-brand);
  width: 24px;
  border-radius: 4px;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

响应式时间线

vue
<!-- .vitepress/theme/components/HomeTimeline.vue -->
<script setup lang="ts">
interface TimelineItem {
  date: string
  title: string
  description: string
  icon?: string
}

const items: TimelineItem[] = [
  { date: '2026 Q1', title: '项目启动', description: '确定技术选型,搭建基础架构', icon: '🚀' },
  { date: '2026 Q2', title: '内容完善', description: '编写核心文档,建立贡献指南', icon: '📝' },
  { date: '2026 Q3', title: '社区建设', description: '开源发布,吸引贡献者', icon: '🌐' },
  { date: '2026 Q4', title: '生态扩展', description: '插件系统,主题市场', icon: '🔌' }
]
</script>

<template>
  <section class="home-timeline">
    <h2 class="section-title">发展路线</h2>
    <div class="timeline">
      <div v-for="(item, i) in items" :key="i" class="timeline-item">
        <div class="timeline-marker">
          <span class="marker-icon">{{ item.icon || '📍' }}</span>
        </div>
        <div class="timeline-content">
          <span class="timeline-date">{{ item.date }}</span>
          <h3 class="timeline-title">{{ item.title }}</h3>
          <p class="timeline-desc">{{ item.description }}</p>
        </div>
      </div>
    </div>
  </section>
</template>

<style scoped>
.home-timeline {
  padding: 64px 24px;
  max-width: 800px;
  margin: 0 auto;
}

.section-title {
  font-size: 24px;
  font-weight: 700;
  text-align: center;
  margin-bottom: 48px;
}

.timeline {
  position: relative;
  padding-left: 32px;
}

.timeline::before {
  content: '';
  position: absolute;
  left: 15px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--vp-c-divider);
}

.timeline-item {
  position: relative;
  padding-bottom: 32px;
}

.timeline-item:last-child {
  padding-bottom: 0;
}

.timeline-marker {
  position: absolute;
  left: -32px;
  top: 0;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.marker-icon {
  font-size: 20px;
  background: var(--vp-c-bg);
  padding: 2px;
  border-radius: 50%;
  position: relative;
  z-index: 1;
}

.timeline-content {
  padding: 16px 24px;
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  transition: border-color 0.25s;
}

.timeline-content:hover {
  border-color: var(--vp-c-brand);
}

.timeline-date {
  font-size: 12px;
  font-weight: 600;
  color: var(--vp-c-brand);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.timeline-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--vp-c-text-1);
  margin: 4px 0;
}

.timeline-desc {
  font-size: 14px;
  color: var(--vp-c-text-2);
  line-height: 1.6;
}
</style>

首页性能优化

图片懒加载

vue
<!-- 使用 IntersectionObserver 实现懒加载 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const imageRef = ref<HTMLImageElement>()
const isLoaded = ref(false)

onMounted(() => {
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      isLoaded.value = true
      observer.disconnect()
    }
  })
  if (imageRef.value) observer.observe(imageRef.value)
})
</script>

<template>
  <img
    ref="imageRef"
    :src="isLoaded ? '/images/hero-banner.webp' : ''"
    :data-src="/images/hero-banner.webp"
    alt="VitePress 首页横幅"
    loading="lazy"
  />
</template>

关键 CSS 内联

确保首页首屏内容快速渲染:

ts
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import './styles/home-critical.css'

export default DefaultTheme
css
/* .vitepress/theme/styles/home-critical.css */
/* 首页关键样式 - 内联优先 */
.custom-home .hero {
  min-height: 60vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.custom-home .hero-name {
  font-size: clamp(40px, 8vw, 64px);
  font-weight: 800;
}

最佳实践

实践说明
移动优先先设计移动端布局,再逐步增强桌面端
CSS 变量使用 var(--vp-c-*) 变量,确保暗色模式兼容
渐进增强基础内容不依赖 JS,JS 仅增强交互
图片优化使用 WebP 格式,懒加载非首屏图片
性能监控关注 LCP(最大内容绘制)指标
语义化 HTML使用 sectionarticleh1-h6 等语义标签

相关资源

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献