So I recently tried to find a package to download a pdf document in Next JS without having the content rendered on the page first and it proved a little difficult so I had to figure out a different solution which I would be sharing in this short blog post.
Before we continue, I would like to outline the technologies we would be using:
html-node-pdf
package. You can install the package by running yarn add html-node-pdf
.The approach we would be using is pretty straightforward. We would outline the steps below and go into each in more details. The steps include:
Let's use a simple html template that would serve as the content of the pdf.
const Template = () => {return `<!DOCTYPE html><html><body><h1>PDF Content</h1></body></html>`}
We would typically have a function that makes a call to the backend and send the html string
const handleDownload = async () => {const response = await generatePDFAPICall({htmlString: Template(),title: "Test.pdf" // Name of the pdf})console.log(response)}
You should have a route that the client can send a request to with the pdf content and the controller configured
// mediaRoutes.ts// You would typically have your auth middleware and the middleware for the routeimport express from 'express';import { generatePDFController } from 'path-to-controller';const router = express.Router();router.post('/pdf', generatePDFController);export default router;
// generatePDFController.tsimport { RequestHandler } from 'express';const uploadPDFController: RequestHandler = async (req, res) => {};export { uploadPDFController };
// s3Service.tsimport * as AWS from '@aws-sdk/client-s3';import { Upload } from '@aws-sdk/lib-storage';interface IUpload<B> {Body: BKey: stringBucket: stringFileName: stringContentType: string}interface S3Service {upload: (params: IUpload) => void}const client = new AWS.S3Client({region: process.env.REGION,credentials: {accessKeyId: process.env.AWS_ACCESS_KEY_ID_ENV!,secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY_ENV!}});class S3Service implements S3Service {static upload = async <B>({ Key, Body, Bucket, ContentType, FileName }: IUpload<B>) => {const params: AWS.PutObjectCommandInput = {Key,Body,Bucket,ContentType};const initiateUpload = new Upload({client,params});const result = await initiateUpload.done();const response = {url: result.Location,fileName: result.Key}const newContentDisposition = `attachment; filename="${FileName}"`;const copyObjectParams = {Key,Bucket,CopySource: `/${Bucket}/${Key}`,MetadataDirective: AWS.MetadataDirective.REPLACE,Metadata: {'Content-Disposition': newContentDisposition}};const copyObjectCommand = new AWS.CopyObjectCommand(copyObjectParams);await client.send(copyObjectCommand);return response}}export default S3Service
Let's break down what we've just come up with
upload
to handle media uploadsUpload
methodContent-Disposition
header provides information on how the content should be handled by the browser or client. It is often used to specify whether the content should be displayed inline in the browser or treated as an attachment and downloaded. In our case, we want it to be downloaded as an attachment.fileName
and url
Note: You need to make the S3 Bucket public so it's accessible. You can do this by adding the following policy to your bucket.
{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Principal": "*","Action": "s3:GetObject","Resource": "arn:aws:s3:::your-bucket-name/*"}]}
// generatePDFController.tsimport pdf from 'html-pdf-node';import { promisify } from 'util';import { v4 as uuid } from 'uuid';import S3Service from 'path-to-s3-service'import { RequestHandler, Request } from 'express';interface IBody {htmlString: string;title: string;}type IReq = Request<{}, {}, IBody>const generateIdentifier = (fileName: string) => {const uniqueIdentifier = uuid();const splitString = str.split('.');const extension = splitString.pop();const newString = `${splitString.join('')}-${uniqueIdentifier}.${extension}`;return newString;}const uploadPDFController: RequestHandler = async (req: IReq, res) => {const data: IBody = req.bodyconst options = {format: 'A4'};const bufferCreator = promisify(pdf.generatePdf);const buffer = await bufferCreator({ content: data.htmlString }, options);const FileName = data.titleconst Body = bufferconst Key = generateIdentifier(FileName)const imageResponse = await S3Service.upload({Key,Body,FileName,Bucket: "your-bucket-name",ContentType: "application/pdf",});const response = {data: imageResponse,error: null,status: 200,message: 'PDF Download link generated successfully'};res.status(response.status).send(response);};export { uploadPDFController };
And that's it. We've been able to write the controller that receives the html string, converts it to a buffer, uploads the buffer to S3 and retreive the object url. Now let's set up the client side logic to download the pdf.
const handleDownload = async () => {try {const response = await generatePDFAPICall({htmlString: Template(),title: "Test.pdf" // Name of the pdf});const status = response.status;if (status === 200) {const data = response.data;const link = document.createElement("a");link.href = data.url;link.download = data.fileName;link.click();} else {// handle server side errors}} catch (err) {// handle function execution errors}}
We've setup the logic to get back the response from the backend and open the link in the browser. Because of the content disposition header we set up the browser automatically downloads the document.
You can setup a lambda function with a cron expression to create a schedule that empties the bucket after a certain period. You might not want to keep the buckets content since it's just for the user to download.
That’s it! I hope you enjoyed reading and you find this helpful! If you have any questions, feel free to ask. I’m here and also on Twitter.
Thanks for reading! 🙂