使用 vitepress 搭建个人博客
前言
vitepress 是一个比较新的静态网页构建工具, 用来搭建个人博客的话可能没有使用 hexo, hugo 这些工具方便, 主题感觉也没有其它的丰富, 不过胜在个性化强, 编写也比较容易, 可以用 vue3 来写.
简单在这里写一下用 vitepress 搭建的教程.
初始化一个 vitepress 项目
我使用的是 pnpm, 如果需要安装的话可以执行下面的命令
npm install -g pnpm
在你想要放博客项目的文件夹里打开命令行, 输入
pnpm add -g vitepress
然后执行
vitepress init
然后会问你一些问题
- Where should VitePress initialize the config?
按需填写路径, 我是直接./
表示在当前目录安装 vitepress 框架 - Site title
网站名称 - Site description
网站描述 - Theme
按需选择 - Use TypeScript for config and theme files
是否启用 ts, 按需选择 - Add VitePress npm scripts to package.json
这个建议选择 yes, 手动加也可以
如果按官方文档(不全局安装 vitepress 库)里安装的话, 需要先pnpm init
初始化项目, 再pnpm add -D vitepress
安装 vitepress 库, pnpm vitepress init
.
vitepress 的布局
vitepress 提供了三种布局, doc
, home
, page
.
- home:vitepress 生成的初始网页首页就是 home 布局
- doc:vitepress 生成的初始网页文章就是 doc 布局
- page:空白页, 用于写自己的布局
在 frontmatter 里可以声明布局, 不声明默认是 doc 布局.
---
layout: home
---
可以在初始网页参考一下哪些页面需要哪种布局.
也可以用自定义布局, 需要在.vitepress/theme/index.js 里注册布局
import DefaultTheme from 'vitepress/theme'
import YourLayout from './YourLayout.vue'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('YourLayout', YourLayout)
}
}
我目前的博客使用的都是doc布局, 感觉博客没啥用home布局的必要(个人观点).
设置路由
createContentLoader
vitepress 不像 hexo, 只要在什么地方放文章.md 就可以了, 它需要自己设置路由.
进入.vitepress/config.mjs(如果用了 ts 那应该是 mts), 在 themeConfig 里能看到 nav 和 sidebar 之类的东西.
- nav 顶部导航栏
- sidebar 左导航栏, 类似目录
- socialLinks 社交应用链接, github, facebook 之类的东西
自己设置 nav 还好, 基本也就首页, 归档, 友链那几个. 但是文章路由手动设置就比较麻烦了, 每次写都要多写点东西到路由里.
vitepress 提供了一个函数可以提取某一文件夹的所有文件, 可以用它来自动配置.
这份代码应该命名为 xxx.data.mjs, 表示构建时执行.
import { createContentLoader } from 'vitepress';
export default createContentLoader('posts/**/*.md', {
transform(raw) {
const posts = raw
.map(({ url, frontmatter, html }) => {
return {
title: frontmatter.title,
url,
description: frontmatter.description || undefined,
date: formatDate(frontmatter.date),
tags: frontmatter.tags,
id: frontmatter.id || 0,
};
})
.sort((a, b) => {
if (b.date.time == a.date.time) {
return b.id - a.id;
}
return b.date.time - a.date.time;
});
return posts;
},
});
function formatDate(raw) {
const date = new Date(raw);
date.setUTCHours(12);
return {
time: +date,
string: date.toLocaleDateString('zh-Hans', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}),
year: date.toLocaleDateString('zh-Hans', {
year: 'numeric',
}),
monthDay: date.toLocaleDateString('zh-Hans', {
month: '2-digit',
day: '2-digit',
}),
};
}
这里的 frontmatter 是 markdown 文章开头的配置, 一般是这样的
--- title: example description: example date: 2024-11-30 tags: - example - asd --- 如果有需要还可以添加/删除其它属性, 也可以写成json
createContentLoader
的第一个参数是 string 或者 string[], 表示路径;第二个参数应该是一个 Object
- transform: Function 接收一个文件内容数组 raw: ContentData[], 在这份代码里就是文章数组, 里面存放有文章信息的对象
interface ContentData {
// 页面的映射 URL, 如 /posts/hello.html(不包括 base)
// 手动迭代或使用自定义 `transform` 来标准化路径
url: string
// 页面的 frontmatter 数据
frontmatter: Record<string, any>
// 只有启用相关选项才会传入
src: string | undefined
html: string | undefined
excerpt: string | undefined
}
- includeSrc:启用后会传入 src(文件路径)
- render: 启用后会传入渲染后的 html
- excerpt: 为 true 则会传入渲染的摘录HTML, 指的是文章第一个---上面的内容;为 Function 则表示怎么从内容提取摘录;为 string 则定义用于提取摘录的自定义分隔符
<!-- 为true的情况 -->
## hello world
---
title: example
description: example
date: 2024-11-30
---
hello world
---
这里得到的就是<p>hello world</p>
, 注意不要占用frontmatter的---, 否则会出现错误.
## <!-- 为字符串'excerpt'的情况 -->
title: example
description: example
date: 2024-11-30
---
hello world
excerpt
这里得到的也是<p>hello world</p>
, 不会提取 frontmatter. 这里可能需要注意一下, 提取的是分隔符之前所有内容(不包括 frontmatter), 不要用两个分隔符来包裹 excerpt, 如果要这样做的话就得用函数了.
excerpt:(file)=>{
file.excerpt = file.content.split('excerpt')[1]
},
这个函数会按excerpt
切开字符串, 如果是获取第一个被excerpt
包裹的东西的话, 获取第二个元素就可以了.
---
title: example
description: example
date: 2024-11-30
---
excerpt
hello world
excerpt
这样也会得到<p>hello world</p>
这里有一个建议, 就是每篇文章都尽量写 description, 有益于 SEO. vitepress 会将 description 放在页面的元数据里, 要是不写的话就默认是 config.mjs 里的 description 了, 不利于 SEO.
<meta
name="description"
content="aaa"
/>
使用导出的数据
这个数据可以放在归档页面, 也可以放在首页, 喜欢就好.
<template v-for="post in posts" :key="post.url">
<h2
:id="post.title"
class="post-title"
>
<a
:href="post.url"
class="title"
>{{ post.title }}</a
>
<a
class="header-anchor title"
:href="`#${post.title}`"
:aria-label="`Permalink to "${post.title}"`"
></a
>
<div
id="date"
class="post-date hollow-text"
:data="post.date.string"
></div>
</h2>
<span
v-for="tag in post.tags"
class="tag"
>{{ tag }}</span
>
<p v-if="post.description">{{ post.description }}</p>
</template>
<script setup>
import { data as posts } from './.vitepress/theme/posts.data.mjs';
</script>
这里可以做成分页, 也可以做成瀑布式布局, 看个人喜欢.
使用插槽
插槽是一个很强大的东西, 即使不自己写布局也可以扩展很多. 他让你只需要写一些组件就可以插入到页面的某一个地方.
doc布局有这些插槽
- doc-top 文章顶部, 宽度不是文章容器宽度而是页面宽度, 在doc-before上面
- doc-bottom 页面底部, 宽度不是文章容器宽度而是页面宽度, 在doc-after下面
- doc-footer-before 在footer和最后更新时间上面
- doc-before 宽度与文章容器宽度相同
- doc-after 宽度与文章容器宽度相同, 在footer上面, 最后更新时间下面
- sidebar-nav-before 在sidebar里, 导航上面
- sidebar-nav-after 在sidebar里, 导航下面
- aside-top 页面导航上面
- aside-bottom 页面导航下面
- aside-outline-before 没测试出来, 也是在页面导航上面
- aside-outline-after 没测试出来, 也是在页面导航下面
- aside-ads-before
- aside-ads-after
然后在index.js里插入组件
import { h } from 'vue';
import Theme from 'vitepress/theme-without-fonts';
import { useRoute } from 'vitepress';
import BlogComment from './components/BlogComment.vue';
import Avatar from './components/Avatar.vue';
import PostInfo from './components/PostInfo.vue';
import './style.css';
import './custom.css';
export default {
...Theme,
Layout() {
const route = useRoute();
const notShowComment =
route.path === '/' ||
route.path === '/archive' ||
route.path === '/about';
const notShowAside = route.path.includes('/notes/');
return h(Theme.Layout, null, {
'doc-before': () => h(PostInfo),
'aside-top': () => (notShowAside ? null : h(Avatar)),
'doc-after': () => (notShowComment ? null : h(BlogComment)),
});
},
enhanceApp({ app, router }) {
},
};
除了doc有插槽, home和page也有一些插槽. 具体可以在官方文档里查看, 我这两种布局都没有使用就不写了.
我把我目前的博客放在GitHub上了(不包含文章和笔记), 如果有需要可以拿走.