最近看到一个 AIQA 能力总览的 landing 页:顶部卡片阵列、中间带坐标的 SVG 流程图、配色和阴影都很克制。我想把这种表达力带进博客,但不想每次写文章都重画一遍 SVG。于是重新梳理了一下 Hugo 这边的做法,落地成两个 shortcode 和一段作用域化的 CSS。

一、问题是什么

照搬示例页面有几个硬伤:

  1. 硬坐标 SVGx="340" y="230" 那种)一改文案就要重算坐标,和 markdown 的轻量写作完全反着来。
  2. 样式耦合:示例把所有 CSS 变量都挂在 :root 上,塞进 post 会污染全站命名空间。
  3. Goldmark unsafe=true 虽然已经开了,但直接在 md 里糊 200 行 HTML,语义层级乱掉。

所以目标是:流程图文本化、卡片样式作用域化、写作体验仍然是 markdown

二、技术栈取舍

📊

Mermaid 代替 SVG

流程图用文本语法描述,diff 友好,无需关心坐标。按需加载,不用 Mermaid 的页面零开销。
🎴

Shortcode 组合卡片

capability-grid + capability-card 两级组合,cols 和 color 用参数控制,写法贴近 markdown。
🎯

CSS 作用域隔离

所有变量和类名挂在 .cap-overview 下,不会污染主题的 :root,未来换主题也不冲突。

三、流程图:Mermaid 代替手画 SVG

写作时只要这样:

{{< mermaid >}}
graph LR
    A[Markdown 源文件] --> B[Hugo 构建]
    B --> C[Shortcode 展开]
    C --> D[静态 HTML]
    D --> E[浏览器渲染]
    E --> F{含 Mermaid?}
    F -- 是 --> G[CDN 加载 mermaid.esm.min.mjs]
    F -- 否 --> H[零额外开销]
    G --> I[文本语法转 SVG]
{{< /mermaid >}}

渲染结果:

graph LR A[Markdown 源文件] --> B[Hugo 构建] B --> C[Shortcode 展开] C --> D[静态 HTML] D --> E[浏览器渲染] E --> F{含 Mermaid?} F -- 是 --> G[CDN 加载 mermaid.esm.min.mjs] F -- 否 --> H[零额外开销] G --> I[文本语法转 SVG]

关键实现细节是用 .Page.Scratch 保证同一页面里只引一次 mermaid 的 JS,不会每个图都重复加载:

{{- if not (.Page.Scratch.Get "mermaid_loaded") -}}
{{- .Page.Scratch.Set "mermaid_loaded" true -}}
<script type="module">
  import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/...";
  mermaid.initialize({ startOnLoad: true });
</script>
{{- end -}}

四、卡片:两级 shortcode

capability-grid 负责栅格和作用域容器,capability-card 负责单张卡片内容,.Innermarkdownify 再渲染,所以卡片正文可以继续写 markdown。

{{< capability-grid cols="2" >}}
{{< capability-card icon="⚡" title="标题" color="blue" >}}
正文支持 **markdown** 和 `代码`。
{{< /capability-card >}}
...
{{< /capability-grid >}}

四种配色(blue / green / orange / violet)只改顶部 3px 边线和 icon 背景,视觉统一:

Blue

默认色,适合通用信息。

Green

成功、成果类。

Orange

警示、权衡类。

Violet

亮点、强调项。

五、CSS 作用域这件事

示例里配色变量直接挂 :root,搬过来会和主题的变量重名。我的做法是把它们挪到 .cap-overview 下:

.cap-overview {
  --cap-blue: #4c8bf5;
  --cap-blue-soft: #eaf2ff;
  /* ... */
}
.cap-card--blue { border-top: 3px solid var(--cap-blue); }

这样整个主题的变量空间没被污染,未来想换一套色板也只需改这个作用域。

顺带踩过的坑:custom.css 必须放在 assets/ 而不是 static/,否则 Hugo 的 resources.Get 会静默跳过,HTML 根本不会 <link> 它。这条之前就吃过亏,项目 CLAUDE.md 里也记了。

六、要不要整个做成 layout?

早期版本我考虑过给"能力总览"做一个独立的 layouts/capability-overview/single.html,front matter 里填数据、模板里渲染。最终放弃的理由:

方案适用场景代价
专用 layout强结构化数据,多处复用,需要 JSON/YAML 数据源写一次模板,但和文章正文割裂
Shortcode(当前)一篇文章里插一段卡片总览模板轻量,但需要手工组合
独立 HTML 丢 static/真的只做一次不走 markdown 管线,搜索/RSS 都没了

对我这种"偶尔在博客里插一张能力图"的需求,shortcode 是最合适的颗粒度。真要做一个对外展示的 landing,再升级成 layout 不迟。

七、成果

  • layouts/shortcodes/mermaid.html —— 文本化流程图
  • layouts/shortcodes/capability-grid.html + capability-card.html —— 卡片阵列
  • assets/css/custom.css 新增 .cap-overview 作用域 + .mermaid 容器样式
  • 本文就是第一个使用者,上面所有的卡片和流程图都是 shortcode 渲染的

下次写架构、能力盘点类文章时,就不用再纠结"这一段要不要画图"——直接写 Mermaid;要做对比矩阵,就用 capability-grid。写作体验仍然是 markdown,表达力上了一个台阶。