在Next.js + Sanity应用中实现PDF缩略图生成及前端预览下载功能的技术方案咨询
在Next.js + Sanity应用中实现PDF缩略图生成及前端预览下载功能的技术方案咨询
嘿,我刚好做过类似的Next.js + Sanity项目,给你几个实用的解决方案,帮你搞定PDF缩略图和预览下载的需求:
方案一:后端(Sanity侧)生成缩略图(推荐生产环境使用)
这种方式是在PDF上传到Sanity后,自动在后端生成第一页的缩略图并存储到Sanity中,前端直接读取缩略图URL即可,性能和体验都更优,适合生产环境。
具体实现步骤
- 安装依赖
在你的Sanity项目根目录下安装PDF处理和Sanity客户端依赖:
npm install pdf2pic @sanity/client fs-extra
(用fs-extra是为了更安全地处理文件操作)
- 给PDF文档类型添加自动处理钩子
在Sanity的schema文件(比如schemas/pdfDocument.js)中,添加beforePublish钩子,实现PDF下载、缩略图生成、上传回Sanity的流程:
// schemas/pdfDocument.js import { createClient } from '@sanity/client'; import { fromPath } from 'pdf2pic'; import fs from 'fs-extra'; import path from 'path'; import os from 'os'; // 初始化Sanity客户端(需要写入权限的token) const client = createClient({ projectId: process.env.SANITY_PROJECT_ID, dataset: process.env.SANITY_DATASET, token: process.env.SANITY_WRITE_TOKEN, useCdn: false, }); export default { name: 'pdfDocument', type: 'document', title: 'PDF文档', fields: [ { name: 'pdfTitle', type: 'string', title: 'PDF标题' }, { name: 'pdfFile', type: 'file', title: 'PDF文件' }, { name: 'pdfThumbnail', type: 'image', title: 'PDF缩略图', description: '自动生成的PDF第一页缩略图' }, ], hooks: { beforePublish: async (document) => { // 如果没有上传PDF,直接返回原文档 if (!document.pdfFile?.asset?.url) return document; try { // 创建临时目录存储PDF和生成的缩略图 const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pdf-thumb-')); const pdfTempPath = path.join(tempDir, 'temp.pdf'); // 下载PDF到临时文件 const pdfResponse = await fetch(document.pdfFile.asset.url); const pdfBuffer = await pdfResponse.arrayBuffer(); await fs.writeFile(pdfTempPath, Buffer.from(pdfBuffer)); // 配置缩略图生成参数 const convertOptions = { density: 120, // 控制分辨率,越高越清晰 saveFilename: 'thumbnail', savePath: tempDir, format: 'png', width: 220, // 缩略图宽度 height: 310, // 缩略图高度(对应A4比例) }; // 生成第一页的缩略图 const convert = fromPath(pdfTempPath, convertOptions); const thumbnailResult = await convert(1, { responseType: 'base64' }); // 把缩略图上传到Sanity资产库 const thumbnailAsset = await client.assets.upload('image', Buffer.from(thumbnailResult.base64, 'base64'), { filename: `${document.pdfTitle || 'pdf'}_thumbnail.png`, }); // 关联缩略图到当前文档 document.pdfThumbnail = { _type: 'image', asset: { _ref: thumbnailAsset._id }, }; // 清理临时文件 await fs.remove(tempDir); } catch (error) { console.error('生成PDF缩略图失败:', error); // 即使生成失败,也不阻止文档发布 } return document; }, }, };
注意:需要在Sanity项目的.env文件中配置SANITY_PROJECT_ID、SANITY_DATASET和SANITY_WRITE_TOKEN(这个token需要有文档编辑和资产上传权限)。
方案二:前端(Next.js侧)生成缩略图(适合快速原型或小PDF场景)
如果不想改动Sanity后端,也可以在浏览器端直接解析PDF并生成缩略图,适合小体积PDF或者快速验证需求的场景。
具体实现代码
首先安装PDF.js依赖:
npm install pdfjs-dist
然后创建一个可复用的PDF缩略图组件:
// components/PDFThumbnail.jsx import { useEffect, useRef, useState } from 'react'; import * as pdfjsLib from 'pdfjs-dist'; import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'; // 配置PDF.js的worker路径 pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; const PDFThumbnail = ({ pdfUrl, title }) => { const canvasRef = useRef(null); const [thumbnailSrc, setThumbnailSrc] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const generateThumbnail = async () => { setIsLoading(true); try { // 加载PDF文档 const pdf = await pdfjsLib.getDocument(pdfUrl).promise; // 获取第一页 const page = await pdf.getPage(1); // 设置缩放比例(控制缩略图大小) const viewport = page.getViewport({ scale: 0.5 }); const canvas = canvasRef.current; canvas.width = viewport.width; canvas.height = viewport.height; // 渲染PDF页面到canvas await page.render({ canvasContext: canvas.getContext('2d'), viewport, }).promise; // 把canvas转成图片URL setThumbnailSrc(canvas.toDataURL('image/png')); } catch (err) { console.error('生成缩略图出错:', err); // 加载失败时显示默认占位图 setThumbnailSrc('https://via.placeholder.com/220x310?text=PDF+加载失败'); } finally { setIsLoading(false); } }; if (pdfUrl) generateThumbnail(); }, [pdfUrl]); // 处理预览和下载逻辑 const handlePreview = () => { window.open(pdfUrl, '_blank', 'noopener,noreferrer'); }; const handleDownload = () => { const link = document.createElement('a'); link.href = pdfUrl; link.download = `${title || 'document'}.pdf`; link.click(); link.remove(); }; return ( <div className="pdf-card" style={{ maxWidth: '220px', margin: '0 auto' }}> {isLoading ? ( <div style={{ width: '220px', height: '310px', backgroundColor: '#f5f5f5', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <span>加载缩略图中...</span> </div> ) : ( <img src={thumbnailSrc} alt={`${title} 缩略图`} onClick={handlePreview} style={{ width: '100%', height: 'auto', cursor: 'pointer', border: '1px solid #eee', borderRadius: '4px' }} /> )} <div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}> <button onClick={handlePreview} style={{ flex: 1, padding: '6px', border: 'none', borderRadius: '4px', backgroundColor: '#e0e7ff', cursor: 'pointer' }}> 预览 </button> <button onClick={handleDownload} style={{ flex: 1, padding: '6px', border: 'none', borderRadius: '4px', backgroundColor: '#fef2f2', cursor: 'pointer' }}> 下载 </button> </div> </div> ); }; export default PDFThumbnail;
然后在页面中使用这个组件:
// pages/pdfList.jsx import { useEffect, useState } from 'react'; import { createClient } from '@sanity/client'; import PDFThumbnail from '../components/PDFThumbnail'; const sanityClient = createClient({ projectId: '你的项目ID', dataset: '你的数据集', useCdn: true, }); const PDFListPage = () => { const [pdfs, setPdfs] = useState([]); useEffect(() => { const fetchPDFs = async () => { const query = '*[_type == "pdfDocument"]{_id, pdfTitle, pdfFile{asset{url}}}'; const result = await sanityClient.fetch(query); setPdfs(result); }; fetchPDFs(); }, []); return ( <div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}> <h1>PDF文档列表</h1> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '16px', marginTop: '24px' }}> {pdfs.map((pdf) => ( <PDFThumbnail key={pdf._id} pdfUrl={pdf.pdfFile.asset.url} title={pdf.pdfTitle} /> ))} </div> </div> ); }; export default PDFListPage;
前端方案注意事项
- 跨域问题:如果Sanity的PDF URL存在跨域限制,需要在Next.js的
next.config.js中配置反向代理:
之后组件中的// next.config.js module.exports = { async rewrites() { return [ { source: '/api/pdf/:path*', destination: 'https://cdn.sanity.io/files/你的项目ID/你的数据集/:path*', }, ]; }, };pdfUrl就替换为/api/pdf/${pdf.pdfFile.asset.url.split('/').slice(-2).join('/')}(提取Sanity URL中的文件路径部分)。 - 性能限制:大体积PDF(比如超过10MB)在前端渲染会卡顿,甚至导致浏览器崩溃,所以这种情况优先选择后端生成方案。
方案三:使用第三方服务生成缩略图(可选)
如果你不想自己处理PDF解析,也可以用Cloudinary这类媒体处理服务:
- 把PDF上传到Cloudinary(可以在Sanity上传时通过webhook转发)
- 直接使用Cloudinary的URL参数生成第一页缩略图,比如
https://res.cloudinary.com/你的账号/image/upload/w_220,h_310,c_fill,f_png,pg_1/你的PDF路径
这种方式无需自己写解析代码,但需要依赖第三方服务。
根据你的项目需求选择合适的方案就行:生产环境优先后端生成,小项目快速验证可以用前端方案。
备注:内容来源于stack exchange,提问作者S. N
相关产品推荐
相关产品推荐

