搜索 K
主题
@vaultkit/vitepress-plugin-copyright 是文章版权声明卡 Vue 组件,挂在正文之后——那是「转载须知」的标准位。卡片三行信息一目了然:文章标题 + 规范链接(带一键复制)、作者(可链到关于页)、许可协议(可链到协议原文),另可选一句转载须知整句。滚到本页末尾就能看到它的现场效果——本站每篇文章底部的版权卡正是本组件渲染的。
实现上是「零依赖纯渲染 + 原生 Clipboard API」:SSR 输出确定性 HTML 无水合差异,复制按钮的交互态全在客户端,复制失败静默降级不炸页面。本文覆盖全部导出、全部 props(含 labels 八个字段逐一说明)与设计细节。
中文技术博客的内容被无署名搬运是常态。版权声明卡不能从技术上阻止搬运,但它做三件有实际价值的事:
社区常见做法是在主题里手写一段 HTML,但字段成链逻辑(作者有无主页、协议有无原文链接)、复制交互、明暗配色每个站点都要重写一遍。本组件把这些封装成一个零依赖组件,peer 只有 vue ^3.5.0。
pnpm add -D @vaultkit/vitepress-plugin-copyright使用 @vaultkit/vitepress-theme-vault 主题则开箱即带(主题内 NoteCopyright 适配层负责从站点配置组装 props)。
推荐挂在 Layout 的 #doc-after 插槽(正文之后、评论区之前),用 useData 接线页面数据:
<script setup lang="ts">
import { CopyrightCard } from '@vaultkit/vitepress-plugin-copyright'
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
const { page, site, frontmatter } = useData()
</script>
<template>
<DefaultTheme.Layout>
<template #doc-after>
<CopyrightCard
v-if="frontmatter.layout !== 'home'"
:url="`https://example.com/${page.relativePath.replace(/\\.md$/, '')}`"
:title="page.title"
:author="site.title"
author-url="/about"
:license="{
name: 'CC BY-NC-SA 4.0',
url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans',
}"
:labels="{
heading: '版权声明',
link: '本文链接',
author: '作者',
license: '许可协议',
copy: '复制链接',
copied: '已复制',
notice: '转载或引用本文时请注明作者及出处,不得用于商业用途。',
aria: '版权声明',
}"
/>
</template>
</DefaultTheme.Layout>
</template>本包发布的是含 .vue 源文件的产物,消费方 VitePress 项目必须把它加进 vite.ssr.noExternal,否则 SSR 构建会报错:
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
vite: { ssr: { noExternal: ['@vaultkit/vitepress-plugin-copyright'] } },
})| 导出 | 形态 | 说明 |
|---|---|---|
CopyrightCard | Vue 组件(具名 + 默认导出) | 版权卡本体 |
CopyrightLicense | 类型 | { name, url? } |
CopyrightLabels | 类型 | 八字段文案注入 |
string,必填const canonical = `https://example.com${route.path}`string,默认 ''url 本身——能用就是丑,强烈建议传 page.title,让第一行读作「本文链接:文章标题」。string,必填string,默认 ''/about 或社交主页)。缺省不成链,作者名渲染为纯文本;传值后成链,把「这是谁写的」变成可点击的动线。CopyrightLicense({ name: string, url?: string }),必填name 是协议名(如 CC BY-NC-SA 4.0);url 可选,传了之后协议名成为外链并以 target="_blank" rel="noopener" 新窗口打开(协议原文是外站,不该抢走当前页)。中文站点建议直接链中文 deed:const license = {
name: 'CC BY-NC-SA 4.0',
url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans',
}保留所有权利的站点可以只传 { name: '© 2026 作者名,保留所有权利' },不给 url 即不成链。
CopyrightLabels,默认 {}(各字段回退英文)| 字段 | 默认值 | 位置与作用 |
|---|---|---|
heading | 'Copyright' | 卡片标题行(前置 lucide 版权图标) |
link | 'Link' | 第一行字段名(本文链接) |
author | 'Author' | 第二行字段名(作者) |
license | 'License' | 第三行字段名(许可协议) |
copy | 'Copy link' | 复制按钮的 aria-label / title |
copied | 'Copied' | 复制成功后的提示文字(role="status" 播报) |
notice | 无(缺省不渲染) | 卡片底部的转载须知整句,虚线分隔 |
aria | 'Copyright notice' | 外层 <aside> 容器的 aria-label |
中文文案完整示例:
const labels = {
heading: '版权声明',
link: '本文链接',
author: '作者',
license: '许可协议',
copy: '复制链接',
copied: '已复制',
notice: '转载或引用本文时请注明作者及出处,不得用于商业用途。',
aria: '版权声明',
}注意 notice 的特殊性:其他字段缺省回退英文,而 notice 缺省整行不渲染——转载须知是可选的强调句,不写就干净地没有,不会冒出一句英文占位。
复制按钮用原生 navigator.clipboard.writeText,不引任何剪贴板库。关键在失败路径的处理:
async function copyLink() {
try {
await navigator.clipboard.writeText(props.url)
}
catch {
return // 静默降级:不炸页面、不弹错误
}
copied.value = true
// 2 秒后复位……
}Clipboard API 在两种场景会拒绝:非安全上下文(HTTP 站点)与权限被拒。组件的态度是静默返回——按钮不进入「已复制」态(不撒谎),页面也不抛未捕获异常。「已复制」提示 2 秒后自动复位,重复点击会重置计时器(clearTimeout 后重新计时),组件卸载时清理定时器不泄漏。
成功反馈是三重的:图标从「复制」切为「对勾」、出现 copied 文字、且该文字带 role="status"——屏幕阅读器会在不打断焦点的前提下播报「已复制」,视觉与听觉用户获得同等反馈。
组件唯一的客户端状态是 copied,初始恒为 false——SSR 输出与客户端首帧完全一致,无水合差异。这也是它敢直接挂在每篇文章底部的原因:不需要 <ClientOnly> 包裹。
三行信息用的是 <dl> 定义列表(<dt> 字段名 / <dd> 值),外层是 <aside>——对读屏用户,这是「与正文相关但可跳过的补充信息」的正确语义。配色全部走 --vp-c-* 变量(软底、品牌色左边线、三级文字色阶),明暗两态零配置跟随主题。长 URL 经 overflow-wrap: anywhere 断行,不会把移动端布局撑爆。
#doc-after 顺序是赞赏(转化入口)→ 版权(转载须知)→ 评论,版权卡 margin-top: 48px 与正文拉开呼吸感frontmatter.license 动态传不同协议Q:点复制没反应? 最常见原因是站点跑在 HTTP 上——Clipboard API 仅在安全上下文(HTTPS / localhost)可用,组件按设计静默降级。
Q:转载须知那行怎么不见了?labels.notice 没传。它缺省不渲染,传整句才出现。
Q:想要「本文由 XX 创作,采用 XX 协议」的整段式声明而不是三行表? 那是另一种排版形态,本组件专注结构化三行卡。整段式用 notice 字段承载大部分语义也能凑合,或直接在主题里写静态段落。
Q:SSR 构建报 .vue 无法解析? 没配 vite.ssr.noExternal,见上文安装一节。