自定义首页开发
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 配置项
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 主标题上方的大字 |
text | string | 主标题 |
tagline | string | 副标题 |
image.src | string | Hero 图片路径 |
image.alt | string | 图片 alt 文本 |
actions | array | 操作按钮列表 |
Features 配置项
| 字段 | 类型 | 说明 |
|---|---|---|
icon | string | 图标(Emoji 或组件名) |
title | string | 特性标题 |
details | string | 特性描述 |
link | string | 点击跳转链接 |
linkText | string | 链接文本 |
限制
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 DefaultThemecss
/* .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 | 使用 section、article、h1-h6 等语义标签 |