<?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: David Garay</title>
    <description>The latest articles on DEV Community by David Garay (@garaekz).</description>
    <link>https://dev.to/garaekz</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3938955%2Fdbc856d2-1da4-42c8-a346-9dafa366c45b.jpeg</url>
      <title>DEV Community: David Garay</title>
      <link>https://dev.to/garaekz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/garaekz"/>
    <language>en</language>
    <item>
      <title>I Built Pliego: Native HTML-to-PDF for Laravel</title>
      <dc:creator>David Garay</dc:creator>
      <pubDate>Tue, 04 Aug 2026 20:30:27 +0000</pubDate>
      <link>https://dev.to/garaekz/i-built-pliego-native-html-to-pdf-for-laravel-12p0</link>
      <guid>https://dev.to/garaekz/i-built-pliego-native-html-to-pdf-for-laravel-12p0</guid>
      <description>&lt;p&gt;Generating a PDF from HTML sounds like a solved problem.&lt;/p&gt;

&lt;p&gt;In PHP, we already have tools such as dompdf. When browser rendering is required, Laravel applications can use Browsershot, Puppeteer, or a remote rendering service.&lt;/p&gt;

&lt;p&gt;Those options work, but they represent two very different compromises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use a PHP-native renderer with a limited interpretation of the web platform;&lt;/li&gt;
&lt;li&gt;or ship an entire browser runtime to call &lt;code&gt;printToPDF&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to explore a third option.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;Pliego&lt;/strong&gt;: an open-source native HTML-to-PDF engine built on Servo for application-owned documents such as invoices, statements, purchase orders, and operational reports.&lt;/p&gt;

&lt;p&gt;Pliego does not launch Chromium. It does not require Node.js or Java in the runtime. It also does not attempt to render arbitrary websites.&lt;/p&gt;

&lt;p&gt;Its goal is narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Provide a predictable document pipeline for trusted HTML and Blade views, with explicit rendering boundaries and useful evidence when something fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/oxhq/pliego" rel="noopener noreferrer"&gt;https://github.com/oxhq/pliego&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic Laravel experience
&lt;/h2&gt;

&lt;p&gt;The Laravel package can be installed through Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require oxhq/pliego-laravel:^0.1.0

php artisan pliego:install
php artisan pliego:doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pliego:install&lt;/code&gt; downloads the runtime pinned by the installed package, verifies its size and SHA-256, and places it in the application-managed runtime directory.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pliego:doctor&lt;/code&gt; verifies the engine API, storage permissions, bundled font handling, and an offline PDF render.&lt;/p&gt;

&lt;p&gt;Rendering a Blade view is intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Pliego\Laravel\Facades\Document&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rows'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Document-specific options can be added when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices.show'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'invoice'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'es-MX'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PST8PDT'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;denyNetwork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'fonts/invoice.woff2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;resource_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'fonts/invoice.woff2'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first release includes native runtime bundles for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux x86_64&lt;/li&gt;
&lt;li&gt;Windows x86_64&lt;/li&gt;
&lt;li&gt;macOS x86_64&lt;/li&gt;
&lt;li&gt;macOS arm64&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pliego is not a wrapper around &lt;code&gt;printToPDF&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Browsershot and similar tools generally follow this model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Laravel
  → Node.js
  → Puppeteer
  → Chromium
  → printToPDF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pliego follows a different pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Blade or HTML
  → Servo layout
  → canonical DocumentScene
  → preview, validation, evidence and PDF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Servo loads and lays out the document. Pliego then captures the computed document into a canonical scene containing pages, positioned glyphs, paths, images, links, fonts, and related resources.&lt;/p&gt;

&lt;p&gt;The PDF backend consumes that scene directly.&lt;/p&gt;

&lt;p&gt;It does not ask another engine to lay out or shape the document again.&lt;/p&gt;

&lt;p&gt;This gives Pliego a useful internal boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML/CSS/JS behavior
        ↓
DocumentScene
        ↓
PDF and diagnostic artifacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scene can be normalized and hashed. That makes it possible to inspect what Pliego captured, compare repeated renders, generate previews, and distinguish layout problems from PDF serialization problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pliego 0.1 supports
&lt;/h2&gt;

&lt;p&gt;The current release focuses on common application-owned documents.&lt;/p&gt;

&lt;p&gt;Its verified profile includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authored page breaks;&lt;/li&gt;
&lt;li&gt;paged tables;&lt;/li&gt;
&lt;li&gt;repeated table headers;&lt;/li&gt;
&lt;li&gt;row keep-together constraints;&lt;/li&gt;
&lt;li&gt;selectable text;&lt;/li&gt;
&lt;li&gt;links;&lt;/li&gt;
&lt;li&gt;embedded TTF, OTF, WOFF, and WOFF2 fonts;&lt;/li&gt;
&lt;li&gt;local application assets;&lt;/li&gt;
&lt;li&gt;network-denied rendering by default;&lt;/li&gt;
&lt;li&gt;explicit allowlists for remote stylesheets, images, and fonts;&lt;/li&gt;
&lt;li&gt;retained input, scene, resource, PDF, and diagnostic artifacts;&lt;/li&gt;
&lt;li&gt;controlled JavaScript readiness;&lt;/li&gt;
&lt;li&gt;a deliberately bounded Chart.js 4.5.1 path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository README includes two PDFs generated locally through a Laravel application using the released runtime:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A two-page invoice with embedded fonts, an authored page break, allocation bars, a dense 20-row ledger, and calculated totals.&lt;/li&gt;
&lt;li&gt;A one-page operating report containing a Chart.js graph, summary metrics, and an account table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnpv1r602vrmm50akt9p0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnpv1r602vrmm50akt9p0.png" alt="Chart.js operating report rendered by Pliego" width="800" height="1035"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The linked PDF remains text-selectable outside the rendered Chart.js canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static documents require no readiness API
&lt;/h2&gt;

&lt;p&gt;A normal Blade view does not need custom JavaScript calls.&lt;/p&gt;

&lt;p&gt;Pliego waits for the page load event and &lt;code&gt;document.fonts.ready&lt;/code&gt; before capturing the result.&lt;/p&gt;

&lt;p&gt;The explicit readiness API is only necessary when asynchronous work continues changing the document after load.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pliego&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;loadReportData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;drawReport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pliego&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pliego&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids using arbitrary sleeps such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The document declares when it is finished instead of hoping that a fixed timeout is long enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chart.js support without pretending to support everything
&lt;/h2&gt;

&lt;p&gt;The initial Chart.js integration is intentionally specific.&lt;/p&gt;

&lt;p&gt;The covered path uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chart.js 4.5.1;&lt;/li&gt;
&lt;li&gt;fixed canvas dimensions;&lt;/li&gt;
&lt;li&gt;animations and events disabled;&lt;/li&gt;
&lt;li&gt;the final chart draw completed synchronously;&lt;/li&gt;
&lt;li&gt;a full-canvas &lt;code&gt;getImageData()&lt;/code&gt; readback;&lt;/li&gt;
&lt;li&gt;readiness signaled after that readback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The retained RGBA pixels become the authoritative canvas result for the document scene.&lt;/p&gt;

&lt;p&gt;That does not imply support for every Chart.js plugin, version, rendering mode, or Canvas API.&lt;/p&gt;

&lt;p&gt;This distinction matters to Pliego.&lt;/p&gt;

&lt;p&gt;A successful fixture is not treated as proof that an entire web platform feature is universally supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail closed instead of silently dropping content
&lt;/h2&gt;

&lt;p&gt;One of the most important design decisions in Pliego is how unsupported rendering is handled.&lt;/p&gt;

&lt;p&gt;Many document engines attempt to produce something even when parts of the document cannot be represented. That can be convenient, but it can also result in documents that look complete while silently missing visual information.&lt;/p&gt;

&lt;p&gt;Pliego takes the opposite approach.&lt;/p&gt;

&lt;p&gt;If the document uses paint outside the verified profile, the normal SDK path fails and does not publish a partial PDF.&lt;/p&gt;

&lt;p&gt;The failure retains artifacts that can be inspected by the application or developer.&lt;/p&gt;

&lt;p&gt;The current unsupported paint boundary includes features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS gradients;&lt;/li&gt;
&lt;li&gt;background-image layers;&lt;/li&gt;
&lt;li&gt;box and text shadows;&lt;/li&gt;
&lt;li&gt;text decorations;&lt;/li&gt;
&lt;li&gt;rounded and mixed-color borders;&lt;/li&gt;
&lt;li&gt;transforms;&lt;/li&gt;
&lt;li&gt;opacity;&lt;/li&gt;
&lt;li&gt;filters;&lt;/li&gt;
&lt;li&gt;blend modes;&lt;/li&gt;
&lt;li&gt;general clipping behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This profile is currently narrow. That is deliberate.&lt;/p&gt;

&lt;p&gt;Pliego is based on Servo, but it does not claim that every feature Servo can calculate is already represented safely and faithfully by the Pliego scene and PDF backend.&lt;/p&gt;

&lt;p&gt;The public contract is what the complete pipeline can verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource handling is explicit
&lt;/h2&gt;

&lt;p&gt;Pliego denies network access by default.&lt;/p&gt;

&lt;p&gt;Assets can be supplied directly by the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'fonts/inter.woff2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;resource_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'fonts/inter.woff2'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remote resources must be explicitly allowed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'report'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowHttpRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://fonts.googleapis.com/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowHttpRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://fonts.gstatic.com/s/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Successful remote requests retain information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL;&lt;/li&gt;
&lt;li&gt;response status;&lt;/li&gt;
&lt;li&gt;content type;&lt;/li&gt;
&lt;li&gt;byte count;&lt;/li&gt;
&lt;li&gt;SHA-256 digest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Host-font fallback, unrestricted networking, redirects, and asset caching are disabled by default.&lt;/p&gt;

&lt;p&gt;The objective is to reduce the number of invisible environmental inputs that can change a generated document.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens during a Laravel render
&lt;/h2&gt;

&lt;p&gt;The Laravel integration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;renders the Blade view to HTML;&lt;/li&gt;
&lt;li&gt;creates a private input bundle;&lt;/li&gt;
&lt;li&gt;copies only explicitly declared local assets;&lt;/li&gt;
&lt;li&gt;hashes those assets;&lt;/li&gt;
&lt;li&gt;records locale, timezone, page geometry, and resource policy;&lt;/li&gt;
&lt;li&gt;launches one native Pliego process;&lt;/li&gt;
&lt;li&gt;waits for the render result;&lt;/li&gt;
&lt;li&gt;validates that scene capture completed;&lt;/li&gt;
&lt;li&gt;returns the PDF or a typed exception.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A successful &lt;code&gt;render()&lt;/code&gt; call returns paths to the PDF, input bundle, and retained artifacts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$pdfPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pdfPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$artifactsPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;artifactsPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$inputBundlePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inputBundlePath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed render preserves the diagnostic paths on the exception.&lt;/p&gt;

&lt;p&gt;This is useful when a failure happens inside a queue worker or production environment where opening a browser inspector is not an option.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it compare to dompdf?
&lt;/h2&gt;

&lt;p&gt;Pliego is not currently a drop-in replacement for dompdf.&lt;/p&gt;

&lt;p&gt;Dompdf is mature, PHP-native, Composer-only, and much simpler to deploy. If it already renders a document correctly, there may be no reason to replace it.&lt;/p&gt;

&lt;p&gt;Pliego uses a native Servo-based runtime and is aimed at documents that need some combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;richer layout behavior;&lt;/li&gt;
&lt;li&gt;controlled JavaScript or Canvas output;&lt;/li&gt;
&lt;li&gt;explicit resource policies;&lt;/li&gt;
&lt;li&gt;retained rendering evidence;&lt;/li&gt;
&lt;li&gt;deterministic scene identities;&lt;/li&gt;
&lt;li&gt;fail-closed behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is a newer project, a native runtime dependency, and a deliberately narrower public rendering profile.&lt;/p&gt;

&lt;p&gt;I have not yet published a fair performance comparison between the two, so Pliego currently makes no speed claim against dompdf.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it compare to Browsershot?
&lt;/h2&gt;

&lt;p&gt;Browsershot gives an application access to Chromium, which means it starts with a much broader browser compatibility surface.&lt;/p&gt;

&lt;p&gt;That is a major advantage when the input is essentially a web page.&lt;/p&gt;

&lt;p&gt;Pliego is more constrained, but it also has a different operational model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no Chromium installation;&lt;/li&gt;
&lt;li&gt;no Node.js runtime;&lt;/li&gt;
&lt;li&gt;application-owned HTML rather than arbitrary pages;&lt;/li&gt;
&lt;li&gt;explicit networking;&lt;/li&gt;
&lt;li&gt;a canonical intermediate scene;&lt;/li&gt;
&lt;li&gt;retained evidence;&lt;/li&gt;
&lt;li&gt;failure when unsupported paint would make the document incomplete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance also needs to be measured carefully.&lt;/p&gt;

&lt;p&gt;A fair benchmark should distinguish between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cold Chromium startup;&lt;/li&gt;
&lt;li&gt;a persistent remote Chrome process;&lt;/li&gt;
&lt;li&gt;Pliego’s one-render-per-process runtime;&lt;/li&gt;
&lt;li&gt;PDF-only output;&lt;/li&gt;
&lt;li&gt;PDF plus Pliego’s previews and diagnostic evidence;&lt;/li&gt;
&lt;li&gt;memory usage and concurrency;&lt;/li&gt;
&lt;li&gt;short invoices and large statements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until that benchmark exists, the meaningful comparison is architectural rather than “X is faster than Y.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Servo?
&lt;/h2&gt;

&lt;p&gt;Servo provides a real web layout engine written primarily in Rust.&lt;/p&gt;

&lt;p&gt;Pliego is maintained as a hard fork rather than as a thin wrapper that must continuously follow every upstream change.&lt;/p&gt;

&lt;p&gt;The repository preserves Servo’s source layout so upstream fixes can be reviewed and selectively integrated. An &lt;code&gt;upstream-main&lt;/code&gt; branch mirrors Servo, while temporary synchronization branches carry chosen changes into Pliego.&lt;/p&gt;

&lt;p&gt;Not every Servo feature or refactor automatically belongs in a document renderer.&lt;/p&gt;

&lt;p&gt;Updates can be evaluated based on their effect on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;document layout;&lt;/li&gt;
&lt;li&gt;security;&lt;/li&gt;
&lt;li&gt;determinism;&lt;/li&gt;
&lt;li&gt;supported fixtures;&lt;/li&gt;
&lt;li&gt;maintenance cost;&lt;/li&gt;
&lt;li&gt;the public Pliego profile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Servo provides the foundation. Pliego owns the document-oriented product boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pliego is not
&lt;/h2&gt;

&lt;p&gt;Pliego 0.1 is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a general-purpose browser;&lt;/li&gt;
&lt;li&gt;a hosted PDF service;&lt;/li&gt;
&lt;li&gt;a renderer for hostile or tenant-authored HTML;&lt;/li&gt;
&lt;li&gt;a claim of complete CSS compatibility;&lt;/li&gt;
&lt;li&gt;a persistent rendering daemon;&lt;/li&gt;
&lt;li&gt;a universal replacement for dompdf, wkhtmltopdf, or Chromium.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is an early open-source document engine with a specific goal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Render trusted application documents through a native, inspectable, and predictable pipeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;For Laravel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require oxhq/pliego-laravel:^0.1.0
php artisan pliego:install
php artisan pliego:doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rows'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository contains the support profile, native release bundles, installation documentation, Laravel examples, generated PDFs, and focused rendering fixtures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/oxhq/pliego" rel="noopener noreferrer"&gt;https://github.com/oxhq/pliego&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The feedback that would be most useful at this stage is concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did installation work in a clean project?&lt;/li&gt;
&lt;li&gt;Which real document failed?&lt;/li&gt;
&lt;li&gt;Was the failure report understandable?&lt;/li&gt;
&lt;li&gt;Which unsupported capability blocks adoption?&lt;/li&gt;
&lt;li&gt;Does the retained evidence help diagnose production rendering?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pliego is still narrow.&lt;/p&gt;

&lt;p&gt;Now it needs real documents to determine where it should grow.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>rust</category>
      <category>pdf</category>
    </item>
    <item>
      <title>I built a Laravel PDF package because I was tired of debugging screenshots from ghosts</title>
      <dc:creator>David Garay</dc:creator>
      <pubDate>Mon, 18 May 2026 21:32:07 +0000</pubDate>
      <link>https://dev.to/garaekz/i-built-a-laravel-pdf-package-because-i-was-tired-of-debugging-screenshots-from-ghosts-2872</link>
      <guid>https://dev.to/garaekz/i-built-a-laravel-pdf-package-because-i-was-tired-of-debugging-screenshots-from-ghosts-2872</guid>
      <description>&lt;p&gt;Every HTML-to-PDF system eventually hits the same wall.&lt;/p&gt;

&lt;p&gt;Chrome captures too early.&lt;/p&gt;

&lt;p&gt;Not because Chrome is broken.&lt;br&gt;
Not because the PDF library is bad.&lt;/p&gt;

&lt;p&gt;Because the renderer is guessing.&lt;/p&gt;

&lt;p&gt;Sometimes the chart is still animating.&lt;br&gt;
Sometimes the font has not swapped in yet.&lt;br&gt;
Sometimes async data arrived 200ms later on a slow queue worker.&lt;/p&gt;

&lt;p&gt;And eventually somebody reports:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The invoice from 3 days ago looked wrong.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you are debugging a render that already shipped.&lt;/p&gt;

&lt;p&gt;No logs.&lt;br&gt;
No screenshot.&lt;br&gt;
No DOM state.&lt;br&gt;
No idea what Chrome actually saw.&lt;/p&gt;

&lt;p&gt;I have fought this problem for years across different stacks and projects.&lt;/p&gt;

&lt;p&gt;The usual fixes always looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;waitUntilNetworkIdle&lt;/span&gt;
&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// cross fingers &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works.&lt;br&gt;
Until it does not.&lt;/p&gt;

&lt;p&gt;The core problem is simple:&lt;/p&gt;

&lt;p&gt;The renderer does not actually know when your application is ready.&lt;/p&gt;

&lt;p&gt;Only your application knows that.&lt;/p&gt;

&lt;p&gt;So I built Canio.&lt;/p&gt;
&lt;h2&gt;
  
  
  The readiness contract
&lt;/h2&gt;

&lt;p&gt;Instead of relying on timing heuristics, the page explicitly signals readiness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__CANIO_READY__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That signal can happen after:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fonts are loaded&lt;/li&gt;
&lt;li&gt;charts finish animating&lt;/li&gt;
&lt;li&gt;async requests complete&lt;/li&gt;
&lt;li&gt;Vue hydration finishes&lt;/li&gt;
&lt;li&gt;whatever “done” means for your document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The renderer stops guessing.&lt;/p&gt;

&lt;p&gt;Your application decides when capture happens.&lt;/p&gt;

&lt;p&gt;That single inversion changes the entire rendering model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real feature is not readiness
&lt;/h2&gt;

&lt;p&gt;The part I actually care about is this:&lt;/p&gt;

&lt;p&gt;Every render can leave evidence.&lt;/p&gt;

&lt;p&gt;Canio can persist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML source&lt;/li&gt;
&lt;li&gt;DOM snapshot&lt;/li&gt;
&lt;li&gt;screenshot at capture time&lt;/li&gt;
&lt;li&gt;console logs&lt;/li&gt;
&lt;li&gt;network logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when someone says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This PDF looked broken last Tuesday.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You do not try to reproduce the race condition.&lt;/p&gt;

&lt;p&gt;You open the artifact screenshot and see exactly what Chromium saw.&lt;/p&gt;

&lt;p&gt;That changes PDF debugging from archaeology into inspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Most HTML-to-PDF tooling treats rendering as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML in
PDF out 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But rendering is not instantaneous.&lt;/p&gt;

&lt;p&gt;Modern pages are temporal systems.&lt;/p&gt;

&lt;p&gt;Fonts load later.&lt;br&gt;
Animations complete later.&lt;br&gt;
Hydration finishes later.&lt;br&gt;
Data arrives later.&lt;/p&gt;

&lt;p&gt;The renderer is sampling a moving target.&lt;/p&gt;

&lt;p&gt;Canio treats rendering as a synchronization problem instead of a timeout problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;Canio uses a Go runtime called Stagehand.&lt;/p&gt;

&lt;p&gt;Stagehand talks directly to real Chromium over CDP.&lt;/p&gt;

&lt;p&gt;Why Go instead of Node?&lt;/p&gt;

&lt;p&gt;Mostly deployment ergonomics.&lt;/p&gt;

&lt;p&gt;I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a single static binary&lt;/li&gt;
&lt;li&gt;predictable memory usage&lt;/li&gt;
&lt;li&gt;no node_modules in production&lt;/li&gt;
&lt;li&gt;isolated render infrastructure&lt;/li&gt;
&lt;li&gt;simpler containerization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Canio supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embedded runtime&lt;/li&gt;
&lt;li&gt;remote CDP runtime&lt;/li&gt;
&lt;li&gt;existing local Chrome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also installs a pinned Chrome for Testing build automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browsershot is still excellent
&lt;/h2&gt;

&lt;p&gt;This is important to say clearly:&lt;/p&gt;

&lt;p&gt;Browsershot is excellent.&lt;/p&gt;

&lt;p&gt;I still use it.&lt;/p&gt;

&lt;p&gt;Canio is not trying to replace Browsershot for simple rendering.&lt;/p&gt;

&lt;p&gt;Canio exists for the cases where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rendering timing matters&lt;/li&gt;
&lt;li&gt;deterministic capture matters&lt;/li&gt;
&lt;li&gt;production debugging matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the lane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require oxhq/canio
php artisan canio:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo:&lt;br&gt;
&lt;a href="https://github.com/oxhq/canio" rel="noopener noreferrer"&gt;https://github.com/oxhq/canio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback and criticism are genuinely welcome.&lt;br&gt;
Especially from people who have fought HTML-to-PDF rendering in production.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>opensource</category>
      <category>php</category>
      <category>go</category>
    </item>
  </channel>
</rss>
