<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gábor Liktor</title>
    <description>The latest articles on DEV Community by Gábor Liktor (@gbor_liktor_5ba6846f550d).</description>
    <link>https://dev.to/gbor_liktor_5ba6846f550d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909476%2F9d069385-e3e0-4d8e-9c0a-08e9c4bc692c.jpg</url>
      <title>DEV Community: Gábor Liktor</title>
      <link>https://dev.to/gbor_liktor_5ba6846f550d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gbor_liktor_5ba6846f550d"/>
    <language>en</language>
    <item>
      <title>Generating PDFs shouldn't be this hard in 2026</title>
      <dc:creator>Gábor Liktor</dc:creator>
      <pubDate>Sun, 03 May 2026 12:27:00 +0000</pubDate>
      <link>https://dev.to/gbor_liktor_5ba6846f550d/generating-pdfs-shouldnt-be-this-hard-in-2026-306m</link>
      <guid>https://dev.to/gbor_liktor_5ba6846f550d/generating-pdfs-shouldnt-be-this-hard-in-2026-306m</guid>
      <description>&lt;p&gt;There's a moment every developer knows. You need to generate a PDF. It looks simple. You've done harder things.&lt;/p&gt;

&lt;p&gt;Three hours later, you're reading a Stack Overflow thread from 2016 that ends with "works on my machine."&lt;/p&gt;

&lt;p&gt;This post is about that moment — the actual options, what breaks in each, and where I landed after years of hitting this in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The options are far from perfect
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1, wkhtmltopdf: The Aging Classic
&lt;/h3&gt;

&lt;p&gt;It uses a stripped-down WebKit engine and converts HTML to PDF directly. Simple to install, simple to use, and it works fine if your document is simple HTML from 2012.&lt;/p&gt;

&lt;p&gt;The problems show up when you use modern CSS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No Flexbox support (or extremely broken)&lt;/li&gt;
&lt;li&gt;No CSS Grid&lt;/li&gt;
&lt;li&gt;No support for &lt;code&gt;@page&lt;/code&gt;: Doing headers/footers is a nightmare.&lt;/li&gt;
&lt;li&gt;The last release was 2019. The project is unmaintained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For basic invoices with tables and some styling, wkhtmltopdf can still work. The moment your designer touches the template, you have a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  2, Puppeteer / Playwright
&lt;/h3&gt;

&lt;p&gt;The modern approach. Spin up a real headless Chrome, navigate to the URL or inject HTML, call &lt;code&gt;page.pdf()&lt;/code&gt;. Full CSS and JS support — if Chrome renders it, this renders it.&lt;/p&gt;

&lt;p&gt;The infrastructure cost is real:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker:&lt;/strong&gt; Chrome needs specific flags to run inside a container (&lt;code&gt;--no-sandbox&lt;/code&gt;, &lt;code&gt;--disable-setuid-sandbox&lt;/code&gt;). Get one wrong and you get a cryptic crash at 2am. You also need specific base images — the official Chrome Docker setup is not a one-liner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; Chrome is not lightweight. A pool of 3 Chrome instances will use 600–900MB on a modest server. Under load spikes, you'll OOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Display server:&lt;/strong&gt; Puppeteer needs a display to run. In a standard Linux server environment, you need Xvfb or the &lt;code&gt;--headless&lt;/code&gt; flag handled correctly. Miss this and the server works locally and fails in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version pinning:&lt;/strong&gt; Chrome updates frequently. The Puppeteer version and Chrome version need to stay in sync or you'll get subtle rendering changes in production that nobody notices for weeks.&lt;/p&gt;

&lt;p&gt;None of these are insurmountable. They're all solved problems. But they're also not your problem to solve — they're infrastructure problems that eat hours if not days that should go to your product.&lt;/p&gt;

&lt;h3&gt;
  
  
  3, LibreOffice headless
&lt;/h3&gt;

&lt;p&gt;For DOCX, XLSX, PPTX → PDF, LibreOffice is essentially the only real option. Nothing else handles Office formats reliably without a full Office licence.&lt;/p&gt;

&lt;p&gt;The problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Needs a display server (same as Puppeteer)&lt;/li&gt;
&lt;li&gt;Memory leaks under sustained load — needs periodic restarts&lt;/li&gt;
&lt;li&gt;Rendering fidelity varies by document complexity&lt;/li&gt;
&lt;li&gt;Startup time is slow (200–400ms cold, longer under load)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LibreOffice is powerful software doing a hard job. It's just not something you should be babysitting in your application layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I wanted (and why I built APIJolt)
&lt;/h2&gt;

&lt;p&gt;I'd hit all of the above in different projects. What I wanted was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One HTTPS call, get a PDF back&lt;/li&gt;
&lt;li&gt;Every format: HTML, URL, DOCX, XLSX, PPTX, images&lt;/li&gt;
&lt;li&gt;Flat pricing — no credits, no expiring balances&lt;/li&gt;
&lt;li&gt;Files not stored indefinitely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;a href="https://apijolt.com" rel="noopener noreferrer"&gt;APIJolt&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the API works
&lt;/h2&gt;

&lt;p&gt;The simplest case — HTML string to PDF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.apijolt.com/v1/html-to-pdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer aj_live_xxxx"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"html": "&amp;lt;h1&amp;gt;Invoice #1042&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Due: $240.00&amp;lt;/p&amp;gt;"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a binary PDF back. That's it.&lt;/p&gt;

&lt;p&gt;For a URL conversion (useful when your app already renders the page):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.apijolt.com/v1/url-to-pdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer aj_live_xxxx"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://yourapp.com/invoice/1234"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For no-code tools (n8n, Zapier, Make) where you need JSON not binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Invoice&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"base64"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;response_type&lt;/code&gt; parameter covers all integration patterns without adapters: &lt;code&gt;binary&lt;/code&gt; for direct streaming/storage, &lt;code&gt;base64&lt;/code&gt; for JSON workflows, &lt;code&gt;url&lt;/code&gt; for async jobs.&lt;/p&gt;

&lt;p&gt;For DOCX → PDF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.apijolt.com/v1/word-to-pdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@contract.docx"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer aj_live_xxxx"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full format support: HTML, URL, Markdown, DOCX/DOC/ODT/RTF, XLSX/XLS/ODS/CSV, PPTX/PPT/ODP, JPG/PNG/WebP/GIF/BMP/TIFF. There's also a &lt;code&gt;/v1/convert-to-pdf&lt;/code&gt; endpoint that &lt;strong&gt;auto-detects the format.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's under the hood
&lt;/h2&gt;

&lt;p&gt;Chrome pool (Puppeteer) for HTML and URL conversions — full CSS3, Flexbox, Grid, JavaScript. LibreOffice headless for Office formats. ImageMagick for images. It's the same stack you'd build yourself — I'm just managing it so you don't have to.&lt;/p&gt;

&lt;p&gt;Files are deleted after 1 hour. Not in a Trust Center — right here.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pricing decision
&lt;/h2&gt;

&lt;p&gt;I debated this a lot. Credit systems are the default in this space and they make revenue predictable from the provider side. But from the user side, credits expire, overages surprise you and you're constantly aware of a meter running.&lt;/p&gt;

&lt;p&gt;I went with flat monthly with a hard cap and an upgrade prompt. You know exactly what you're paying. No surprises.&lt;/p&gt;

&lt;p&gt;The free tier is 1,000 conversions/month, all formats.&lt;/p&gt;




&lt;h2&gt;
  
  
  Help me shape the Roadmap
&lt;/h2&gt;

&lt;p&gt;I'm currently in beta and I want to build what &lt;em&gt;you&lt;/em&gt; actually need. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I focus on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better templating (sending JSON data to a pre-defined layout)?&lt;/li&gt;
&lt;li&gt;Tighter GDPR/Data Privacy compliance?&lt;/li&gt;
&lt;li&gt;Direct S3/Google Drive uploads?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have 90 seconds, I’d love your expert feedback in this &lt;strong&gt;&lt;a href="https://tally.so/r/xX9Qdr?source=devto" rel="noopener noreferrer"&gt;quick survey&lt;/a&gt;&lt;/strong&gt;. Your input will literally determine what I build in the next 3 months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it out:&lt;/strong&gt; &lt;a href="https://apijolt.com" rel="noopener noreferrer"&gt;apijolt.com&lt;/a&gt; (No card required, first conversion in under 5 minutes).&lt;/p&gt;

&lt;p&gt;I'll be in the comments — let me know how you're currently handling (or suffering through) PDF generation!&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>showdev</category>
      <category>api</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
