Skip to content

创建自定义布局

创建自定义布局实现独特的页面展示效果。

布局基础

使用 frontmatter 切换布局

yaml
---
layout: page
---

VitePress 内置三种布局:

  • doc - 文档布局(带侧边栏)
  • page - 简单页面(无侧边栏)
  • home - 首页布局

创建自定义布局

1. 创建布局组件

创建 .vitepress/theme/layouts/CustomLayout.vue

vue
<script setup lang="ts">
import { useData } from 'vitepress'

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

<template>
  <div class="custom-layout">
    <header class="header">
      <h1>{{ frontmatter.title }}</h1>
      <p v-if="frontmatter.subtitle">{{ frontmatter.subtitle }}</p>
    </header>
    
    <main class="content">
      <Content />
    </main>
    
    <footer class="footer">
      <p>© 2026 我的站点</p>
    </footer>
  </div>
</template>

<style scoped>
.custom-layout {
  max-width: 900px;
  margin: 0 auto;
  padding: 2rem;
}

.header {
  text-align: center;
  margin-bottom: 3rem;
}

.header h1 {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

.content {
  min-height: 60vh;
}

.footer {
  margin-top: 4rem;
  padding-top: 2rem;
  border-top: 1px solid var(--vp-c-divider);
  text-align: center;
  color: var(--vp-c-text-2);
}
</style>

2. 注册布局

ts
// .vitepress/theme/index.ts
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './layouts/CustomLayout.vue'

export default {
  extends: DefaultTheme,
  Layout: () => {
    // 根据条件返回不同布局
    return h(DefaultTheme.Layout)
  }
}

// 通过 frontmatter 使用
// 在页面中使用:
// ---
// layout: page
// ---

扩展默认布局

使用插槽

vue
<!-- .vitepress/theme/index.ts -->
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import MyFooter from './components/MyFooter.vue'
import AdBanner from './components/AdBanner.vue'

export default {
  extends: DefaultTheme,
  Layout: () => {
    return h(DefaultTheme.Layout, null, {
      // 文档内容前
      'doc-before': () => h(AdBanner),
      
      // 文档内容后
      'doc-after': () => h(MyFooter),
      
      // 侧边栏前
      'sidebar-nav-before': () => h('div', { class: 'sidebar-tip' }, '提示内容'),
      
      // 导航栏后
      'nav-bar-content-after': () => h('div', { class: 'nav-extra' }, '额外内容')
    })
  }
}

可用插槽

插槽说明
layout-top布局顶部
layout-bottom布局底部
nav-bar-title-before标题前
nav-bar-title-after标题后
nav-bar-content-before导航内容前
nav-bar-content-after导航内容后
sidebar-nav-before侧边栏前
sidebar-nav-after侧边栏后
doc-before文档前
doc-after文档后
aside-top大纲顶部
aside-bottom大纲底部

博客列表布局

vue
<!-- .vitepress/theme/layouts/BlogLayout.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useData } from 'vitepress'

interface Post {
  title: string
  path: string
  date: string
  excerpt: string
  tags: string[]
}

const { frontmatter } = useData()
const posts = ref<Post[]>(frontmatter.value.posts || [])

const selectedTag = ref<string | null>(null)

const filteredPosts = computed(() => {
  if (!selectedTag.value) return posts.value
  return posts.value.filter(p => p.tags.includes(selectedTag.value!))
})

const allTags = computed(() => {
  const tags = new Set<string>()
  posts.value.forEach(p => p.tags.forEach(t => tags.add(t)))
  return Array.from(tags)
})
</script>

<template>
  <div class="blog-layout">
    <header class="blog-header">
      <h1>博客</h1>
      <div class="tags">
        <button
          :class="{ active: !selectedTag }"
          @click="selectedTag = null"
        >
          全部
        </button>
        <button
          v-for="tag in allTags"
          :key="tag"
          :class="{ active: selectedTag === tag }"
          @click="selectedTag = tag"
        >
          {{ tag }}
        </button>
      </div>
    </header>
    
    <div class="posts">
      <article
        v-for="post in filteredPosts"
        :key="post.path"
        class="post-card"
      >
        <time class="date">{{ post.date }}</time>
        <h2 class="title">
          <a :href="post.path">{{ post.title }}</a>
        </h2>
        <p class="excerpt">{{ post.excerpt }}</p>
        <div class="tags">
          <span v-for="tag in post.tags" :key="tag" class="tag">
            {{ tag }}
          </span>
        </div>
      </article>
    </div>
  </div>
</template>

<style scoped>
.blog-layout {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

.blog-header {
  margin-bottom: 2rem;
}

.blog-header h1 {
  font-size: 2rem;
  margin-bottom: 1rem;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.tags button {
  padding: 0.25rem 0.75rem;
  background: var(--vp-c-bg-alt);
  border: 1px solid var(--vp-c-divider);
  border-radius: 16px;
  cursor: pointer;
  transition: all 0.2s;
}

.tags button.active,
.tags button:hover {
  background: var(--vp-c-brand-1);
  color: white;
  border-color: var(--vp-c-brand-1);
}

.post-card {
  padding: 1.5rem 0;
  border-bottom: 1px solid var(--vp-c-divider);
}

.date {
  font-size: 0.85rem;
  color: var(--vp-c-text-3);
}

.title {
  margin: 0.5rem 0;
}

.title a {
  color: var(--vp-c-text-1);
  text-decoration: none;
}

.title a:hover {
  color: var(--vp-c-brand-1);
}

.excerpt {
  color: var(--vp-c-text-2);
  margin-bottom: 0.75rem;
}

.tag {
  font-size: 0.75rem;
  padding: 0.125rem 0.5rem;
  background: var(--vp-c-brand-soft);
  color: var(--vp-c-brand-1);
  border-radius: 4px;
}
</style>

使用自定义布局

在 frontmatter 中指定

yaml
---
layout: ../../.vitepress/theme/layouts/BlogLayout.vue
posts:
  - title: 文章标题
    path: /blog/post-1
    date: 2026-04-01
    excerpt: 文章摘要
    tags: [Vue, VitePress]
---

在 Markdown 中使用组件

markdown
---
layout: page
---

<script setup>
import BlogLayout from '../.vitepress/theme/layouts/BlogLayout.vue'
</script>

<BlogLayout />

学习检查清单

  • [ ] 理解了布局系统
  • [ ] 创建了自定义布局组件
  • [ ] 使用了布局插槽
  • [ ] 实现了博客列表布局
  • [ ] 在页面中应用了自定义布局

下一步

继续学习 开发自定义主题,创建完整的自定义主题。

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献