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

1

1

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

Angelitaangell answered 30/10, 2022 at 17:43 Comment(5)
Why did you expect anything different? You read the file contents and plopped them in with setContent, literally. If you want to process that text as EJS, run EJS on it and pass in the user data. Hint: const rendered = require("ejs").render(html), then setContent(rendered). BTW, readFileSync is not a good thing to put into a request handler--it'll slow your app down, preventing the main thread from handling other requests as it waits for the kernel. It's already an async func so use await fs.promises.readFile. – Sucy
Thank you. Actually I have no Idea about it. Cause I am using for the first timeπŸ˜….Thanks for the hint.. – Angelitaangell
But how to pass that ({user : user data}) while rendering. I tried but I'm confused about it. If you have any source please let me know – Angelitaangell
Ah, I missed that you already have a render call in your code. const rendered = require("ejs").render(html, {user: userData}) should work. You're confusing res.render which renders EJS as the body of a HTTP response with running EJS to process HTML for PDF purposes without sending a response. Probably remove your res.render("./patientpanel/invoice", { user: userData }); line, because that sends a response right away rather than waiting for the PDF to render. You probably want to run Puppeteer to make the PDF, screenshot it, then send that as the response with res.sendFile(). – Sucy
Thank you πŸ™Œ. Finally I got the data 😌 – Angelitaangell
S
4

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.

Sucy answered 4/11, 2022 at 20:33 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.