dmx
dmx

Reputation: 83

Express/Puppeteer: generate PDF from EJS template and send as response

This User's route with Puppeteer code:

Router.get('/generate_invoice', (req, res) => {


    const userData = req.session.user;
    res.render("./patientpanel/invoice", { user: userData });

    (async () => {
        // launch a new chrome instance
        const browser = await puppeteer.launch({
            headless: true
        });

        const page = await browser.newPage();
        const filePathName = path.resolve(__dirname, '../views/patientpanel/invoice.ejs');

        const html = fs.readFileSync(filePathName, 'utf8')
        await page.goto("http://localhost:5000/generate_invoice" + html);
        await page.setContent(html, {
            waitUntil: 'domcontentloaded'
        });
        const pdfBuffer = await page.pdf({
            format: 'A4'
        });

        // or a .pdf file
        await page.pdf({ path: "./user.pdf", format: pdfBuffer });
        await browser.close()
    })();
});

The PDF file generated successfully but it shows the EJS template as it is without any proper format and data which I rendered through the above router.

The EJS template code:

<tr class="information">
    <td colspan="2">
        <table>
            <tr>
                <td>
                    Name: <%- user.firstname %>
                    <%- user.lastname %><br />
                    Email: <%- user.email %><br />
                    Mobile No. : <%- user.mob %>
                </td>
            </tr>
        </table>
    </td>
</tr>

Getting Output Like This

Upvotes: 1

Views: 4337

Answers (1)

ggorlen
ggorlen

Reputation: 56855

The basic misunderstanding seems to be familiarity with EJS in conjunction with Express (e.g. res.render()), but not standalone. EJS offers renderFile and render methods that work with files and plain strings respectively. You want to use these to prepare the HTML, which is then put into Puppeteer, PDF'd and then sent as the response.

Here's the basic workflow to handle a request:

.-------------------.
| EJS template file |
`-------------------`
       |
[ejs.renderFile]
       |
       v
.-------------.
| HTML string |
`-------------`
       |
[page.setContent]
       |
       v
.-------------.
| browser tab |
`-------------`
       |
   [page.pdf]
       |
       v
.------------.
| PDF buffer |
`------------`
       |
   [res.send]
       |
       v
.----------.
| response |
`----------`

Here's a complete example you can work from (I simplified the path to make it easier to reproduce without your folder structure; template is in the same directory as the server code):

const ejs = require("ejs"); // 3.1.8
const express = require("express"); // ^4.18.1
const puppeteer = require("puppeteer"); // ^19.1.0

express()
.get("/generate-invoice", (req, res) => {
  const userData = { // for example
    firstname: "linus",
    lastname: "torvalds",
    email: "[email protected]",
    mob: "900-900-9000"
  };

  let browser;
  (async () => {
    browser = await puppeteer.launch();
    const [page] = await browser.pages();
    const html = await ejs.renderFile("invoice.ejs", {user: userData});
    await page.setContent(html);
    const pdf = await page.pdf({format: "A4"});
    res.contentType("application/pdf");

    // optionally:
    res.setHeader(
      "Content-Disposition",
      "attachment; filename=invoice.pdf"
    );

    res.send(pdf);
  })()
    .catch(err => {
      console.error(err);
      res.sendStatus(500);
    }) 
    .finally(() => browser?.close());
})
.listen(3000);

Navigate to http://localhost:3000/generate-invoice to test.

You can use ejs.render() along with fs.readFile to pull the file in once at the start of the app, saving an extra file read on each request with ejs.renderFile.

As an aside, it's a bit heavy to launch a browser on every request, so you might consider working with a single browser and multiple pages. See this answer for a potential approach.

Upvotes: 5

Related Questions