[{"data":1,"prerenderedAt":1002},["Reactive",2],{"/guides/html-to-pdf-node":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"dateModified":10,"author":11,"body":12,"_type":997,"_id":998,"_source":999,"_file":1000,"_extension":1001},"/guides/html-to-pdf-node","guides",false,"","HTML to PDF in Node.js: replace html-pdf-node","html-pdf-node is Google's top result for HTML to PDF in Node, and it failed on our invoice. Here's what it does to your HTML, and the code to use instead.","2026-11-13","Cyril Nicodème",{"type":13,"children":14,"toc":989},"root",[15,40,47,52,229,295,321,347,395,417,423,428,440,445,454,513,518,793,798,832,838,865,887,893,905,914,932,962,968],{"type":16,"tag":17,"props":18,"children":19},"element","p",{},[20,23,30,32,38],{"type":21,"value":22},"text","The ",{"type":16,"tag":24,"props":25,"children":27},"code",{"className":26},[],[28],{"type":21,"value":29},"generatePdf({ content: html }, options)",{"type":21,"value":31}," call on html-pdf-node's npm page looks like it hands your HTML straight to Chrome. It doesn't. Your HTML first goes through a CSS inliner and a strict Handlebars template, and every call starts a new browser. Our test invoice didn't survive the first step. Keep your route and your options, and swap the package: Puppeteer with one browser you keep open, or one ",{"type":16,"tag":24,"props":33,"children":35},{"className":34},[],[36],{"type":21,"value":37},"fetch",{"type":21,"value":39}," to PDFShift.",{"type":16,"tag":41,"props":42,"children":44},"h2",{"id":43},"what-does-html-pdf-node-do-to-your-html",[45],{"type":21,"value":46},"What does html-pdf-node do to your HTML?",{"type":16,"tag":17,"props":48,"children":49},{},[50],{"type":21,"value":51},"The whole package is one 94-line file, short enough to read in a few minutes.",{"type":16,"tag":53,"props":54,"children":56},"guide-figure",{"caption":55},"The steps inside html-pdf-node's generatePdf, and what each one did to our test invoice. Our test, November 13, 2026.",[57],{"type":16,"tag":58,"props":59,"children":68},"svg",{"viewBox":60,"xmlns":61,"role":62,"ariaLabelledBy":63,"style":66,"fontFamily":67},"0 0 360 436","http://www.w3.org/2000/svg","img",[64,65],"h2n-pipe-title","h2n-pipe-desc","max-width:360px;display:block;margin:0 auto","DM Sans, sans-serif",[69,75,81,100,121,147,168,190],{"type":16,"tag":70,"props":71,"children":72},"title",{"id":64},[73],{"type":21,"value":74},"What html-pdf-node does between your HTML and the PDF",{"type":16,"tag":76,"props":77,"children":78},"desc",{"id":65},[79],{"type":21,"value":80},"Your HTML goes through three steps before it becomes a PDF. Step 1, inline-css: it removed the style tag and threw an error on the page-number rule. Step 2, Handlebars in strict mode: any double curly braces in the HTML throw an error. Step 3, a new Chrome for every call: about 2.2 seconds per PDF including the launch, and the browser stays open when a step before it fails.",{"type":16,"tag":82,"props":83,"children":84},"defs",{},[85],{"type":16,"tag":86,"props":87,"children":93},"marker",{"id":88,"viewBox":89,"refX":90,"refY":90,"markerWidth":91,"markerHeight":91,"orient":92},"h2n-pipe-arrow","0 0 10 10","5","7","auto",[94],{"type":16,"tag":95,"props":96,"children":99},"path",{"d":97,"fill":98},"M0,0 L10,5 L0,10 z","#4C556D",[],{"type":16,"tag":101,"props":102,"children":106},"g",{"style":103,"fill":104,"textAnchor":105},"font-size:13px","#0A0C3D","middle",[107,115],{"type":16,"tag":21,"props":108,"children":112},{"x":109,"y":110,"style":111},"180","20","font-weight:600",[113],{"type":21,"value":114},"Your HTML",{"type":16,"tag":21,"props":116,"children":118},{"x":109,"y":117,"style":111},"428",[119],{"type":21,"value":120},"PDF",{"type":16,"tag":101,"props":122,"children":125},{"stroke":98,"strokeWidth":123,"markerEnd":124},"1.5","url(#h2n-pipe-arrow)",[126,132,137,142],{"type":16,"tag":127,"props":128,"children":131},"line",{"x1":109,"y1":129,"x2":109,"y2":130},"28","50",[],{"type":16,"tag":127,"props":133,"children":136},{"x1":109,"y1":134,"x2":109,"y2":135},"148","170",[],{"type":16,"tag":127,"props":138,"children":141},{"x1":109,"y1":139,"x2":109,"y2":140},"268","290",[],{"type":16,"tag":127,"props":143,"children":146},{"x1":109,"y1":144,"x2":109,"y2":145},"388","410",[],{"type":16,"tag":101,"props":148,"children":151},{"fill":149,"stroke":150,"strokeWidth":123},"#FFFFFF","#DBE4F2",[152,160,164],{"type":16,"tag":153,"props":154,"children":159},"rect",{"x":110,"y":155,"width":156,"height":157,"rx":158},"56","320","88","12",[],{"type":16,"tag":153,"props":161,"children":163},{"x":110,"y":162,"width":156,"height":157,"rx":158},"176",[],{"type":16,"tag":153,"props":165,"children":167},{"x":110,"y":166,"width":156,"height":157,"rx":158},"296",[],{"type":16,"tag":101,"props":169,"children":171},{"style":170,"fill":104,"textAnchor":105},"font-size:14px;font-weight:600",[172,178,184],{"type":16,"tag":21,"props":173,"children":175},{"x":109,"y":174},"82",[176],{"type":21,"value":177},"1. inline-css",{"type":16,"tag":21,"props":179,"children":181},{"x":109,"y":180},"202",[182],{"type":21,"value":183},"2. Handlebars, strict mode",{"type":16,"tag":21,"props":185,"children":187},{"x":109,"y":186},"322",[188],{"type":21,"value":189},"3. A new Chrome for every call",{"type":16,"tag":101,"props":191,"children":192},{"style":103,"fill":98,"textAnchor":105},[193,199,205,211,217,223],{"type":16,"tag":21,"props":194,"children":196},{"x":109,"y":195},"106",[197],{"type":21,"value":198},"Removed the \u003Cstyle> tag",{"type":16,"tag":21,"props":200,"children":202},{"x":109,"y":201},"126",[203],{"type":21,"value":204},"Threw on the page-number rule",{"type":16,"tag":21,"props":206,"children":208},{"x":109,"y":207},"226",[209],{"type":21,"value":210},"Any {{ }} in your HTML throws",{"type":16,"tag":21,"props":212,"children":214},{"x":109,"y":213},"246",[215],{"type":21,"value":216},"Your markup is read as a template",{"type":16,"tag":21,"props":218,"children":220},{"x":109,"y":219},"346",[221],{"type":21,"value":222},"About 2.2 s a PDF, launch included",{"type":16,"tag":21,"props":224,"children":226},{"x":109,"y":225},"366",[227],{"type":21,"value":228},"Left open when a step fails",{"type":16,"tag":17,"props":230,"children":231},{},[232,238,240,246,248,254,256,262,264,270,272,277,279,285,287,293],{"type":16,"tag":233,"props":234,"children":235},"strong",{},[236],{"type":21,"value":237},"1. inline-css.",{"type":21,"value":239}," It copies your CSS into ",{"type":16,"tag":24,"props":241,"children":243},{"className":242},[],[244],{"type":21,"value":245},"style",{"type":21,"value":247}," attributes and then removes the ",{"type":16,"tag":24,"props":249,"children":251},{"className":250},[],[252],{"type":21,"value":253},"\u003Cstyle>",{"type":21,"value":255}," tag, which is what its ",{"type":16,"tag":24,"props":257,"children":259},{"className":258},[],[260],{"type":21,"value":261},"removeStyleTags",{"type":21,"value":263}," option does by default. Rules that can't be inlined go with it, ",{"type":16,"tag":24,"props":265,"children":267},{"className":266},[],[268],{"type":21,"value":269},"@page",{"type":21,"value":271}," size and margins included. Our invoice also had a page-number footer in an ",{"type":16,"tag":24,"props":273,"children":275},{"className":274},[],[276],{"type":21,"value":269},{"type":21,"value":278}," margin box (",{"type":16,"tag":24,"props":280,"children":282},{"className":281},[],[283],{"type":21,"value":284},"@bottom-right { content: \"Page \" counter(page) \" of \" counter(pages) }",{"type":21,"value":286},"). inline-css couldn't parse it and threw ",{"type":16,"tag":24,"props":288,"children":290},{"className":289},[],[291],{"type":21,"value":292},"Error: Unexpected } (line 2, char 128)",{"type":21,"value":294},", so there was no PDF at all.",{"type":16,"tag":17,"props":296,"children":297},{},[298,303,305,311,313,319],{"type":16,"tag":233,"props":299,"children":300},{},[301],{"type":21,"value":302},"2. Handlebars in strict mode.",{"type":21,"value":304}," html-pdf-node compiles your HTML as a Handlebars template, with your HTML passed in as the data. Any double braces in it make the call fail. ",{"type":16,"tag":24,"props":306,"children":308},{"className":307},[],[309],{"type":21,"value":310},"\u003Cp>Hello {{name}}\u003C/p>",{"type":21,"value":312}," threw ",{"type":16,"tag":24,"props":314,"children":316},{"className":315},[],[317],{"type":21,"value":318},"Cannot use 'in' operator to search for 'name' in \u003Cp>Hello {{name}}\u003C/p>",{"type":21,"value":320},". Double braces turn up in code samples and in Vue or Angular markup.",{"type":16,"tag":17,"props":322,"children":323},{},[324,329,331,337,339,345],{"type":16,"tag":233,"props":325,"children":326},{},[327],{"type":21,"value":328},"3. A new browser per call.",{"type":21,"value":330}," ",{"type":16,"tag":24,"props":332,"children":334},{"className":333},[],[335],{"type":21,"value":336},"generatePdf",{"type":21,"value":338}," launches Chrome with ",{"type":16,"tag":24,"props":340,"children":342},{"className":341},[],[343],{"type":21,"value":344},"--no-sandbox",{"type":21,"value":346}," before steps 1 and 2, prints one PDF and closes the browser, but only when everything worked. When a step throws, Chrome stays open. Three failed requests to our Express handler left three Chrome processes running.",{"type":16,"tag":17,"props":348,"children":349},{},[350,352,361,363,369,371,377,379,385,387,393],{"type":21,"value":351},"Its last release, 1.0.8, came out on December 14, 2021, and it still gets ",{"type":16,"tag":353,"props":354,"children":358},"a",{"href":355,"rel":356},"https://www.npmjs.com/package/html-pdf-node",[357],"nofollow",[359],{"type":21,"value":360},"about 50,000 downloads a week",{"type":21,"value":362},". It pins Puppeteer ",{"type":16,"tag":24,"props":364,"children":366},{"className":365},[],[367],{"type":21,"value":368},"^10.4.0",{"type":21,"value":370}," from September 2021, which installs Chromium 93. Installing it today prints npm's own warning, ",{"type":16,"tag":24,"props":372,"children":374},{"className":373},[],[375],{"type":21,"value":376},"puppeteer@10.4.0: \u003C 24.15.0 is no longer supported",{"type":21,"value":378},", and ",{"type":16,"tag":24,"props":380,"children":382},{"className":381},[],[383],{"type":21,"value":384},"npm audit",{"type":21,"value":386}," reports 14 high-severity vulnerabilities. On an Apple Silicon Mac without Rosetta, the Chromium it downloads is an x86_64 build that won't start (",{"type":16,"tag":24,"props":388,"children":390},{"className":389},[],[391],{"type":21,"value":392},"spawn Unknown system error -86",{"type":21,"value":394},"), so we pointed it at a current Chrome 157. The three failures described above come from html-pdf-node's own code, not from the old browser.",{"type":16,"tag":17,"props":396,"children":397},{},[398,400,406,408,415],{"type":21,"value":399},"The older ",{"type":16,"tag":24,"props":401,"children":403},{"className":402},[],[404],{"type":21,"value":405},"html-pdf",{"type":21,"value":407}," package (PhantomJS) isn't a fix either. It's deprecated on npm, and ",{"type":16,"tag":353,"props":409,"children":412},{"href":410,"rel":411},"https://www.npmjs.com/package/html-pdf",[357],[413],{"type":21,"value":414},"its notice",{"type":21,"value":416}," reads \"Please migrate your projects to a newer library like puppeteer\".",{"type":16,"tag":41,"props":418,"children":420},{"id":419},"how-do-you-replace-html-pdf-node-with-puppeteer",[421],{"type":21,"value":422},"How do you replace html-pdf-node with Puppeteer?",{"type":16,"tag":17,"props":424,"children":425},{},[426],{"type":21,"value":427},"Say your app has this Express route. It takes HTML in a POST and returns a PDF:",{"type":16,"tag":429,"props":430,"children":435},"pre",{"className":431,"code":433,"language":434,"meta":7},[432],"language-js","// Before: html-pdf-node\nconst express = require('express');\nconst html_to_pdf = require('html-pdf-node');\n\nconst app = express();\napp.use(express.text({ type: 'text/html', limit: '5mb' }));\n\napp.post('/invoice.pdf', async (req, res) => {\n  const pdf = await html_to_pdf.generatePdf(\n    { content: req.body },\n    { format: 'A4', printBackground: true }\n  );\n  res.type('application/pdf').send(pdf);\n});\n\napp.listen(3001);\n","js",[436],{"type":16,"tag":24,"props":437,"children":438},{"__ignoreMap":7},[439],{"type":21,"value":433},{"type":16,"tag":17,"props":441,"children":442},{},[443],{"type":21,"value":444},"And here's the same route with Puppeteer. The browser starts once, when the server starts, and each request gets its own page:",{"type":16,"tag":429,"props":446,"children":449},{"className":447,"code":448,"language":434,"meta":7},[432],"// After: Puppeteer, one browser for the whole process\nimport express from 'express';\nimport puppeteer from 'puppeteer';\n\nconst browser = await puppeteer.launch();\n\nconst app = express();\napp.use(express.text({ type: 'text/html', limit: '5mb' }));\n\napp.post('/invoice.pdf', async (req, res) => {\n  const page = await browser.newPage();\n  try {\n    await page.setContent(req.body, { waitUntil: 'networkidle0' });\n    const pdf = await page.pdf({\n      format: 'A4',\n      printBackground: true,\n      preferCSSPageSize: true,\n    });\n    res.type('application/pdf').send(Buffer.from(pdf));\n  } finally {\n    await page.close();\n  }\n});\n\napp.listen(3002);\n",[450],{"type":16,"tag":24,"props":451,"children":452},{"__ignoreMap":7},[453],{"type":21,"value":448},{"type":16,"tag":17,"props":455,"children":456},{},[457,459,465,467,473,475,481,483,489,491,497,499,504,506,511],{"type":21,"value":458},"The options carry over, because html-pdf-node passes them straight to Puppeteer's ",{"type":16,"tag":24,"props":460,"children":462},{"className":461},[],[463],{"type":21,"value":464},"page.pdf()",{"type":21,"value":466},". Keep ",{"type":16,"tag":24,"props":468,"children":470},{"className":469},[],[471],{"type":21,"value":472},"printBackground: true",{"type":21,"value":474}," (the default is ",{"type":16,"tag":24,"props":476,"children":478},{"className":477},[],[479],{"type":21,"value":480},"false",{"type":21,"value":482},") and set ",{"type":16,"tag":24,"props":484,"children":486},{"className":485},[],[487],{"type":21,"value":488},"format",{"type":21,"value":490},", because Puppeteer defaults to Letter. Add ",{"type":16,"tag":24,"props":492,"children":494},{"className":493},[],[495],{"type":21,"value":496},"preferCSSPageSize: true",{"type":21,"value":498}," so an ",{"type":16,"tag":24,"props":500,"children":502},{"className":501},[],[503],{"type":21,"value":269},{"type":21,"value":505}," size in your CSS wins over ",{"type":16,"tag":24,"props":507,"children":509},{"className":508},[],[510],{"type":21,"value":488},{"type":21,"value":512},".",{"type":16,"tag":17,"props":514,"children":515},{},[516],{"type":21,"value":517},"We posted the same invoice to both handlers. html-pdf-node answered 500. Puppeteer answered 200 with a 2-page A4 PDF: \"Page 1 of 2\" and \"Page 2 of 2\" in the footer, the table's header row repeated at the top of page 2, and the invoice's one line of JavaScript had run. To get any PDF out of html-pdf-node, we had to delete the page-number rule. Here's what both handlers printed from that cut-down invoice:",{"type":16,"tag":53,"props":519,"children":521},{"caption":520},"Page 1 of the same invoice, without its page-number rule, from each Express handler. Our test, November 13, 2026.",[522],{"type":16,"tag":58,"props":523,"children":528},{"viewBox":524,"xmlns":61,"role":62,"ariaLabelledBy":525,"style":66,"fontFamily":67},"0 0 360 262",[526,527],"h2n-pages-title","h2n-pages-desc",[529,534,539,553,603,757,767],{"type":16,"tag":70,"props":530,"children":531},{"id":526},[532],{"type":21,"value":533},"html-pdf-node printed to the edge of the page; Puppeteer kept the 15 mm margins",{"type":16,"tag":76,"props":535,"children":536},{"id":527},[537],{"type":21,"value":538},"Two A4 pages side by side. Left, html-pdf-node: the invoice header, addresses and table run to the edges of the sheet, because the @page margins were removed with the style tag. Right, Puppeteer: the same content sits inside 15 millimetre margins on every side.",{"type":16,"tag":101,"props":540,"children":542},{"fill":149,"stroke":98,"strokeWidth":541},"1",[543,549],{"type":16,"tag":153,"props":544,"children":548},{"x":110,"y":545,"width":546,"height":547},"8","150","212",[],{"type":16,"tag":153,"props":550,"children":552},{"x":551,"y":545,"width":546,"height":547},"190",[],{"type":16,"tag":101,"props":554,"children":555},{"fill":104},[556,560,565,571,575,579,585,590,594,598],{"type":16,"tag":153,"props":557,"children":559},{"x":110,"y":545,"width":155,"height":558},"9",[],{"type":16,"tag":153,"props":561,"children":564},{"x":562,"y":545,"width":563,"height":90},"130","40",[],{"type":16,"tag":153,"props":566,"children":570},{"x":110,"y":567,"width":568,"height":569},"30","34","4",[],{"type":16,"tag":153,"props":572,"children":574},{"x":573,"y":567,"width":568,"height":569},"95",[],{"type":16,"tag":153,"props":576,"children":578},{"x":110,"y":577,"width":546,"height":91},"48",[],{"type":16,"tag":153,"props":580,"children":584},{"x":581,"y":582,"width":583,"height":91},"198","16","44",[],{"type":16,"tag":153,"props":586,"children":589},{"x":587,"y":582,"width":588,"height":569},"300","32",[],{"type":16,"tag":153,"props":591,"children":593},{"x":581,"y":568,"width":129,"height":592},"3",[],{"type":16,"tag":153,"props":595,"children":597},{"x":596,"y":568,"width":129,"height":592},"266",[],{"type":16,"tag":153,"props":599,"children":602},{"x":581,"y":130,"width":600,"height":601},"134","6",[],{"type":16,"tag":101,"props":604,"children":606},{"stroke":150,"strokeWidth":605},"2",[607,611,615,619,623,627,631,635,639,642,645,649,652,656,660,664,668,671,674,678,682,687,691,695,698,702,706,710,714,718,722,726,730,734,738,741,745,749,753],{"type":16,"tag":127,"props":608,"children":610},{"x1":110,"y1":609,"x2":135,"y2":609},"62",[],{"type":16,"tag":127,"props":612,"children":614},{"x1":110,"y1":613,"x2":135,"y2":613},"70",[],{"type":16,"tag":127,"props":616,"children":618},{"x1":110,"y1":617,"x2":135,"y2":617},"78",[],{"type":16,"tag":127,"props":620,"children":622},{"x1":110,"y1":621,"x2":135,"y2":621},"86",[],{"type":16,"tag":127,"props":624,"children":626},{"x1":110,"y1":625,"x2":135,"y2":625},"94",[],{"type":16,"tag":127,"props":628,"children":630},{"x1":110,"y1":629,"x2":135,"y2":629},"102",[],{"type":16,"tag":127,"props":632,"children":634},{"x1":110,"y1":633,"x2":135,"y2":633},"110",[],{"type":16,"tag":127,"props":636,"children":638},{"x1":110,"y1":637,"x2":135,"y2":637},"118",[],{"type":16,"tag":127,"props":640,"children":641},{"x1":110,"y1":201,"x2":135,"y2":201},[],{"type":16,"tag":127,"props":643,"children":644},{"x1":110,"y1":600,"x2":135,"y2":600},[],{"type":16,"tag":127,"props":646,"children":648},{"x1":110,"y1":647,"x2":135,"y2":647},"142",[],{"type":16,"tag":127,"props":650,"children":651},{"x1":110,"y1":546,"x2":135,"y2":546},[],{"type":16,"tag":127,"props":653,"children":655},{"x1":110,"y1":654,"x2":135,"y2":654},"158",[],{"type":16,"tag":127,"props":657,"children":659},{"x1":110,"y1":658,"x2":135,"y2":658},"166",[],{"type":16,"tag":127,"props":661,"children":663},{"x1":110,"y1":662,"x2":135,"y2":662},"174",[],{"type":16,"tag":127,"props":665,"children":667},{"x1":110,"y1":666,"x2":135,"y2":666},"182",[],{"type":16,"tag":127,"props":669,"children":670},{"x1":110,"y1":551,"x2":135,"y2":551},[],{"type":16,"tag":127,"props":672,"children":673},{"x1":110,"y1":581,"x2":135,"y2":581},[],{"type":16,"tag":127,"props":675,"children":677},{"x1":110,"y1":676,"x2":135,"y2":676},"206",[],{"type":16,"tag":127,"props":679,"children":681},{"x1":110,"y1":680,"x2":135,"y2":680},"214",[],{"type":16,"tag":127,"props":683,"children":686},{"x1":581,"y1":684,"x2":685,"y2":684},"64","332",[],{"type":16,"tag":127,"props":688,"children":690},{"x1":581,"y1":689,"x2":685,"y2":689},"72",[],{"type":16,"tag":127,"props":692,"children":694},{"x1":581,"y1":693,"x2":685,"y2":693},"80",[],{"type":16,"tag":127,"props":696,"children":697},{"x1":581,"y1":157,"x2":685,"y2":157},[],{"type":16,"tag":127,"props":699,"children":701},{"x1":581,"y1":700,"x2":685,"y2":700},"96",[],{"type":16,"tag":127,"props":703,"children":705},{"x1":581,"y1":704,"x2":685,"y2":704},"104",[],{"type":16,"tag":127,"props":707,"children":709},{"x1":581,"y1":708,"x2":685,"y2":708},"112",[],{"type":16,"tag":127,"props":711,"children":713},{"x1":581,"y1":712,"x2":685,"y2":712},"120",[],{"type":16,"tag":127,"props":715,"children":717},{"x1":581,"y1":716,"x2":685,"y2":716},"128",[],{"type":16,"tag":127,"props":719,"children":721},{"x1":581,"y1":720,"x2":685,"y2":720},"136",[],{"type":16,"tag":127,"props":723,"children":725},{"x1":581,"y1":724,"x2":685,"y2":724},"144",[],{"type":16,"tag":127,"props":727,"children":729},{"x1":581,"y1":728,"x2":685,"y2":728},"152",[],{"type":16,"tag":127,"props":731,"children":733},{"x1":581,"y1":732,"x2":685,"y2":732},"160",[],{"type":16,"tag":127,"props":735,"children":737},{"x1":581,"y1":736,"x2":685,"y2":736},"168",[],{"type":16,"tag":127,"props":739,"children":740},{"x1":581,"y1":162,"x2":685,"y2":162},[],{"type":16,"tag":127,"props":742,"children":744},{"x1":581,"y1":743,"x2":685,"y2":743},"184",[],{"type":16,"tag":127,"props":746,"children":748},{"x1":581,"y1":747,"x2":685,"y2":747},"192",[],{"type":16,"tag":127,"props":750,"children":752},{"x1":581,"y1":751,"x2":685,"y2":751},"200",[],{"type":16,"tag":127,"props":754,"children":756},{"x1":581,"y1":755,"x2":685,"y2":755},"208",[],{"type":16,"tag":101,"props":758,"children":762},{"stroke":759,"strokeWidth":123,"fill":760,"strokeDashArray":761},"#FF9D28","none",[592,592],[763],{"type":16,"tag":153,"props":764,"children":766},{"x":581,"y":582,"width":600,"height":765},"196",[],{"type":16,"tag":101,"props":768,"children":769},{"style":103,"textAnchor":105},[770,776,782,788],{"type":16,"tag":21,"props":771,"children":773},{"x":573,"y":772,"fill":104,"style":111},"240",[774],{"type":21,"value":775},"html-pdf-node",{"type":16,"tag":21,"props":777,"children":779},{"x":573,"y":778,"fill":98},"256",[780],{"type":21,"value":781},"no margins",{"type":16,"tag":21,"props":783,"children":785},{"x":784,"y":772,"fill":104,"style":111},"265",[786],{"type":21,"value":787},"Puppeteer",{"type":16,"tag":21,"props":789,"children":790},{"x":784,"y":778,"fill":98},[791],{"type":21,"value":792},"15 mm margins kept",{"type":16,"tag":17,"props":794,"children":795},{},[796],{"type":21,"value":797},"html-pdf-node took 2.2 s per request on that cut-down invoice, browser launch included. The Puppeteer handler printed the same cut-down invoice in 1.1 s with its browser already open. Single requests on one Mac: rough numbers, not a benchmark.",{"type":16,"tag":17,"props":799,"children":800},{},[801,803,809,811,816,818,824,825,831],{"type":21,"value":802},"Prefer Playwright? It's the same route with ",{"type":16,"tag":24,"props":804,"children":806},{"className":805},[],[807],{"type":21,"value":808},"chromium.launch()",{"type":21,"value":810}," and ",{"type":16,"tag":24,"props":812,"children":814},{"className":813},[],[815],{"type":21,"value":464},{"type":21,"value":817},". It printed the same 2 pages in our script. For memory limits, crashed browsers and timeouts under load, see our guides to ",{"type":16,"tag":353,"props":819,"children":821},{"href":820},"/guides/puppeteer-pdf-in-production/",[822],{"type":21,"value":823},"Puppeteer PDF in production",{"type":21,"value":810},{"type":16,"tag":353,"props":826,"children":828},{"href":827},"/guides/playwright-pdf-in-production/",[829],{"type":21,"value":830},"Playwright PDF in production",{"type":21,"value":512},{"type":16,"tag":41,"props":833,"children":835},{"id":834},"what-does-puppeteer-install-and-why-pin-it",[836],{"type":21,"value":837},"What does Puppeteer install, and why pin it?",{"type":16,"tag":17,"props":839,"children":840},{},[841,847,849,855,857,863],{"type":16,"tag":24,"props":842,"children":844},{"className":843},[],[845],{"type":21,"value":846},"npm i puppeteer",{"type":21,"value":848}," also downloads its own browsers: Chrome for Testing and chrome-headless-shell. For Puppeteer 25.17.0 that was Chrome for Testing 157 (390 MB on our Mac) and chrome-headless-shell (210 MB), and they go in every image you ship. npm 11 now prints a warning that Puppeteer's install script isn't in your ",{"type":16,"tag":24,"props":850,"children":852},{"className":851},[],[853],{"type":21,"value":854},"allowScripts",{"type":21,"value":856}," list. It still ran and downloaded Chrome for us, but with ",{"type":16,"tag":24,"props":858,"children":860},{"className":859},[],[861],{"type":21,"value":862},"strict-allow-scripts",{"type":21,"value":864}," turned on, npm fails the install instead.",{"type":16,"tag":17,"props":866,"children":867},{},[868,870,876,878,885],{"type":21,"value":869},"Each Puppeteer release downloads one specific Chrome version, so a version bump in your lockfile is also a browser upgrade. Lock the ",{"type":16,"tag":24,"props":871,"children":873},{"className":872},[],[874],{"type":21,"value":875},"puppeteer",{"type":21,"value":877}," version and update it on purpose. One developer ",{"type":16,"tag":353,"props":879,"children":882},{"href":880,"rel":881},"https://www.reddit.com/r/node/comments/10avyxb/is_node_the_right_choice_for_html_to_pdf/",[357],[883],{"type":21,"value":884},"on r/node",{"type":21,"value":886}," got hit by exactly this: \"we didn't lock down our chrome version and after a chrome release everything we printed in the factory was shrunk in scale by like 20%.\"",{"type":16,"tag":41,"props":888,"children":890},{"id":889},"can-the-same-handler-call-an-api-instead-of-running-chrome",[891],{"type":21,"value":892},"Can the same handler call an API instead of running Chrome?",{"type":16,"tag":17,"props":894,"children":895},{},[896,898,903],{"type":21,"value":897},"Yes. Keep the route and replace the browser with one request to PDFShift. ",{"type":16,"tag":24,"props":899,"children":901},{"className":900},[],[902],{"type":21,"value":37},{"type":21,"value":904}," has been built into Node since version 18, so there's nothing to install:",{"type":16,"tag":429,"props":906,"children":909},{"className":907,"code":908,"language":434,"meta":7},[432],"import express from 'express';\n\nconst app = express();\napp.use(express.text({ type: 'text/html', limit: '5mb' }));\n\napp.post('/invoice.pdf', async (req, res) => {\n  const response = await fetch('https://api.pdfshift.io/v3/convert/pdf', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'X-API-Key': process.env.PDFSHIFT_API_KEY,\n    },\n    body: JSON.stringify({ source: req.body, format: 'A4', use_print: true }),\n  });\n  if (!response.ok) {\n    return res.status(response.status).send(await response.text());\n  }\n  res.type('application/pdf').send(Buffer.from(await response.arrayBuffer()));\n});\n\napp.listen(3003);\n",[910],{"type":16,"tag":24,"props":911,"children":912},{"__ignoreMap":7},[913],{"type":21,"value":908},{"type":16,"tag":17,"props":915,"children":916},{},[917,923,925,930],{"type":16,"tag":24,"props":918,"children":920},{"className":919},[],[921],{"type":21,"value":922},"use_print: true",{"type":21,"value":924}," renders your HTML with its print CSS, the same media Puppeteer's ",{"type":16,"tag":24,"props":926,"children":928},{"className":927},[],[929],{"type":21,"value":464},{"type":21,"value":931}," uses. We posted the full invoice, page-number rule included, and got back the same 2 pages Puppeteer printed. Each request took 1.5 to 1.9 s, network included. That's the whole setup: no Chrome in your install or your image, no browser to keep alive, and a failed request leaves nothing running.",{"type":16,"tag":17,"props":933,"children":934},{},[935,937,943,945,952,954,960],{"type":21,"value":936},"Requests without a key work too, watermarked and up to 10 a day. ",{"type":16,"tag":353,"props":938,"children":940},{"href":939},"/register/",[941],{"type":21,"value":942},"Create a free account",{"type":21,"value":944}," to get 50 credits a month, no card needed. Every parameter is in the ",{"type":16,"tag":353,"props":946,"children":949},{"href":947,"rel":948},"https://docs.pdfshift.io/",[357],[950],{"type":21,"value":951},"API docs",{"type":21,"value":953},", and our ",{"type":16,"tag":353,"props":955,"children":957},{"href":956},"/guides/node/",[958],{"type":21,"value":959},"Node guides",{"type":21,"value":961}," show the same call with axios, got and node-fetch.",{"type":16,"tag":41,"props":963,"children":965},{"id":964},"what-about-jspdf-pdfkit-pdfmake-and-html2pdfjs",[966],{"type":21,"value":967},"What about jsPDF, PDFKit, pdfmake and html2pdf.js?",{"type":16,"tag":17,"props":969,"children":970},{},[971,973,979,981,987],{"type":21,"value":972},"None of them print the HTML and CSS you render on your server the way a browser does. PDFKit and pdfmake build a PDF from code: you place the text, tables and lines yourself. jsPDF's ",{"type":16,"tag":24,"props":974,"children":976},{"className":975},[],[977],{"type":21,"value":978},".html()",{"type":21,"value":980}," depends on html2canvas, and html2pdf.js calls itself \"Client-side HTML-to-PDF rendering\". They fit a receipt you lay out in code, or a download button in the browser. For an invoice you already have as HTML, use one of the handlers above. If your app is in React, ",{"type":16,"tag":353,"props":982,"children":984},{"href":983},"/guides/react-to-pdf/",[985],{"type":21,"value":986},"our React to PDF guide",{"type":21,"value":988}," compares those routes.",{"title":7,"searchDepth":990,"depth":990,"links":991},2,[992,993,994,995,996],{"id":43,"depth":990,"text":46},{"id":419,"depth":990,"text":422},{"id":834,"depth":990,"text":837},{"id":889,"depth":990,"text":892},{"id":964,"depth":990,"text":967},"markdown","content:guides:html-to-pdf-node.md","content","guides/html-to-pdf-node.md","md",1794574966610]