vuepress-theme-wings
v0.5.6
Published
Wings — 新一代 VuePress 博客 / 文档主题,轻量、快速、可组合
Maintainers
Readme
Wings
为 VuePress 2 而生的新一代博客 / 文档主题
轻量 · 快速 · 可组合
特性
架构:完全独立实现
不 extends 默认主题,Layout / Navbar / Sidebar / TOC / 首页 / 404 全部自带。
依赖面收敛到 vuepress + @vuepress/bundler-vite + sass:
- 不受默认主题的
sass-palette弃用告警与 rc 版本 API 漂移影响; - 样式来源唯一(
--wings-*变量),不会与默认主题的变量打架; - 想改一处布局,不必先弄清父主题的实现细节。
博客
- 文章列表(分页)、标签总览(点击筛选)、年月归档(折叠);
- 数据在构建期从
/posts/的 frontmatter 汇总,客户端只做筛选与渲染; - frontmatter 支持
date/tags/categories/description/sticky/cover; - 摘要自动生成,字数与阅读时长中英文分别计数。
文档站
- 多级侧边栏,默认展开层级可配(
sidebarDepth),层级指示线可关; - 右侧目录滚动高亮,点击平滑定位;
- 本地搜索,
⌘/Ctrl + K唤起,索引延迟加载; - 上一页 / 下一页按侧边栏顺序推导,调整目录时自动跟随。
首页
Hero(标题、副标题、图片、深浅色双图)+ actions + features + 最新文章 + 友链 + 页脚, 全部 frontmatter 驱动,主题配置只作为共享默认值。
组件库
13 个全局组件,markdown 里直接写标签即可,不必 import:
WCard WSteps / WStep WTimeline / WTimelineItem WFileTree / WFileTreeItem
WBadge / WBadgeList WIcon WLinkCard WBanner WQuote FriendLinks
成对标签内部的 markdown 会被正常解析 —— 主题改写了 markdown-it 的 HTML 块规则,
把 W 前缀的标签当容器处理,因此卡片里能写加粗、列表与代码块。
集成
- 评论 —— Giscus(GitHub Discussions)与 Waline,运行时按需加载;
- 订阅 —— RSS 2.0 / Atom / JSON Feed,默认只收录文章;
- sitemap —— 生成
sitemap.xml并追加进robots.txt; - 分享卡片 —— Open Graph / Twitter Card /
canonical/ JSON-LD,默认开启; - Algolia DocSearch —— 与本地搜索可同站共存,按语言分组配置凭据;
- 多语言站点模板 —— 一条配置补齐缺失语言的页面骨架与语言切换器。
其它
- 零运行时依赖 —— 主题自身不引入第三方运行时库,样式变量化,客户端按需收集。
- 开箱可用 —— 只填导航与侧边栏即可上线;其余能力有经过取舍的默认值。
- 按语言 / 按路径组合 —— 导航栏与侧边栏支持多语言与路径前缀匹配,一套配置服务多个栏目。
- 本地搜索 —— 内置基于
@vuepress/plugin-search的搜索面板,索引延迟加载,⌘/Ctrl + K唤起。 索引在构建期生成,只收录给人读的正文:代码块整体排除, 行内代码只去掉反引号而保留标识符(siteName这类配置名是可搜的), 表格按行切分,组件属性只保留取值。想调整收录范围改src/shared/search/extract.ts即可,它由单元测试覆盖。 - 深浅色模式 —— 跟随系统,支持手动切换并记忆选择;改动只落在一个
html.dark作用域。 - 可换肤 —— 所有视觉细节收敛为 CSS 变量,改一条
--wings-accent即可换主题色。 - 无障碍 —— 语义化标签、键盘可达、
prefers-reduced-motion与prefers-color-scheme均被尊重。
安装
脚手架(推荐)
npm create wings-theme@latestnpm create wings-theme 会在 npm 上找 create-wings-theme 这个包(一个薄壳,
与主题同版本发布),它转发到主题包里的脚手架 —— 只有一份实现、一套测试。
不想装薄壳也可以直接用主题包自带的入口,两条命令等价:
npx vuepress-theme-wings create-wings-theme依次回答站点标题、描述、语言、域名,得到的是一个空站点 ——
docs/posts/ 下只有列表页,没有任何文章,装完不会多出别人的内容:
├─ package.json
├─ README.md
└─ docs/
├─ .vuepress/config.ts # 标题 / 描述 / 语言 / 导航已按回答填好
├─ index.md # 空首页
└─ posts/ # 只有列表页:posts.md / tags.md / archive.md不提问的写法:
npm create wings-theme@latest -- -y \
--title "我的站点" --description "用 Wings 搭建" --lang zh-CN \
--hostname https://example.com
npm create wings-theme@latest -- --no-blog # 纯文档站,不要博客
npm create wings-theme@latest -- --waline # 顺带打开 Waline 评论生成器也可以当库用(resolveScaffoldOptions / scaffoldFiles / writeScaffold):
import { resolveScaffoldOptions, scaffoldFiles, writeScaffold } from 'vuepress-theme-wings/scaffold'
const options = resolveScaffoldOptions({ title: '我的站点', lang: 'zh-CN' })
await writeScaffold('/path/to/site', scaffoldFiles(options))手动安装
npm i -D [email protected] vuepress-theme-wings @vuepress/[email protected]主题的样式是 SCSS,sass-embedded 写在主题的 dependencies 里,装主题时会一并装上,
不必自己加。
使用
import { viteBundler } from '@vuepress/bundler-vite'
import { defineUserConfig } from 'vuepress'
import { wingsTheme } from 'vuepress-theme-wings'
export default defineUserConfig({
lang: 'zh-CN',
title: '我的站点',
description: '用 Wings 搭建',
bundler: viteBundler(),
theme: wingsTheme({
siteName: '我的站点',
search: 'search',
navbar: [
{ text: '首页', link: '/' },
{ text: '指南', link: '/guide/getting-started.html' },
],
sidebar: {
'/guide/': [
{
text: '指南',
children: [
{ text: '快速开始', link: '/guide/getting-started.html' },
{ text: '主题配置', link: '/guide/configuration.html' },
],
},
],
},
}),
})vuepress dev docs # 本地开发
vuepress build docs # 产出静态文件到 docs/.vuepress/dist配置速览
| 配置 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| siteName | string | 站点 title | 导航栏与页脚名称 |
| hostname | string | — | 站点域名。feed / sitemap / seo 省略各自的 hostname 时回落到它;带子路径时同步到 vuepress 的 base |
| logo / logoDark | string | — | Logo,深色模式可单独指定;建议用透明底 PNG |
| heroImage / heroImageDark | string | — | 首页 Hero 配图,深色模式可单独指定 |
| accentColor | string | #0a84ff | 主题色 |
| colorMode | 'auto' \| 'light' \| 'dark' | 'auto' | 深浅色模式 |
| navbar | 数组 / 对象 / false | — | 导航项,可按语言分组 |
| sidebar | 数组 / 对象 / false | — | 侧边栏,可按路径前缀匹配 |
| toc / tocDepth | boolean / number | true / 2 | 右侧目录与深度 |
| prevNext | boolean | true | 上下篇导航 |
| search | 'search' \| 'docsearch' \| false | false | 搜索 |
| externalLinkIcon | boolean | true | 站外链接图标提示 |
| editLink | 对象 / false | — | 编辑此页 |
| lastUpdated | 对象 / boolean | true | 上次更新时间(需 @vuepress/plugin-git) |
| contributors | 对象 / boolean | true | 贡献者(需 @vuepress/plugin-git) |
| footer | 对象 | — | 页脚 |
| comment | 对象 / false | — | 评论(Giscus / Waline) |
| feed | 对象 / false | — | RSS / Atom / JSON Feed |
| sitemap | 对象 / false | — | sitemap.xml |
| seo | 对象 / false | 开启 | 分享卡片、canonical 与结构化数据 |
| searchLocales | 对象 | — | DocSearch 凭据,按语言分组 |
| i18n | 对象 | — | 多语言站点模板 |
| blog | 对象 / false | — | 博客能力,见下 |
| friendLinks | 数组 | — | FriendLinks 组件默认读取的友链 |
| components | boolean | true | 是否在 markdown 中注册全局组件 |
| sidebarCollapsible | boolean | false | 侧边栏默认是否可折叠 |
| sidebarDepth | number | 1 | 多级侧边栏默认展开到第几级 |
| sidebarGuideLine | boolean | true | 是否展示目录层级指示线 |
| faviconProxy | string | — | WLinkCard 取站点图标的代理地址 |
博客配置:
| 配置 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| blog.postsDir | string | /posts/ | 文章目录,决定从哪些页面收集 |
| blog.pageSize | number | 10 | 列表页每页篇数,0 表示不分页 |
| blog.limit | number | 5 | 首页最新文章数量,0 表示不展示 |
| blog.excerptLength | number | 160 | 摘要截断长度 |
| blog.wordsPerMinute | number | 300 | 估算阅读时长用 |
| blog.sortBy | 'date' \| 'title' | 'date' | 排序字段 |
| blog.nav | 数组 | 内置三栏 | 博客栏目导航 |
集成能力的配置示例:
wingsTheme({
// 评论:四个 ID 在 giscus.app 生成
comment: {
provider: 'giscus',
repo: 'owner/repo',
repoId: 'R_kgDOxxxxxxx',
category: 'Announcements',
categoryId: 'DIC_kwDOxxxxxxxx',
},
// 订阅、sitemap 与分享卡片都需要绝对域名,写一处即可复用
feed: { hostname: 'https://example.com', atom: true },
sitemap: { hostname: 'https://example.com' },
seo: { hostname: 'https://example.com', author: 'cosc' },
// 多语言站点模板:补齐缺失语言的页面
i18n: { enabled: true, locales: ['en', 'zh'] },
})配置不齐时对应的能力安静地不渲染,不会中断构建 —— 可以先写好配置,再去申请各类 ID。
完整类型见 src/shared/options/types.ts,
使用说明见 docs/,集成能力见
评论、订阅与多语言。
关于 git 信息
「上次更新」与「贡献者」由主题内置的 @vuepress/plugin-git 提供,无需额外配置。
两项都通过 lastUpdated / contributors 开关控制,设为 false 时主题不会遍历 git 历史。
::: tip 部署时的 git 历史
这两个字段依赖完整的提交记录。CI 里通常只做浅克隆,记得把 fetch-depth 设为 0,
否则所有页面会显示同一个时间。
:::
项目结构
src/
├─ node/ # Node 端:主题入口、插件装配、构建期数据处理
│ ├─ index.ts
│ ├─ resolveOptions.ts
│ ├─ resolvePlugins.ts
│ ├─ i18n.ts # 多语言站点模板与语言对应关系
│ ├─ searchIndexPlugin.ts # 生成客户端搜索用的 searchindex.json
│ ├─ blog/postPlugin.ts # 从 /posts/ 汇总博客数据
│ └─ markdown/
│ ├─ sidebarPlugin.ts # 侧边栏匹配 + 上下篇推导
│ ├─ containerPlugin.ts # 提示 / 折叠容器
│ ├─ componentPlugin.ts # 组件标签的 markdown 支持
│ └─ componentRule.ts # 让 W 前缀标签成为 markdown 容器
├─ client/ # 浏览器端:布局、组件、组合式函数、样式
│ ├─ config.ts
│ ├─ layouts/ # Layout / Home / NotFound / BlogPosts / BlogTags / BlogArchive
│ ├─ components/ # 13 个全局组件 + 内部组件
│ ├─ composables/
│ └─ styles/
├─ shared/ # 两端共享:类型、语言包、博客 / 搜索数据层(纯函数)
│ ├─ options/ # 类型、默认值、语言包、文案解析
│ ├─ blog/ # collect / query / types
│ └─ search/ # 索引结构 + 检索算法 + 正文提取
└─ templates/ # Html 模板
docs/ # 主题自带的示例 / 文档站点
test/ # 纯逻辑与架构约束测试
scripts/build.js # 构建脚本分层约定
三层各有明确的边界,破坏其中任何一条都不会让构建失败, 但会让站点在浏览器里静默失效:
shared/是两端共用的纯函数,不得 import Node 内置模块或vuepress/core;client/不得触达node/**—— 否则vuepress/core会被打进浏览器包,createRequire在浏览器里不存在,应用会在水合前崩掉;- 需要两端共用的逻辑,放
shared/,各自只保留「必须跑在自己那边」的那一小步 (如搜索:算法在shared/search/,fetch在client/)。
这三条由 test/architecture.test.mjs 自动校验,改代码时不必靠记忆维持。
博客布局
/posts/ 下三个列表页只写 frontmatter:
---
layout: BlogPosts # 或 BlogTags / BlogArchive
title: 文章
article: false # 必需:否则列表页自己也会被当成一篇文章
---开发
pnpm install
pnpm run build # 构建主题到 lib/
pnpm run typecheck # 类型检查
pnpm run test # 单元测试
pnpm run docs:build # 构建文档站点test、docs:build 与 dev 都挂了前置钩子,会先自动构建主题产物(lib/),
新 clone 的仓库直接跑即可,不必手动先 build。
发布
打与 package.json 版本号同名的 Tag 即触发发布流水线:
node -p "require('./package.json').version" # 先确认版本号
git tag 0.2.3
git push origin 0.2.3流水线会依次校验「Tag 名 = 版本号」「CHANGELOG 有对应条目」,再构建、打包,
并检查 package/lib/node/index.js 与 index.d.ts 确实进了 tarball。
是否发布到 npm 取决于仓库密钥里有没有 NPM_TOKEN:
- 有 → 自动
npm publish --access public(版本已存在时静默跳过,可安全重跑旧 Tag); - 没有 → 打包照常完成、流水线保持绿色,只是不上传, tarball 可在构建日志的制品中下载。
这样 fork 或内部仓库在没接凭据时,不会被一条卡在认证上的流水线拖红。
怎么把包发到 npm
发布分两半,一半在仓库里已经做好了,一半需要你去 npm 拿一次凭据。
1. 拿一个 npm token。 登录 npmjs.com → 头像 → Access Tokens → Generate New Token, 选 Automation 类型(它绕过 OTP 交互,否则 CI 会卡在验证码上)。
2. 存进仓库的密钥。 CNB 仓库 → 设置 → 密钥,新增一个名为 NPM_TOKEN 的密钥,
值就是上一步复制的 token。名字必须一字不差 —— 流水线只认这个变量。
3. 打 tag 触发发布。
# 先把 package.json 的 version 与 CHANGELOG.md 的条目改好
git tag 0.5.2 && git push origin 0.5.2流水线会依次校验「Tag 名 = package.json 版本」「CHANGELOG 有该版本条目」,
再构建、打包,然后:
- 有
NPM_TOKEN→npm publish --access public; - 没有 → 只打包,流水线照样绿色,tarball 在构建日志的制品里下载。
4. 想补发某个已打过的 tag。 配好密钥后在 CNB 上重跑那条 tag 流水线即可, 版本已存在于 npm 时会静默跳过,不会报错。
发布包里有什么:lib/(构建产物,含 lib/bin/create-wings-theme.js)、
docs/guide/ 与 docs/index.md(README 里指向的那些文档)。
docs/posts/ 与 docs/advanced/ 是主题自己的示例站点,不进包 ——
使用者装完不该在自己目录里看到别人的文章。
Stage 脚本由
sh(dash)执行,不是 bash。写set -euo pipefail会因为pipefail不是 POSIX 选项而在第一行就报Illegal option退出 —— 本地用 bash 测不出来,只有真正跑一次流水线才会暴露。本仓库统一只用set -eu。
