基于rehype的博客PDF嵌入组件实现
- 1导航栏 UI 改造全记录:卡片边框、悬浮胶囊、三段收拢与 PC 收拢态快捷面板(附完整复现指南)
- 2博客性能优化全记录:Lighthouse 报告拆解到帧率根因排查(A–F 六项实战)
- 3Firefly 博客实战:从零搭建任意设备可用的发布台(Vercel + GitHub API)
- 4评论区回复时表情面板被截断:一次 overflow 裁剪排查实录
- 5Firefly 博客实战:给文章页加阅读进度条与圆环百分比
- 6Firefly 博客接入友链朋友圈:从零开始的完整部署教程
- 7从零开始搭建个人图床
- 8文章中添加音乐播放器
- 9基于Firefly主题的列表封面位置改造
- 10基于rehype的博客PDF嵌入组件实现本文
- 11Twikoo评论系统部署踩坑记录:Vercel域名无法访问的解决之路
- 12这是我的第一篇BLOG

博客里已经有了 GitHub 卡片、B站卡片这类 Markdown 扩展组件,一直想着能不能把 PDF 也直接嵌进文章里,不用再丢个链接让读者跳出去看。今天折腾了一下,用 AI 辅助搞定了,由于制作B站卡片时忘记记录了,于是决定记录一下制作PDF组件全过程。
思路
项目里已经有一套基于 rehype 的自定义指令组件体系,比如 ::github{...} 和 ::bilibili{...}。我要做的就是照着这个模式,新增一个 ::pdf{...} 指令,让它在 Markdown 里渲染成一个带标题栏和下载按钮的 PDF 查看器。
整个过程分三步:写插件 → 注册插件 → 写样式。样式直接写在框架统一存放 Markdown 扩展样式的 markdown-extend.styl 里,它由 Markdown.astro 自动加载,不需要额外引入。
第一步:创建 PDF 查看器插件
在 src/plugins/ 路径下新建 rehype-component-pdf-viewer.mjs,核心逻辑是接收 url、title、height 三个参数,用 hastscript 拼出一个带 header 和 iframe 的 DOM 结构。
代码里做了两个校验:如果指令不是叶子类型(带了子内容),或者没传 url,就返回一个隐藏的错误提示 div,避免页面崩掉。
/// <reference types="mdast" />import { h } from "hastscript";
/** * Creates a PDF Viewer component. * * @param {Object} properties - The properties of the component. * @param {string} properties.url - The URL of the PDF file. * @param {string} [properties.title] - Optional title for the PDF viewer. * @param {number} [properties.height] - Optional height in pixels (default: 600). * @param {import('mdast').RootContent[]} children - The children elements. * @returns {import('mdast').Parent} The created PDF Viewer component. */export function PdfViewerComponent(properties, children) { if (Array.isArray(children) && children.length !== 0) return h("div", { class: "hidden" }, [ 'Invalid directive. ("pdf" directive must be leaf type "::pdf{url="https://example.com/file.pdf"}")', ]);
if (!properties.url) return h( "div", { class: "hidden" }, 'Invalid URL. ("url" attribute is required)', );
const url = properties.url; const title = properties.title || "PDF Viewer"; const height = properties.height || 600;
// 浏览器原生 PDF 预览(iframe 直接加载),不引入 pdf.js 重依赖 return h("div", { class: "pdf-container" }, [ h("div", { class: "pdf-header" }, [ h("div", { class: "pdf-title" }, title), h( "a", { class: "pdf-download", href: url, target: "_blank", download: "", }, "下载PDF", ), ]), h("div", { class: "pdf-viewer-wrapper" }, [ h("iframe", { class: "pdf-viewer", src: url, style: `height: ${height}px;`, title: title, }), ]), ]);}第二步:在 astro.config.mjs 中注册插件
插件写好了还得注册才能用。打开 astro.config.mjs,分两处修改。
第一处:顶部 import(和其他组件插件放一起,约 41–44 行附近)
import { BilibiliCardComponent } from "./src/plugins/rehype-component-bilibili-card.mjs";import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";import { MusicPlayerComponent } from "./src/plugins/rehype-component-music-player.mjs";import { PdfViewerComponent } from "./src/plugins/rehype-component-pdf-viewer.mjs";第二处:components 中注册(约第 329 行附近)
找到 components 配置,在里面加一行 pdf 字段:
components: { github: GithubCardComponent, bilibili: BilibiliCardComponent, pdf: PdfViewerComponent,},第三步:添加 CSS 样式
光有 DOM 还不够,得给它穿件衣服。打开框架统一的 Markdown 扩展样式文件 src/styles/markdown-extend.styl(GitHub 卡片、图表控件的样式也都在这里),按 Stylus 语法追加一段。
有两点值得注意:颜色全部用主题变量而不是写死的色值——var(--license-block-bg)、var(--line-divider) 这类变量在暗色主题下会自动切换,不用再手写一套 .dark 覆盖;另外要显式覆盖 .custom-md iframe 的全局 margin 和圆角,否则 iframe 会被文章里通用的 iframe 规则影响。
// ─── PDF 查看器(::pdf 指令,rehype-component-pdf-viewer) ──────────────────
.pdf-container margin: 1.5rem 0 border-radius: 0.75rem overflow: hidden background: var(--license-block-bg) border: 1px solid var(--line-divider)
.pdf-header display: flex align-items: center justify-content: space-between padding: 0.75rem 1rem border-bottom: 1px solid var(--line-divider)
.pdf-title font-weight: 600 font-size: 0.95rem color: var(--tw-prose-headings)
.pdf-download font-size: 0.85rem color: var(--primary) text-decoration: none padding: 0.25rem 0.75rem border-radius: 0.375rem background: var(--btn-regular-bg) transition: background-color 0.2s ease
&:hover background: var(--btn-regular-bg-hover)
.pdf-viewer-wrapper position: relative width: 100%
.pdf-viewer display: block width: 100% border: none
// 覆盖上方 .custom-md iframe 的全局 margin/border-radius,由容器自己控制留白.custom-md .pdf-viewer margin: 0 border-radius: 0由于 PDF 阅读器并未进行本地配置而是选择了 iframe 直接嵌入,因此实际阅读控件和读者的浏览器有关。
样式写完就完事了——markdown-extend.styl 已经由 Markdown.astro 引入,不需要在 Layout.astro 里再手动 import 一份。
使用方法
全部搞定之后,在任何 Markdown 文章里直接写一行指令就行:
::pdf{url="https://example.com/your-file.pdf" title="我的PDF文档" height="600"}三个参数说明:
| 参数 | 说明 | 是否必填 | 默认值 |
|---|---|---|---|
url | PDF 文件的 URL 地址 | 必填 | — |
title | 查看器顶部显示的标题 | 可选 | "PDF Viewer" |
height | 查看器高度(像素) | 可选 | 600 |
PDF控件示例:
小结
整体下来没什么坑,核心就是复用项目已有的 rehype 组件模式,新增一个指令处理器而已。iframe 直接加载 PDF URL,浏览器原生支持预览,省得引入 pdf.js 那种重依赖。header 上放个下载按钮,读者想保存也方便。
以后写技术文章放论文、说明书之类的 PDF 就舒服多了,不用再让读者跳出博客去看。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!




