You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Next.js + Sanity应用中实现PDF缩略图生成及前端预览下载功能的技术方案咨询

在Next.js + Sanity应用中实现PDF缩略图生成及前端预览下载功能的技术方案咨询

嘿,我刚好做过类似的Next.js + Sanity项目,给你几个实用的解决方案,帮你搞定PDF缩略图和预览下载的需求:

方案一:后端(Sanity侧)生成缩略图(推荐生产环境使用)

这种方式是在PDF上传到Sanity后,自动在后端生成第一页的缩略图并存储到Sanity中,前端直接读取缩略图URL即可,性能和体验都更优,适合生产环境。

具体实现步骤

  1. 安装依赖
    在你的Sanity项目根目录下安装PDF处理和Sanity客户端依赖:
npm install pdf2pic @sanity/client fs-extra

(用fs-extra是为了更安全地处理文件操作)

  1. 给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这类媒体处理服务:

  1. 把PDF上传到Cloudinary(可以在Sanity上传时通过webhook转发)
  2. 直接使用Cloudinary的URL参数生成第一页缩略图,比如https://res.cloudinary.com/你的账号/image/upload/w_220,h_310,c_fill,f_png,pg_1/你的PDF路径

这种方式无需自己写解析代码,但需要依赖第三方服务。


根据你的项目需求选择合适的方案就行:生产环境优先后端生成,小项目快速验证可以用前端方案。

备注:内容来源于stack exchange,提问作者S. N

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 10:44:30