本文介绍如何在全新的 Astro 项目中使用两个自定义 Remark 插件——remark-admonitions(提示框)和 remark-button(按钮),让 Markdown 内容支持丰富的自定义组件。
当然我也知道网上应该有不少开箱即用的插件,但我还是选择了这个方式。
效果预览
在正式安装之前,先来看看这两个插件能实现什么效果。
提示框(Admonitions)
五种提示框分别适用于不同场景:
这是一个普通的注意提示框,用于补充说明。
这是一个友好的提示信息,可以分享小技巧。
这是一个信息提示框,用于提供额外信息。
这是一个警告提示框,用于提醒用户注意。
这是一个危险警告,用于提醒用户谨慎操作。
按钮(Button)
以下是各种按钮变体:
小号主要按钮
中号成功按钮
大号危险按钮
警告按钮
自定义紫色圆角按钮
插件简介
这两个插件基于 remark-directive 构建,利用 Markdown 的 ::: 指令语法在文章中插入自定义 UI 组件。
- remark-admonitions:在 Markdown 中渲染提示框,支持 Note、Tip、Info、Warning、Danger 五种类型
- remark-button:在 Markdown 中渲染样式按钮,支持预设尺寸、颜色与完全自定义样式
前置依赖
在开始之前,确保你的项目满足以下条件:
- Node.js >= 22.12.0
- Astro >= 6.x
- pnpm(推荐)或 npm
需要安装的 npm 包:
| 包名 | 用途 |
|---|---|
remark-directive | 解析 Markdown 中的 ::: 指令语法 |
unist-util-visit | 遍历和操作 AST 节点 |
hastscript | 以声明式方式创建 hast(HTML AST)节点 |
pnpm add remark-directive unist-util-visit hastscript
第一步:创建插件文件
在项目根目录下创建 plugins/ 目录,并将以下两个文件放入其中。
remark-admonitions.ts
import { visit } from "unist-util-visit";
import { h } from "hastscript";
const admonitions = ["note", "tip", "info", "warning", "danger"];
export default function remarkAdmonitions() {
return tree => {
visit(tree, node => {
if (
node.type === "containerDirective" ||
node.type === "leafDirective" ||
node.type === "textDirective"
) {
if (admonitions.includes(node.name)) {
const data = node.data || (node.data = {});
const hast = h("div", node.attributes || {});
if (node.children && node.children.length > 0) {
const firstChild = node.children[0];
if (firstChild.type === "paragraph") {
const firstChildData = firstChild.data || (firstChild.data = {});
const existingClass =
(firstChildData.hProperties?.className as string[]) || [];
firstChildData.hProperties = {
...firstChildData.hProperties,
className: ["admonition-title", ...existingClass],
};
}
}
data.hName = hast.tagName;
data.hProperties = {
...hast.properties,
className: [
node.name,
...((hast.properties?.className as string[]) || []),
],
};
}
}
});
};
}
remark-button.ts
import { visit } from "unist-util-visit";
import { h } from "hastscript";
const PRESET_SIZES = ["xs", "sm", "md", "lg", "xl"];
const PRESET_COLORS = [
"primary",
"secondary",
"success",
"danger",
"warning",
"info",
"light",
"dark",
"pink",
"purple",
"indigo",
"teal",
"cyan",
"orange",
];
const PRESET_TEXT_COLORS = ["white", "black", "gray", "light", "dark"];
export default function remarkButton() {
return tree => {
visit(tree, node => {
if (
node.type === "containerDirective" ||
node.type === "leafDirective" ||
node.type === "textDirective"
) {
if (node.name === "button") {
const data = node.data || (node.data = {});
const hast = h("a", node.attributes || {});
const classNames = ["custom-button"];
const inlineStyles: string[] = [];
if (node.attributes?.size) {
if (PRESET_SIZES.includes(node.attributes.size)) {
classNames.push(`button-${node.attributes.size}`);
} else {
inlineStyles.push(`font-size: ${node.attributes.size}`);
}
}
if (node.attributes?.color) {
if (PRESET_COLORS.includes(node.attributes.color)) {
classNames.push(`button-${node.attributes.color}`);
} else {
inlineStyles.push(`background-color: ${node.attributes.color}`);
}
}
if (node.attributes?.textColor) {
if (PRESET_TEXT_COLORS.includes(node.attributes.textColor)) {
classNames.push(`button-text-${node.attributes.textColor}`);
} else {
inlineStyles.push(`color: ${node.attributes.textColor}`);
}
}
if (node.attributes?.padding) {
inlineStyles.push(`padding: ${node.attributes.padding}`);
}
if (node.attributes?.borderRadius) {
inlineStyles.push(`border-radius: ${node.attributes.borderRadius}`);
}
classNames.push(...((hast.properties?.className as string[]) || []));
const properties: Record<string, string | string[]> = {
...hast.properties,
className: classNames,
target: node.attributes?.target || "_blank",
rel: "noopener noreferrer",
};
if (inlineStyles.length > 0) {
properties.style = inlineStyles.join("; ");
}
data.hName = "a";
data.hProperties = properties;
}
}
});
};
}
第二步:配置 Astro
编辑 astro.config.ts,在 markdown.remarkPlugins 中注册这两个插件:
import { defineConfig } from "astro/config";
import remarkDirective from "remark-directive";
import remarkAdmonitions from "./plugins/remark-admonitions";
import remarkButton from "./plugins/remark-button";
export default defineConfig({
markdown: {
remarkPlugins: [
remarkDirective, // 必须在自定义插件之前注册
remarkAdmonitions,
remarkButton,
],
},
});
remarkDirective 必须排在 remarkAdmonitions 和 remarkButton 之前。因为 remark-directive 负责解析 ::: 语法生成 directive 节点,自定义插件再将这些节点转换为 HTML。如果顺序错误,自定义插件收不到 directive 节点,所有 ::: 区块都不会被渲染。
第三步:添加 CSS 样式
提示框样式
在你的全局 CSS 文件(如 src/styles/global.css)中添加:
/* 提示框基础样式 */
.prose .note,
.prose .tip,
.prose .info,
.prose .warning,
.prose .danger {
@apply my-4 rounded-lg border-l-4 p-4;
}
/* 提示框标题 */
.prose .note .admonition-title,
.prose .tip .admonition-title,
.prose .info .admonition-title,
.prose .warning .admonition-title,
.prose .danger .admonition-title {
@apply mb-2 text-lg font-bold;
}
/* 各类型配色 */
.prose .note {
@apply border-blue-500 bg-blue-50 text-blue-900;
}
.prose .tip {
@apply border-green-500 bg-green-50 text-green-900;
}
.prose .info {
@apply border-cyan-500 bg-cyan-50 text-cyan-900;
}
.prose .warning {
@apply border-yellow-500 bg-yellow-50 text-yellow-900;
}
.prose .danger {
@apply border-red-500 bg-red-50 text-red-900;
}
/* 暗色模式适配 */
html[data-theme="dark"] .prose .note {
@apply border-blue-400 bg-blue-950/50 text-blue-100;
}
html[data-theme="dark"] .prose .tip {
@apply border-green-400 bg-green-950/50 text-green-100;
}
html[data-theme="dark"] .prose .info {
@apply border-cyan-400 bg-cyan-950/50 text-cyan-100;
}
html[data-theme="dark"] .prose .warning {
@apply border-yellow-400 bg-yellow-950/50 text-yellow-100;
}
html[data-theme="dark"] .prose .danger {
@apply border-red-400 bg-red-950/50 text-red-100;
}
按钮样式
/* 按钮基础样式 */
.custom-button {
@apply inline-flex items-center justify-center rounded-lg bg-accent px-6 py-1 text-base font-medium text-white transition-all duration-200 hover:shadow-lg;
text-decoration: none;
}
.custom-button:hover {
transform: translateY(-2px);
}
.custom-button:active {
transform: translateY(0);
}
/* 尺寸变体 */
.button-xs {
@apply px-3 py-1 text-xs;
}
.button-sm {
@apply px-4 py-1.5 text-sm;
}
.button-md {
@apply px-6 py-2.5 text-base;
}
.button-lg {
@apply px-8 py-3 text-lg;
}
.button-xl {
@apply px-10 py-4 text-xl;
}
/* 颜色变体 */
.button-primary {
@apply bg-blue-600 hover:bg-blue-700;
}
.button-secondary {
@apply bg-gray-600 hover:bg-gray-700;
}
.button-success {
@apply bg-green-600 hover:bg-green-700;
}
.button-danger {
@apply bg-red-600 hover:bg-red-700;
}
.button-warning {
@apply bg-yellow-500 hover:bg-yellow-600;
}
.button-info {
@apply bg-blue-500 hover:bg-blue-600;
}
.button-light {
@apply bg-gray-200 hover:bg-gray-300;
}
.button-dark {
@apply bg-gray-800 hover:bg-gray-900;
}
.button-pink {
@apply bg-pink-500 hover:bg-pink-600;
}
.button-purple {
@apply bg-purple-600 hover:bg-purple-700;
}
.button-indigo {
@apply bg-indigo-600 hover:bg-indigo-700;
}
.button-teal {
@apply bg-teal-500 hover:bg-teal-600;
}
.button-cyan {
@apply bg-cyan-500 hover:bg-cyan-600;
}
.button-orange {
@apply bg-orange-500 hover:bg-orange-600;
}
/* 文字颜色变体 */
.button-text-white {
@apply text-white;
}
.button-text-black {
@apply text-black;
}
.button-text-gray {
@apply text-gray-800;
}
.button-text-light {
@apply text-gray-100;
}
.button-text-dark {
@apply text-gray-900;
}
以上样式使用 Tailwind CSS 的 @apply 指令。如果你不使用 Tailwind,将 @apply 替换为等价的 CSS 属性即可。
第四步:在 Markdown 中使用
提示框语法
支持五种类型:note、tip、info、warning、danger。提示框内的第一个段落会自动渲染为加粗标题。
:::note
这是一个普通的注意提示框。
:::
:::tip
这是一个友好的提示信息。
:::
:::info
这是一个信息提示框,用于提供额外信息。
:::
:::warning
这是一个警告提示框,用于提醒用户注意。
:::
:::danger
这是一个危险警告,用于提醒用户谨慎操作。
:::
按钮语法
按钮使用 :::button{属性...} 语法,支持以下属性:
| 属性 | 说明 | 预设值 | 自定义示例 |
|---|---|---|---|
href | 按钮链接地址 | 任意 URL | href="https://example.com" |
size | 按钮尺寸 | xs, sm, md, lg, xl | size="1.5rem" |
color | 背景颜色 | primary, secondary, success, danger, warning, info, light, dark, pink, purple, indigo, teal, cyan, orange | color="#ff6b6b" |
textColor | 文字颜色 | white, black, gray, light, dark | textColor="#2d3436" |
padding | 内边距 | 任意 CSS 值 | padding="1rem 2rem" |
borderRadius | 圆角 | 任意 CSS 值 | borderRadius="50px" |
target | 链接打开方式 | 默认 _blank | target="_self" |
size、color、textColor 使用预设值时生成对应的 CSS 类名;使用非预设值时,插件会将值直接作为内联 CSS 属性写入元素的 style 属性中。
预设尺寸
:::button{href="https://example.com" size="xs"}
超小号按钮
:::
:::button{href="https://example.com" size="sm"}
小号按钮
:::
:::button{href="https://example.com" size="md"}
中号按钮
:::
:::button{href="https://example.com" size="lg"}
大号按钮
:::
:::button{href="https://example.com" size="xl"}
超大号按钮
:::
预设颜色
:::button{href="https://example.com" color="primary"}
主要按钮
:::
:::button{href="https://example.com" color="danger"}
危险按钮
:::
:::button{href="https://example.com" color="warning" textColor="black"}
警告按钮(深色文字)
:::
完全自定义
:::button{href="https://example.com" size="0.75rem"}
超小字体按钮
:::
:::button{href="https://example.com" color="#ff6b6b"}
自定义红色按钮
:::
:::button{href="https://example.com" color="#ffeaa7" textColor="#2d3436"}
自定义黄色按钮深色文字
:::
:::button{href="https://example.com" padding="1rem 2rem"}
自定义内边距按钮
:::
:::button{href="https://example.com" borderRadius="2rem"}
圆角按钮
:::
组合使用
:::button{href="https://example.com" size="lg" color="success"}
大号成功按钮
:::
:::button{href="https://example.com" size="sm" color="danger"}
小号危险按钮
:::
:::button{href="https://example.com" color="#fd79a8" textColor="white" padding="1.5rem 3rem" borderRadius="50px"}
完全自定义按钮
:::
更多注意事项
插件注册顺序
处理流程如下:
remark-directive将:::name{...}语法解析为 directive AST 节点remark-admonitions/remark-button将匹配到的 directive 节点转换为 HTML
关于 TypeScript
如果你使用 astro check 做类型检查,可能需要为插件文件添加类型声明。推荐在 plugins/ 目录下创建对应的声明文件:
// plugins/remark-admonitions.d.ts
declare module "./plugins/remark-admonitions" {
const value: () => (tree: any) => void;
export default value;
}
// plugins/remark-button.d.ts
declare module "./plugins/remark-button" {
const value: () => (tree: any) => void;
export default value;
}
CSS 框架兼容性
- Tailwind CSS v4:示例样式使用
@apply,开箱即用 - Tailwind CSS v3:需要
@tailwindcss/typography插件 - 原生 CSS:将
@apply指令替换为标准 CSS 属性即可 - 其他 CSS 方案(UnoCSS、styled-components 等):类名规则与样式完全独立,适配任意方案
已知限制
- 按钮默认在新标签页打开(
target="_blank"),如需当前页跳转请指定target="_self" - 按钮的
rel属性固定为"noopener noreferrer",无法自定义 - 提示框的第一个段落始终被渲染为标题,无法禁用
- 提示框目前不支持自定义属性传入,仅支持五种预设类型