跳转到内容
关系图谱

nuxt-typography:card、callout、steps 排版容器完全指南

2,651 字 10 分钟

属性 1
series
VaultKit 包指南

技术博客写多了就会发现,光有正文和代码块不够用:推荐阅读要卡片、重点提示要色块、安装教程要步骤列表、长配置文件要能折叠。Nuxt 官方文档的 Prose 组件把这套排版语言打磨得很成熟,@vaultkit/vitepress-plugin-nuxt-typography 把它带进 VitePress:五种容器(card / card-group / callout / steps / code-collapse),视觉逐槽位复刻 @nuxt/ui,但实现是构建期纯 HTML + CSS——不带 40+ Prose 组件进首屏,图标也不走运行时 CDN。

本文是「VaultKit 包指南」系列第三篇。每个容器逐属性详解并实活演示(正文里的容器都是本站构建时真实渲染的),最后附一份构建期报错清单——这个插件信奉 fail-fast,写错属性不会静默吞掉,而是让构建直接失败。

为什么需要它

直接复用 @nuxt/ui 的 Prose 组件行不行?行,但对静态站点是净损耗:Vue 模式的 Prose* 依赖 prose: true(全部 40+ Prose 组件打进首屏 bundle),图标走 @iconify/vue 运行时从 CDN 拉取。本插件反其道而行:

  • card / card-group / callout / steps 是构建期纯 HTML + CSS,客户端零 JS;
  • 只有 code-collapse 有交互态,发射一个轻量客户端组件;
  • 图标构建期内联 SVG:离线可用、SSR 直出、名称写错构建即报错;
  • fence 零侵入:code-collapse 里的代码块仍由宿主 shiki 管线渲染,高亮和复制按钮不受影响;
  • 颜色全部取 VitePress 语义变量(--vp-c-*),明暗模式自动翻转。

安装

sh
pnpm add -D @vaultkit/vitepress-plugin-nuxt-typography

如果你用的是 @vaultkit/vitepress-theme-vault 主题,本插件开箱即带markdown.typography 缺省开启,simple-icons 图标集与中文按钮文案已配好),可跳过下面的接入步骤。

快速上手

ts
// .vitepress/config.ts
import { icons as simpleIcons } from '@iconify-json/simple-icons'
import { nuxtTypography } from '@vaultkit/vitepress-plugin-nuxt-typography'
import { defineConfig } from 'vitepress'

export default defineConfig({
  markdown: {
    config(md) {
      md.use(nuxtTypography, {
        icons: { 'simple-icons': simpleIcons },
        codeCollapse: { name: '代码', openText: '展开', closeText: '收起' },
      })
    },
  },
  vite: {
    // client 部分含 .vue 源文件,SSR 需交给打包器处理
    ssr: { noExternal: ['@vaultkit/vitepress-plugin-nuxt-typography'] },
  },
})
ts
// .vitepress/theme/index.ts
import { NuxtTypographyClient } from '@vaultkit/vitepress-plugin-nuxt-typography/client'
import '@vaultkit/vitepress-plugin-nuxt-typography/styles/index.css'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    app.use(NuxtTypographyClient) // 全局注册 <CodeCollapse>
  },
}

语法契约一句话版:容器名后可跟标题(仅 card 有标题槽),属性一律包在行尾花括号里(key="value" 形式),嵌套时外层容器多一个冒号。

card:卡片

card 是最常用的容器:标题直接跟在容器名后(走 info 尾巴,与 ::: info 标题 同轨,可省略),正文是普通 Markdown。花括号属性共 4 个:

属性说明
icon左上角图标,构建期内联 SVG(写法见下文「双图标写法」)
to链接目标,整卡可点;http(s) 外链缺省新开页 + 右上角角标
hrefto 的别名,二选一(同时给时 to 优先)
target显式覆盖打开方式(如外链改 _self、站内链接改 _blank

全属性演示(外链自动 target="_blank",注意右上角的外链角标):

VitePress

标题、图标、链接俱全的卡片。正文支持 Markdown双链,整卡可点,卡内的行内链接照常独立可点。

hreftarget 覆盖演示(外链但显式 target="_self",本页打开、无角标):

Obsidian

href 等价于 to;显式 target 压过外链缺省的 _blank——角标只在最终 _blank 时出现。

标题与属性都可省略,退化成一个边框容器:

无标题、无属性的极简卡片,只放一段说明文字。

card-group:卡片组与 :::: 嵌套

card-group 自身没有任何专属属性,只是一个两列网格包裹层(移动端单列)。它演示了本插件的嵌套规则:外层容器要比内层多一个冒号——:::::::

md
:::: card-group
::: card 卡片一 {icon="i-lucide-notebook-pen"}

:::
::: card 卡片二 {icon="i-lucide-panels-top-left"}

:::
::::

实活效果:

写作端

Obsidian 负责双链、callouts 与本地优先的写作体验。

发布端

VitePress 负责构建期渲染与静态部署。

callout:单块提示

注意与 Obsidian 的 > [!type] callout 区分:那是带标题的引述块(见 上一篇),这里是 Nuxt 风格的无标题槽单块提示——正文直接写在容器里,给它写标题会构建报错。花括号属性共 5 个:

属性说明
icon行首内联小图标,与首行文字齐排
to链接目标,整块可点,链接态虚线边框
hrefto 的别名,二选一
target显式覆盖打开方式
color色系,7 个值:neutral(缺省)/ primary / secondary / info / success / warning / error

七个颜色值逐色演示:

neutral——缺省值,中性底色 + 分割线边框,不抢戏的补充说明。

primary——品牌色(--vp-c-brand-1),主推操作或核心提示。

secondary——紫色系,次级强调。

info——靛蓝色系,客观信息。

success——绿色系,成功与最佳实践。

warning——黄色系,注意事项。

error——红色系,错误与破坏性操作。

非 neutral 色系的边框 / 底色由主色 color-mix 派生(25% 边框、10% 底色),行内代码自动贴合底色。带 to 时整块可点、边框变虚线,外链同样有右上角角标:

带外链的 callout:虚线边框、整块可点、右上角新开页角标。正文里的 加粗 与行内元素照常渲染。

站内链接(to="/tags"):同样整块可点,但不会新开页、无角标。

steps:步骤列表

steps 给容器内对应级别的直系标题挂序号圆点与左侧连接线,标题之间的任意块级内容都是步骤正文。唯一属性:

属性说明
level步骤标题级别,值域 2 / 3 / 4,缺省 3(与 ProseSteps 一致)

level 的语义是「哪一级标题算一步」:序号由 CSS 计数器实现(counter-increment),只命中 data-level 对应的直系子标题,其他级别的标题与正文不编号。级别应顺承上文标题层级——本节是三级标题的语境,所以下面用 {level="4"};跳级虽然能渲染,但会破坏文档大纲的无障碍语义。

演示:三步接入

安装依赖

sh
pnpm add -D @vaultkit/vitepress-plugin-nuxt-typography

注册 markdown-it 插件

ts
md.use(nuxtTypography, { icons: { 'simple-icons': simpleIcons } })

引入样式与客户端组件

ts
import { NuxtTypographyClient } from '@vaultkit/vitepress-plugin-nuxt-typography/client'
import '@vaultkit/vitepress-plugin-nuxt-typography/styles/index.css'

steps 与 card 一样没有标题槽——::: steps 标题 会构建报错,步骤标题写在容器内部。

code-collapse:折叠代码块

唯一发射客户端组件的容器:长代码默认收起 200px + 底部渐变遮罩,点按钮展开(展开态上限 80vh 可滚动)。代码块本身仍由宿主 fence 管线渲染,shiki 高亮、行号、复制按钮全部保留。花括号属性共 3 个:

属性说明
name按钮文案里的对象名(「展开代码」的「代码」位)
open-text收起态按钮动词(缺省 Expand,可经插件选项站点级覆盖)
close-text展开态按钮动词(缺省 Collapse,同上)

实活演示(自定义三个属性):

ts
type DateChain = 'date' | 'created' | 'published' | 'start'

export function resolveDate(frontmatter: Record<string, unknown>): string | undefined {
  const chain: DateChain[] = ['date', 'created', 'published', 'start']
  for (const key of chain) {
    const value = frontmatter[key]
    if (typeof value === 'string' && value)
      return value
  }
  return undefined
}

export function compareByDateDesc(a: { date?: string }, b: { date?: string }): number {
  return (b.date ?? '').localeCompare(a.date ?? '')
}

按钮文案的合并顺序:行内属性 > 插件选项 codeCollapse > 英文缺省。中西文混排时组件会自动决定动词与对象名之间是否补空格(Expand code 补、「展开代码」不补)。

双图标写法与 icons 选项

icon 属性接受 Nuxt UI 的两种写法,等价互换:

  • 连字符式i-simple-icons-github(按已注册集合前缀匹配,长名优先);
  • 冒号式simple-icons:github

内置 lucide 集,其余集合经插件选项 icons 注册(数据源 @iconify-json/<collection>):

ts
import { icons as carbonIcons } from '@iconify-json/carbon'
import { icons as simpleIcons } from '@iconify-json/simple-icons'

md.use(nuxtTypography, {
  icons: { 'simple-icons': simpleIcons, 'carbon': carbonIcons },
})

插件选项全表:

选项类型缺省说明
iconsRecord<string, IconifyJSON>内置 lucide额外图标集,注册后两种图标写法都可用
codeCollapse{ name?, openText?, closeText? }'code' / 'Expand' / 'Collapse'code-collapse 按钮站点级缺省文案(作者行内属性优先)

此外所有容器都接受 class / id 直通属性({class="my-card" id="anchor"}),用于自定义样式钩子与锚点。

构建期报错清单

写错不会静默降级,而是构建直接失败并给出定位信息——这是刻意设计(错误排版上线比构建失败更贵)。全部触发条件:

写法报错
属性键不在白名单(如 card 写 color未知的 card 属性 "color"(支持:icon / to / href / target)
callout 的 color 不在 7 值内未知的 callout 颜色 "brand"(支持:neutral / primary / …)
steps 的 level 不在 2/3/4非法的 steps 级别 "5"(支持:2 / 3 / 4)
给 callout / steps / code-collapse 写标题callout 没有标题槽(正文直接写在容器内部)
忘写花括号(标题区出现 key="value"属性需包在行尾花括号里(::: card 标题 {icon="…"})
花括号内不是 key="value" 形式属性解析失败(须为 key="value" 形式)
图标集未注册 / 集合内无此图标图标集未注册:carbon / @iconify-json/lucide 中不存在图标:xxx

高级与边界

  • SSR / 水合:card / card-group / callout / steps 是构建期纯 HTML,无水合环节;CodeCollapse 初始恒为收起态,SSR 与客户端一致,零水合差异;
  • 无障碍:整块可点由覆盖层 <a> 实现并带 aria-label;图标 SVG 带 aria-hidden="true";折叠按钮带 aria-expanded;外链 _blank 自动补 rel="noreferrer"
  • 整块可点与卡内链接不冲突:覆盖层 z-index: 1,正文行内链接 z-index: 2 浮在其上,两者都可独立点击;
  • 属性解析的宿主兼容:VitePress 内置 markdown-it-attrs 原生解析花括号;无 attrs 插件的宿主(裸 markdown-it)由本插件回退自解析 info 行尾花括号,两种宿主行为一致;
  • 样式依赖 .vp-doc 作用域--vp-c-* 变量,非 VitePress 宿主需自备等价变量或自写样式(markdown-it 侧 DOM 契约稳定)。

常见问题

::: code-collapse 渲染出了原始的 <CodeCollapse> 标签?

客户端三件套缺一:vite.ssr.noExternal 包含本包、主题入口引入 styles/index.cssenhanceAppapp.use(NuxtTypographyClient)。用 vitepress-theme-vault 主题则三件套已内置。

想要 Obsidian 那种带标题的提示块,该用哪个 callout?

> [!tip] 引述语法(markdown-it-obsidian-callouts 渲染)。本插件的 ::: callout 是无标题的 Nuxt 风格单块提示,两者语义互补、可同站共存。

卡片组能放三列吗?

card-group 固定两列网格(移动端单列),与 ProseCardGroup 一致。需要更多列可用 class 直通属性挂自定义样式。

相关链接