<?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: Michael Oteng</title>
    <description>The latest articles on DEV Community by Michael Oteng (@xetera1).</description>
    <link>https://dev.to/xetera1</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%2F4121886%2Fcedc935e-7231-4384-bfe7-adc8dfd1c4dc.png</url>
      <title>DEV Community: Michael Oteng</title>
      <link>https://dev.to/xetera1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xetera1"/>
    <language>en</language>
    <item>
      <title>What Building a Browser-Based PDF Editor Taught Us About PDF Coordinates, State and Export</title>
      <dc:creator>Michael Oteng</dc:creator>
      <pubDate>Fri, 09 Oct 2026 20:40:08 +0000</pubDate>
      <link>https://dev.to/swiftvecto/what-building-a-browser-based-pdf-editor-taught-us-about-pdf-coordinates-state-and-export-4ogg</link>
      <guid>https://dev.to/swiftvecto/what-building-a-browser-based-pdf-editor-taught-us-about-pdf-coordinates-state-and-export-4ogg</guid>
      <description>&lt;p&gt;Building a PDF editor looks deceptively simple at first.&lt;/p&gt;

&lt;p&gt;Render a PDF in the browser, place some controls around it, let the user add text or images, and then save the result.&lt;/p&gt;

&lt;p&gt;That sounds reasonable until the first object has to survive zooming, page resizing and export.&lt;/p&gt;

&lt;p&gt;While developing the Edit PDF tool for SwiftVecto, we found that the visible editing interface was only a small part of the problem.&lt;/p&gt;

&lt;p&gt;The harder work was underneath it.&lt;/p&gt;

&lt;p&gt;How do you make sure an object placed at a particular position in the browser appears at exactly the same position in the exported PDF?&lt;/p&gt;

&lt;p&gt;How do you represent edits without constantly rewriting the original document?&lt;/p&gt;

&lt;p&gt;What happens when pages have different dimensions?&lt;/p&gt;

&lt;p&gt;What does "redact" actually mean?&lt;/p&gt;

&lt;p&gt;And when should an operation happen in the browser rather than on the server?&lt;/p&gt;

&lt;p&gt;These turned out to be some of the most interesting engineering problems in the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The screen is not the PDF
&lt;/h2&gt;

&lt;p&gt;One of the first problems is coordinate systems.&lt;/p&gt;

&lt;p&gt;Browser interfaces normally work from the top-left:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0,0 ─────────────→ X
 │
 │
 ↓
 Y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PDF coordinate systems traditionally work from the bottom-left:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Y
 ↑
 │
 │
0,0 ─────────────→ X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Already, an object's Y position cannot simply be copied from the browser into the PDF.&lt;/p&gt;

&lt;p&gt;But that is only the beginning.&lt;/p&gt;

&lt;p&gt;Now introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zoom&lt;/li&gt;
&lt;li&gt;page rotation&lt;/li&gt;
&lt;li&gt;crop boxes&lt;/li&gt;
&lt;li&gt;media boxes&lt;/li&gt;
&lt;li&gt;different page dimensions&lt;/li&gt;
&lt;li&gt;device pixel ratios&lt;/li&gt;
&lt;li&gt;responsive editor layouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A text object that appears at &lt;code&gt;(300, 420)&lt;/code&gt; on the screen does not necessarily belong at &lt;code&gt;(300, 420)&lt;/code&gt; in the PDF.&lt;/p&gt;

&lt;p&gt;This becomes especially obvious when zooming.&lt;/p&gt;

&lt;p&gt;Suppose the user positions an object while viewing a page at 80% zoom. If its stored coordinates are based on screen pixels, changing the zoom can effectively change the document.&lt;/p&gt;

&lt;p&gt;That is the wrong model.&lt;/p&gt;

&lt;p&gt;Zoom should change the &lt;strong&gt;view&lt;/strong&gt;, not the underlying document state.&lt;/p&gt;

&lt;p&gt;A much safer architecture is to treat coordinate conversion as its own subsystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PDF coordinates
      ↕
Editor coordinates
      ↕
Screen coordinates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Features then use the same transformation layer rather than implementing their own coordinate calculations.&lt;/p&gt;

&lt;p&gt;That becomes increasingly important as the editor grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Don't rewrite the PDF after every edit
&lt;/h2&gt;

&lt;p&gt;Another important decision was how to represent the document while it is being edited.&lt;/p&gt;

&lt;p&gt;The tempting approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User adds text
      ↓
Modify PDF

User adds image
      ↓
Modify PDF again

User moves object
      ↓
Modify PDF again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That quickly becomes difficult to manage.&lt;/p&gt;

&lt;p&gt;Instead, we found it much more useful to think of the editor as maintaining its own document state.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original PDF
     │
     ├── Original pages
     │
     └── Editor state
           ├── Page operations
           ├── Text objects
           ├── Images
           ├── Drawings
           ├── Annotations
           ├── Links
           ├── Forms
           └── Redactions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An editor object can then have a structure along these lines:&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;obj_82&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;pageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page_1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nx"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SwiftVecto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Helvetica&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#111827&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the exact schema.&lt;/p&gt;

&lt;p&gt;It is the separation between the &lt;strong&gt;source document&lt;/strong&gt; and the &lt;strong&gt;editing state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That separation makes features such as moving objects, undo/redo, deleting objects and changing properties considerably easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Stable page identity matters
&lt;/h2&gt;

&lt;p&gt;Page management introduces another interesting problem.&lt;/p&gt;

&lt;p&gt;Imagine a five-page document.&lt;/p&gt;

&lt;p&gt;The user adds something to page 3.&lt;/p&gt;

&lt;p&gt;Then they move page 3 to the beginning of the document.&lt;/p&gt;

&lt;p&gt;Is the object attached to page 3, or to the page that is now page 1?&lt;/p&gt;

&lt;p&gt;If objects are associated only with page numbers, reordering becomes dangerous.&lt;/p&gt;

&lt;p&gt;A better model gives each page a stable internal identity.&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 javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page_a81f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;sourcePage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visible page number can change.&lt;/p&gt;

&lt;p&gt;The page identity does not.&lt;/p&gt;

&lt;p&gt;That distinction makes page reordering much easier to manage because objects belong to a page entity rather than to a temporary position in an array.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Added content and existing content are different problems
&lt;/h2&gt;

&lt;p&gt;Adding new text to a PDF and editing text that already exists in the PDF may look like the same operation to a user.&lt;/p&gt;

&lt;p&gt;Technically, they are very different.&lt;/p&gt;

&lt;p&gt;Adding new text can often be represented as a new editor object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page
 + text object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Existing PDF text may instead be represented internally as positioned glyphs, text runs, font references and drawing instructions.&lt;/p&gt;

&lt;p&gt;There may not be anything resembling the editable paragraph you would find in a word processor.&lt;/p&gt;

&lt;p&gt;This is one reason PDF editing becomes significantly more complicated once an editor moves beyond adding objects and starts modifying the document's existing content.&lt;/p&gt;

&lt;p&gt;The UI may make both operations look similar.&lt;/p&gt;

&lt;p&gt;The document model underneath them is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Redaction is not a black rectangle
&lt;/h2&gt;

&lt;p&gt;This was one of the most important distinctions in the editor.&lt;/p&gt;

&lt;p&gt;Visually covering information is not necessarily redacting it.&lt;/p&gt;

&lt;p&gt;If sensitive text remains inside the PDF and a black rectangle is simply drawn over it, the underlying information may still exist.&lt;/p&gt;

&lt;p&gt;Depending on how the PDF is constructed, it might still be extractable, searchable or recoverable.&lt;/p&gt;

&lt;p&gt;A real redaction workflow therefore needs to treat redaction differently from ordinary drawing.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Select sensitive region
        ↓
Identify affected content
        ↓
Remove or irreversibly alter that content
        ↓
Apply the visible redaction appearance
        ↓
Generate and validate the resulting PDF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fundamentally different from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draw black rectangle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two may look identical on screen.&lt;/p&gt;

&lt;p&gt;From a document-security perspective, they are not remotely equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. OCR is another separate layer
&lt;/h2&gt;

&lt;p&gt;OCR creates a similar distinction.&lt;/p&gt;

&lt;p&gt;A scanned PDF may contain pages that are effectively just images.&lt;/p&gt;

&lt;p&gt;A human can read the text.&lt;/p&gt;

&lt;p&gt;The PDF cannot necessarily search or select it.&lt;/p&gt;

&lt;p&gt;OCR can identify characters in those images, but an editor then has to decide what to do with that information.&lt;/p&gt;

&lt;p&gt;Possible outcomes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;extracted text&lt;/li&gt;
&lt;li&gt;searchable text layers&lt;/li&gt;
&lt;li&gt;selectable text&lt;/li&gt;
&lt;li&gt;positioning data&lt;/li&gt;
&lt;li&gt;confidence information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "run OCR" is not really a single UI operation.&lt;/p&gt;

&lt;p&gt;It is a document-processing pipeline.&lt;/p&gt;

&lt;p&gt;That affects where the feature belongs architecturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Not everything belongs in the browser
&lt;/h2&gt;

&lt;p&gt;We wanted the editor to feel immediate, so interactive operations naturally belong close to the user.&lt;/p&gt;

&lt;p&gt;Things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rendering&lt;/li&gt;
&lt;li&gt;selection&lt;/li&gt;
&lt;li&gt;dragging&lt;/li&gt;
&lt;li&gt;resizing&lt;/li&gt;
&lt;li&gt;drawing&lt;/li&gt;
&lt;li&gt;annotations&lt;/li&gt;
&lt;li&gt;signatures&lt;/li&gt;
&lt;li&gt;page thumbnails&lt;/li&gt;
&lt;li&gt;reordering&lt;/li&gt;
&lt;li&gt;zoom&lt;/li&gt;
&lt;li&gt;undo and redo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are well suited to browser-side interaction.&lt;/p&gt;

&lt;p&gt;But some operations have different requirements.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;OCR&lt;/li&gt;
&lt;li&gt;true redaction&lt;/li&gt;
&lt;li&gt;document sanitisation&lt;/li&gt;
&lt;li&gt;complex content removal&lt;/li&gt;
&lt;li&gt;PDF repair or normalisation&lt;/li&gt;
&lt;li&gt;final output validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;may require heavier or more controlled processing.&lt;/p&gt;

&lt;p&gt;For SwiftVecto, this led to a hybrid model.&lt;/p&gt;

&lt;p&gt;The browser owns the interactive editing experience.&lt;/p&gt;

&lt;p&gt;The processing layer handles operations where heavier processing or output correctness becomes more important.&lt;/p&gt;

&lt;p&gt;This also means privacy claims need to be precise.&lt;/p&gt;

&lt;p&gt;It would be misleading to say that every PDF operation always happens locally if some features legitimately require server-side processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Export is where everything has to agree
&lt;/h2&gt;

&lt;p&gt;An editor can look perfect in the browser and still produce a bad PDF.&lt;/p&gt;

&lt;p&gt;Export is where all the architectural decisions finally meet.&lt;/p&gt;

&lt;p&gt;Conceptually, our pipeline looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original PDF
      +
Editor state
      ↓
Apply page operations
      ↓
Apply content changes
      ↓
Apply annotations
      ↓
Apply images, text and signatures
      ↓
Apply forms
      ↓
Apply redactions
      ↓
Validate output
      ↓
Final PDF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the coordinate model is wrong, export exposes it.&lt;/p&gt;

&lt;p&gt;If page identity is wrong, export exposes it.&lt;/p&gt;

&lt;p&gt;If zoom has leaked into document coordinates, export exposes it.&lt;/p&gt;

&lt;p&gt;If reordered pages have lost their objects, export exposes it.&lt;/p&gt;

&lt;p&gt;This is why treating export as an afterthought is risky.&lt;/p&gt;

&lt;p&gt;The editor and export pipeline need to share the same understanding of the document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest lesson
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from building the editor has been that a PDF editor is not primarily a toolbar.&lt;/p&gt;

&lt;p&gt;The toolbar is simply the visible interface to a much larger document system.&lt;/p&gt;

&lt;p&gt;Behind buttons such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Text
Image
Sign
Draw
Shapes
Annotate
Link
Forms
Redact
Crop
OCR
Pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are several different engineering problems involving document state, coordinate transformation, page identity, rendering, processing and export.&lt;/p&gt;

&lt;p&gt;Once those concerns are separated properly, adding features becomes much more manageable.&lt;/p&gt;

&lt;p&gt;Without those boundaries, every new feature risks becoming another special case.&lt;/p&gt;

&lt;p&gt;We wrote a more detailed breakdown of the architecture and the problems we encountered in the original SwiftVecto guide:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swiftvecto.com/guides/building-a-browser-based-pdf-editor" rel="noopener noreferrer"&gt;https://swiftvecto.com/guides/building-a-browser-based-pdf-editor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser-based editor discussed here is also available to try on SwiftVecto:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swiftvecto.com/tool/edit-pdf" rel="noopener noreferrer"&gt;https://swiftvecto.com/tool/edit-pdf&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>pdf</category>
      <category>programming</category>
    </item>
    <item>
      <title>Formatting Laravel Blade Templates: Why It Is Harder Than Formatting HTML</title>
      <dc:creator>Michael Oteng</dc:creator>
      <pubDate>Sat, 12 Sep 2026 14:34:11 +0000</pubDate>
      <link>https://dev.to/swiftvecto/formatting-laravel-blade-templates-why-it-is-harder-than-formatting-html-5h4h</link>
      <guid>https://dev.to/swiftvecto/formatting-laravel-blade-templates-why-it-is-harder-than-formatting-html-5h4h</guid>
      <description>&lt;p&gt;Laravel Blade templates can look deceptively simple.&lt;/p&gt;

&lt;p&gt;At first glance, a &lt;code&gt;.blade.php&lt;/code&gt; file is mostly HTML. That can make formatting one seem like the same problem as formatting an ordinary HTML document.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;A Blade template can combine HTML with conditional directives, loops, components, slots, PHP expressions and Laravel-specific syntax, sometimes within the same few lines.&lt;/p&gt;

&lt;p&gt;Consider a deliberately badly formatted example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="user-card"&amp;gt;
@if($user)
@if($user-&amp;gt;active)
&amp;lt;h2&amp;gt;{{ $user-&amp;gt;name }}&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;{{ $user-&amp;gt;email }}&amp;lt;/p&amp;gt;
@if($user-&amp;gt;isAdmin())
&amp;lt;span class="badge"&amp;gt;Administrator&amp;lt;/span&amp;gt;
@elseif($user-&amp;gt;isEditor())
&amp;lt;span class="badge"&amp;gt;Editor&amp;lt;/span&amp;gt;
@else
&amp;lt;span class="badge"&amp;gt;User&amp;lt;/span&amp;gt;
@endif
@else
&amp;lt;p&amp;gt;This account is inactive.&amp;lt;/p&amp;gt;
@endif
@else
&amp;lt;p&amp;gt;No user is available.&amp;lt;/p&amp;gt;
@endif
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTML itself is not particularly complicated. The difficulty comes from understanding the structure created by the Blade directives around it.&lt;/p&gt;

&lt;p&gt;A formatter needs to recognise that &lt;code&gt;@if&lt;/code&gt;, &lt;code&gt;@elseif&lt;/code&gt;, &lt;code&gt;@else&lt;/code&gt; and &lt;code&gt;@endif&lt;/code&gt; are related control structures. It needs to understand how the HTML is nested inside those structures while also preserving Blade expressions such as &lt;code&gt;{{ $user-&amp;gt;name }}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Simply treating the file as HTML is therefore not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Blade-aware formatter needs to understand
&lt;/h2&gt;

&lt;p&gt;Formatting HTML is largely concerned with the hierarchy of elements.&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;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Hello&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The opening and closing tags provide most of the information needed to understand the structure.&lt;/p&gt;

&lt;p&gt;Blade introduces another structural layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@if ($user)
    &amp;lt;div&amp;gt;
        @foreach ($user-&amp;gt;notifications as $notification)
            &amp;lt;p&amp;gt;{{ $notification-&amp;gt;message }}&amp;lt;/p&amp;gt;
        @endforeach
    &amp;lt;/div&amp;gt;
@endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are now at least three things a formatter needs to reason about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The HTML structure.&lt;/li&gt;
&lt;li&gt;The Blade directive structure.&lt;/li&gt;
&lt;li&gt;The expressions embedded within the template.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real templates can become considerably more complicated when components, slots, conditional attributes, sections and other Blade features are introduced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-layout&amp;gt;
    &amp;lt;x-slot:title&amp;gt;
        {{ $pageTitle }}
    &amp;lt;/x-slot:title&amp;gt;

    @if ($user-&amp;gt;isAdmin())
        &amp;lt;x-admin.dashboard :user="$user" /&amp;gt;
    @else
        &amp;lt;x-user.dashboard :user="$user" /&amp;gt;
    @endif
&amp;lt;/x-layout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A formatter that only understands HTML does not have a complete picture of that document.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should our badly formatted example become?
&lt;/h2&gt;

&lt;p&gt;Running the original example through a Blade-aware formatter produces a much clearer structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="user-card"&amp;gt;
    @if ($user)
        @if ($user-&amp;gt;active)
            &amp;lt;h2&amp;gt;{{ $user-&amp;gt;name }}&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;{{ $user-&amp;gt;email }}&amp;lt;/p&amp;gt;
            @if ($user-&amp;gt;isAdmin())
                &amp;lt;span class="badge"&amp;gt;Administrator&amp;lt;/span&amp;gt;
            @elseif ($user-&amp;gt;isEditor())
                &amp;lt;span class="badge"&amp;gt;Editor&amp;lt;/span&amp;gt;
            @else
                &amp;lt;span class="badge"&amp;gt;User&amp;lt;/span&amp;gt;
            @endif
        @else
            &amp;lt;p&amp;gt;This account is inactive.&amp;lt;/p&amp;gt;
        @endif
    @else
        &amp;lt;p&amp;gt;No user is available.&amp;lt;/p&amp;gt;
    @endif
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing particularly exciting happened to the application logic.&lt;/p&gt;

&lt;p&gt;That's the point.&lt;/p&gt;

&lt;p&gt;Formatting isn't supposed to redesign the template. It makes its existing structure easier for a human to see.&lt;/p&gt;

&lt;p&gt;The relationship between the outer user check, the account status check and the role check is now immediately apparent.&lt;/p&gt;

&lt;p&gt;There is also a smaller change that is easy to overlook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@if($user)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@if ($user)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consistent spacing and indentation might seem cosmetic, but across a large collection of views they reduce unnecessary variation and make templates easier to scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting is not linting
&lt;/h2&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;A formatter can make code consistent without determining whether that code represents a good implementation.&lt;/p&gt;

&lt;p&gt;For example, this can be beautifully formatted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@if ($conditionOne)
    @if ($conditionTwo)
        @if ($conditionThree)
            @if ($conditionFour)
                ...
            @endif
        @endif
    @endif
@endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But formatting does not answer whether four levels of nested conditions belong in the template.&lt;/p&gt;

&lt;p&gt;Perhaps some of that logic should move elsewhere. Perhaps a Blade component would make the view clearer. Perhaps the underlying design needs reconsideration.&lt;/p&gt;

&lt;p&gt;Similarly, a formatter cannot necessarily tell you that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a route name does not exist;&lt;/li&gt;
&lt;li&gt;a variable was never passed to the view;&lt;/li&gt;
&lt;li&gt;a component is missing;&lt;/li&gt;
&lt;li&gt;an included view does not exist;&lt;/li&gt;
&lt;li&gt;the business logic is incorrect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Formatting improves presentation. It should not be confused with validation, static analysis, testing or application design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blade formatting options
&lt;/h2&gt;

&lt;p&gt;There are several ways to approach Blade formatting.&lt;/p&gt;

&lt;p&gt;Editor extensions can format templates as part of the development workflow. Blade-aware Prettier tooling can provide automated formatting, and dedicated Blade formatter projects are available for developers who want formatting integrated into their local environment.&lt;/p&gt;

&lt;p&gt;Laravel Pint also supports Blade formatting.&lt;/p&gt;

&lt;p&gt;Blade formatting is not enabled by default in Pint, but it can be invoked with the &lt;code&gt;--blade&lt;/code&gt; option or configured using the &lt;code&gt;Pint/laravel_blade&lt;/code&gt; rule.&lt;/p&gt;

&lt;p&gt;For a Laravel project where Pint is already part of the development workflow, that can be a natural place to enforce formatting consistently.&lt;/p&gt;

&lt;p&gt;There are also situations where you may not want to configure anything locally.&lt;/p&gt;

&lt;p&gt;Perhaps you have been sent a Blade fragment.&lt;/p&gt;

&lt;p&gt;Perhaps you are reviewing an example outside the original project.&lt;/p&gt;

&lt;p&gt;Perhaps you are working on a machine without the project's development environment.&lt;/p&gt;

&lt;p&gt;Or perhaps you simply want to paste a template somewhere, format it and copy the result.&lt;/p&gt;

&lt;p&gt;That is where a browser-based formatter can be useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a browser-based Blade formatter
&lt;/h2&gt;

&lt;p&gt;While developing SwiftVecto, I wanted the Laravel Blade Formatter to work without requiring the user to upload a Blade file or configure a Laravel project.&lt;/p&gt;

&lt;p&gt;The formatter therefore operates directly in the browser using Blade-aware formatting tooling.&lt;/p&gt;

&lt;p&gt;The basic workflow is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Blade source
     ↓
Blade-aware formatter
     ↓
Formatted template
     ↓
Copy result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool also analyses the source separately to provide information such as directive counts, echo expressions, components, slots, loops, conditionals, sections and approximate directive nesting depth.&lt;/p&gt;

&lt;p&gt;Those statistics aren't part of formatting itself. They are simply another way of getting a quick picture of the structure of a template.&lt;/p&gt;

&lt;p&gt;The important architectural distinction is that normal interactive formatting is performed in the browser rather than sending the Blade source to the server for processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating reusable formatting fixtures
&lt;/h2&gt;

&lt;p&gt;While testing formatting behaviour, another useful idea emerged.&lt;/p&gt;

&lt;p&gt;Instead of keeping test examples buried inside development notes, why not make them reusable?&lt;/p&gt;

&lt;p&gt;I created a public SwiftVecto repository for Blade formatting examples and fixtures.&lt;/p&gt;

&lt;p&gt;A simple fixture structure can contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixtures/
├── unformatted/
│   └── conditionals.blade.php
└── expected/
    └── conditionals.blade.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unformatted version provides a reproducible input.&lt;/p&gt;

&lt;p&gt;The expected version provides a reference result.&lt;/p&gt;

&lt;p&gt;That makes it easier to experiment with different formatting workflows and, as the fixture collection grows, test more interesting Blade structures.&lt;/p&gt;

&lt;p&gt;The expected output should not necessarily be treated as the one canonical way every Blade template must look. Formatter versions, configuration and formatting rules can legitimately produce different results.&lt;/p&gt;

&lt;p&gt;The important part is having reproducible examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the example yourself
&lt;/h2&gt;

&lt;p&gt;The example used in this article is available as part of the public Blade formatting fixtures:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/SwiftVecto/blade-formatting-examples" rel="noopener noreferrer"&gt;View the Blade formatting examples on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also paste the unformatted example directly into the browser-based formatter:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swiftvecto.com/tool/laravel-blade-formatter" rel="noopener noreferrer"&gt;Try the SwiftVecto Laravel Blade Formatter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No Laravel project is required to experiment with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Formatting will not make a Blade template well designed.&lt;/p&gt;

&lt;p&gt;It will not fix application logic, replace testing or tell you whether a view has become too complicated.&lt;/p&gt;

&lt;p&gt;What it can do is remove unnecessary visual inconsistency and make the structure that already exists considerably easier to reason about.&lt;/p&gt;

&lt;p&gt;And with Blade, that structure involves more than HTML.&lt;/p&gt;

&lt;p&gt;As the public fixture collection grows, I plan to add examples covering components, slots, loops, layouts, forms, convenience directives and some of the more awkward structures that can appear in real Blade templates.&lt;/p&gt;

&lt;p&gt;If you have an interesting Blade formatting edge case, I'd be interested to see it.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
