VitePress 博客首页美化:不换主题,用 CSS 和 Vue 组件打造个性化布局
VitePress 的默认主题是为文档站设计的——干净、专业、信息密度高。但当你把它用作个人博客时,首页的「文档感」就显得有点冷清了。
本文记录如何在不更换主题、不引入第三方美化插件的前提下,通过 CSS 变量、Vue 组件和少量动画,把 VitePress 默认主题改造成一个有卡片布局、个人档案栏和视觉层次的博客首页。整个过程只涉及一个 Vue 组件文件和一个 CSS 文件。
目标与约束
设计上参考了 Vivia 的布局比例:左侧主内容区 + 右侧窄栏,卡片式文章列表,顶部带有醒目的 banner。色系和字体沿用 VitePress 默认主题的 CSS 变量,确保暗色模式自动适配。
约束条件:
- 零第三方依赖:不安装任何美化插件或 UI 库。VitePress 生态中目前没有成熟的博客美化插件(VitePress Blog 等仍处于早期阶段)。
- 不 fork 主题:所有定制通过
custom.css覆盖和 Vue 组件实现,升级 VitePress 时不会产生合并冲突。 - 首页与文章页视觉统一:两个页面共享布局参数(最大宽度、间距、侧栏宽度),用户在页面间切换时不应感到视觉跳动。
布局系统:CSS 变量统一首页与文章页
VitePress 文章页(.VPDoc)有自己的宽度控制逻辑,首页是完全自定义的 Vue 组件。如果各定各的宽度,用户在首页点进文章时会感到内容区突然变宽或变窄。
解决方案是在 :root 中定义一组共享的布局 token:
:root {
--vp-layout-max-width: 1560px;
--blog-page-max-width: 1260px;
--blog-home-max-width: 1180px;
--blog-content-max-width: 940px;
--blog-aside-width: 250px;
--blog-column-gap: 40px;
}--blog-home-max-width控制首页整体宽度。--blog-content-max-width控制文章页正文区宽度。--blog-aside-width同时用于首页侧栏和文章页右侧 Outline。--blog-column-gap统一主内容和侧栏之间的间距。
首页 .blog-home 直接引用这些变量:
.blog-home {
max-width: var(--blog-home-max-width, 1180px);
margin: 0 auto;
padding: 34px 20px;
}
.home-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--blog-aside-width, 250px);
gap: var(--blog-column-gap, 40px);
align-items: start;
}文章页通过 custom.css 覆盖默认主题的宽度:
.VPDoc.has-aside .content-container {
max-width: var(--blog-content-max-width) !important;
}
.VPDoc.has-aside .content {
max-width: var(--blog-content-max-width) !important;
}
.VPDoc.has-aside .aside {
width: var(--blog-aside-width);
padding-left: 24px;
}这里用 !important 是因为 VitePress 默认主题的 CSS 优先级较高,正常覆盖不生效。这是在不 fork 主题的约束下不得已的选择。
一个调试过程中的教训:最初我把首页宽度设得和文章页一样(1260px),结果首页的侧栏看起来太挤——文章列表卡片占了太多空间。最终选择首页比文章页窄 80px(1180px vs 1260px),视觉上反而更均衡,因为首页没有 Outline 只有窄的 widget 栏。
首页 Banner
首页顶部的 Banner 用渐变背景和品牌色营造视觉焦点:
.home-banner {
border-radius: 26px;
padding: 28px 30px 24px;
background:
radial-gradient(circle at 12% 22%,
color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent),
transparent 42%),
radial-gradient(circle at 88% 12%,
color-mix(in srgb, var(--vp-c-brand-3) 15%, transparent),
transparent 38%),
var(--vp-c-bg-soft);
border: 1px solid color-mix(in srgb, var(--vp-c-brand-1) 20%, var(--vp-c-divider));
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.06);
}这里大量使用了 color-mix() 函数——它允许将品牌色和透明色按比例混合,生成半透明的渐变层。好处是换品牌色时渐变自动跟着变,不需要手动调颜色。
var(--vp-c-brand-1) 是 VitePress 默认主题的主色,暗色模式下会自动切换,所以 Banner 在两种模式下都能正常显示。
文章卡片
每篇文章渲染为一个带圆角、边框和微妙阴影的卡片:
.post-card {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 22px;
padding: 25px 28px;
margin-bottom: 20px;
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.post-card:hover {
transform: translateY(-3px);
border-color: color-mix(in srgb, var(--vp-c-brand-1) 30%, var(--vp-c-divider));
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.1);
}Hover 效果三管齐下:轻微上移(translateY(-3px))、边框变为品牌色、阴影加深。三者都使用 transition 而非 animation,确保只在交互时触发,不产生持续的性能开销。
文章标题使用衬线字体,和正文的无衬线字体形成对比:
.post-title {
font-family: "Palatino Linotype", "Noto Serif SC", "Source Han Serif SC", serif;
font-size: 1.62rem;
letter-spacing: 0.01em;
}Palatino Linotype 覆盖 Windows,Noto Serif SC / Source Han Serif SC 覆盖中文,fallback 到系统 serif。
个人档案卡片
右侧栏顶部是一个个人档案卡片,包含头像、姓名、一句话简介和文章/分类计数。
头像使用了两个动画效果:
光晕脉冲(Glow Pulse):头像外围一个模糊的光圈,持续呼吸式放大缩小。
.avatar-glow {
position: absolute;
inset: -8px;
border-radius: 50%;
background: radial-gradient(circle,
color-mix(in srgb, var(--vp-c-brand-1) 35%, transparent),
transparent 68%);
filter: blur(10px);
animation: glowPulse 2.8s ease-in-out infinite;
}
@keyframes glowPulse {
0%, 100% { opacity: 0.5; transform: scale(0.95); }
50% { opacity: 0.9; transform: scale(1.08); }
}悬浮感(Float):整个头像区域缓慢上下浮动。
.avatar-wrap {
animation: floatUpDown 3s ease-in-out infinite;
}
@keyframes floatUpDown {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}两个动画的周期略有错开(2.8s vs 3s),避免同步导致的机械感。动画幅度都很小(scale 0.95-1.08、translateY -4px),确保不喧宾夺主。
档案卡的背景从顶部的品牌色渐变到底部的默认背景色:
.profile-card {
background: linear-gradient(180deg,
color-mix(in srgb, var(--vp-c-brand-1) 12%, transparent) 0%,
var(--vp-c-bg-soft) 38%);
}Widget 栏
侧栏的分类、归档、最近文章三个 Widget 共享统一的卡片样式:
.profile-card,
.widget {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
padding: 18px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}分类和归档的数量显示为一个药丸形的 badge:
.badge {
min-width: 1.5em;
text-align: center;
border-radius: 999px;
background: color-mix(in srgb, var(--vp-c-brand-1) 16%, var(--vp-c-bg));
color: var(--vp-c-brand-1);
padding: 0 0.45em;
font-size: 0.78rem;
}「最近文章」Widget 有一个文本溢出问题:部分文章标题很长,在 250px 宽的侧栏里会溢出。解决方案是限制为两行,超出部分截断:
.recent-list a {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
overflow-wrap: anywhere;
word-break: break-word;
}同时在 HTML 标签上加 title 属性,鼠标悬停时显示完整标题:
<a :href="withBase(post.url)" :title="post.title">{{ post.title }}</a>Sticky 侧栏
侧栏使用 position: sticky 固定在视口中,滚动时始终可见:
.side-column {
position: sticky;
top: calc(var(--vp-nav-height) + 20px);
display: flex;
flex-direction: column;
gap: 16px;
}top 值使用 VitePress 的 --vp-nav-height 变量加 20px 间距,确保侧栏不会被顶部导航栏遮挡。
文章页排版优化
除了首页,文章页的排版也做了调整。在 custom.css 中覆盖默认的字体大小、行高和间距:
.VPDoc .vp-doc {
font-size: 16.5px;
}
.VPDoc .vp-doc p {
line-height: 1.9;
margin: 1em 0;
}
.VPDoc .vp-doc h1 {
margin-top: 0.2em;
margin-bottom: 0.75em;
}
.VPDoc .vp-doc h2 {
margin-top: 1.5em;
}
.VPDoc .vp-doc div[class*='language-'] {
border-radius: 14px;
margin: 1.2em 0;
}主要变化:
- 正文行高从默认的 1.7 调到 1.9:中文排版需要更大的行间距才舒适。
- 代码块圆角 14px:和卡片的大圆角风格统一。
- H1 底部间距缩小:默认主题的 H1 下方空白太多,作为文章标题时显得松散。
加密文章在首页的展示
加密文章在首页列表中需要特殊处理。如果直接显示摘要,就泄露了加密文章的内容;如果统一显示「此文章已加密」,又不利于用户判断是否值得解密。
最终的方案是基于 Session 状态动态切换:
<p class="post-excerpt">
<template v-if="post.isPrivate && !sessionValid">
此文章已加密,请输入密码查看。
</template>
<template v-else>
{{ post.excerpt }}
</template>
</p>sessionValid 在 onMounted 中检查 localStorage 是否有有效的解密 Session。如果用户已经在其他文章中输入过正确密码(Session 未过期),首页就显示真实摘要;否则显示加密提示。
响应式适配
在小于 1024px 的屏幕上,栅格布局退化为单列,侧栏从 sticky 变为 static:
@media (max-width: 1024px) {
.home-grid {
grid-template-columns: 1fr;
gap: 22px;
}
.banner-title {
font-size: 1.65rem;
}
.side-column {
position: static;
}
}文章页也有对应的响应式处理。VitePress 默认主题在 960px 以下会隐藏右侧 Outline,这一行为不需要额外处理。
暗色模式
所有颜色都通过 VitePress 的 CSS 变量引用,暗色模式下自动切换:
--vp-c-bg-soft:亮色下为浅灰,暗色下为深灰。--vp-c-text-1/2/3:三级文字颜色,暗色下自动反转。--vp-c-brand-1/2/3:品牌色在暗色模式下略有调整。--vp-c-divider:分割线颜色。
唯一需要注意的是阴影。box-shadow 使用的 rgba(0, 0, 0, ...) 在暗色背景下不明显。但由于卡片有 border,视觉上仍然有足够的层次感,所以没有做暗色模式下的阴影特殊处理。
总结
整个美化过程涉及的文件:
| 文件 | 作用 |
|---|---|
BlogHome.vue | 首页布局、卡片、档案栏、Widget |
custom.css | 布局变量定义、文章页样式覆盖 |
theme/index.ts | 引入 custom.css |
public/images/avatar.jpg | 头像资源 |
核心原则是用 VitePress 给的接口做事:CSS 变量覆盖、Layout Slot、Vue 全局组件。没有 monkey-patch 默认主题的任何 JS 逻辑,没有 fork 主题源码,没有引入第三方 UI 框架。
这意味着 VitePress 版本升级时,只要 CSS 变量名和 Layout Slot 没有 breaking change,美化代码就不需要改动。如果未来出现成熟的 VitePress 博客主题或插件生态,也可以随时替换而不影响文章内容本身。