视频加载失败

基于rehype的博客PDF嵌入组件实现

1155 字
6 分钟
基于rehype的博客PDF嵌入组件实现
基于rehype的博客PDF嵌入组件实现

博客里已经有了 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"}

三个参数说明:

参数说明是否必填默认值
urlPDF 文件的 URL 地址必填—
title查看器顶部显示的标题可选"PDF Viewer"
height查看器高度(像素)可选600

PDF控件示例:

我的PDF文档
下载PDF

小结#

整体下来没什么坑,核心就是复用项目已有的 rehype 组件模式,新增一个指令处理器而已。iframe 直接加载 PDF URL,浏览器原生支持预览,省得引入 pdf.js 那种重依赖。header 上放个下载按钮,读者想保存也方便。

以后写技术文章放论文、说明书之类的 PDF 就舒服多了,不用再让读者跳出博客去看。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
基于rehype的博客PDF嵌入组件实现
https://mstzuomu.space/posts/azuma.zuomu-blog-3/
作者
左沐
发布于
2026-08-16
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
导航栏 UI 改造全记录:卡片边框、悬浮胶囊、三段收拢与 PC 收拢态快捷面板(附完整复现指南)
技术分享一次连续迭代的完整复盘——从卡片边框体系、悬浮胶囊导航,到三段结构拆分、下滑收拢动画,再到 PC 收拢态汉堡的控件快捷面板与"行点击原位弹卡"的两处根因修复,追加 PC 搜索控件图标化与收拢动画卡顿的逐帧诊断修复。每个批次改了哪些文件、为什么这么改、踩了哪些坑(CSS 层序、正圆公式、包含块、Vite 缓存、Playwright headless 动画冻结),附 git 重放与手工重做两条复现路径及验收清单
2
博客性能优化全记录:Lighthouse 报告拆解到帧率根因排查(A–F 六项实战)
技术分享从两份 Lighthouse 对比报告出发,完整记录六项性能优化的排查方法、根因定位与修复代码:字体子集化、封面图尺寸 API、CSS 内联、强制重排归零、以及"只有 Chrome 掉帧"的浏览器设置级根因,附全部可复现命令与工具脚本
3
Firefly 博客实战:从零搭建任意设备可用的发布台(Vercel + GitHub API)
技术分享静态博客只能守在电脑前发文章?本文完整复盘如何用 Vercel 无框架函数 + GitHub API 搭一套带登录的发布后台——发文、改稿、删除、预览全部在浏览器里完成,附我们真实踩过的五个部署坑与安全边界设计,照着做可以从零复刻。
4
Win11 资源管理器大小列只显示 KB?开启 KB / MB / GB 自适应显示完整教程
技术分享Win11 详细信息视图的大小列长期以来一律用 KB 显示,4 GB 的文件写成 4,194,304 KB 根本没法看。微软其实已经原生支持自适应单位了,本文给出先更新系统、再用 ViveTool 强制开启(功能 ID 61014711)的完整教程,含回滚方法与常见问题。
5
Codex 重装后三大故障修复全记录:CC Switch 报错、走官方通道、聊天记录丢失
技术分享一次「Codex 删了重装」引发三个环环相扣的故障:CC Switch 报 requireStack、Codex 打到 api.openai.com 401、历史聊天记录不显示。完整根因分析与修复过程。
随机文章随机推荐

评论区

Profile Image of the Author
陌殊途左沐
热爱是拯救无趣人生的唯一途径
公告
欢迎来到我的博客!这里是左沐的个人空间,分享我的学习、生活和兴趣爱好。希望你能在这里找到有趣的内容,请不要对我的喜好做出评价哦!请勿使用公网访问本站。
分类
访客信息
加载中...
标签
最新动态
站点统计
文章
27
分类
2
标签
47
总字数
48,093
运行时长
0 天
最后活动
0 天前
总浏览量
-
访客数
-
文章目录