<?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: Ahmad Faraz</title>
    <description>The latest articles on DEV Community by Ahmad Faraz (@ahmadfarazcrypto).</description>
    <link>https://dev.to/ahmadfarazcrypto</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%2F894944%2F35ea0cb4-12d9-445e-ac33-7b0124401169.jpeg</url>
      <title>DEV Community: Ahmad Faraz</title>
      <link>https://dev.to/ahmadfarazcrypto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmadfarazcrypto"/>
    <language>en</language>
    <item>
      <title>How to Replace a Page in a PDF Programmatically with pdf-lib (No Backend Required)</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Thu, 04 Jun 2026 09:01:30 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/how-to-replace-a-page-in-a-pdf-programmatically-with-pdf-lib-no-backend-required-42fa</link>
      <guid>https://dev.to/ahmadfarazcrypto/how-to-replace-a-page-in-a-pdf-programmatically-with-pdf-lib-no-backend-required-42fa</guid>
      <description>&lt;p&gt;A lawyer friend messaged me at 11 PM last week. He'd sent a 47-page contract to a client. Client signed it, sent it back. Everything looked good until my friend noticed page 12 had a typo in the payment terms — the kind of typo that, if a court ever looked at it, could mean the client owed nothing.&lt;/p&gt;

&lt;p&gt;"Do I have to make him resign the whole thing? It took us three weeks to get this signed."&lt;/p&gt;

&lt;p&gt;Technically no. You just replace page 12 of the signed PDF. The signatures on the other pages stay intact. Five seconds of pdf-lib code.&lt;/p&gt;

&lt;p&gt;This post is the technical writeup. By the end you'll have working browser-side JavaScript that replaces any single page in a PDF with a page from another PDF, no server roundtrip, no system dependencies, no Ghostscript install. Plus the gotchas that aren't obvious until you ship and someone reports a bug.&lt;/p&gt;

&lt;p&gt;If you just want the no-code version: &lt;a href="https://convertkr.com/replace-pdf-page/" rel="noopener noreferrer"&gt;we built a free tool for it&lt;/a&gt; — upload original, pick page, upload replacement, download. Otherwise, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is harder than &lt;code&gt;setPage(12, newPage)&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;PDFs aren't structured the way most developers expect. There is no array of pages you index into. The page tree is a tree (literally — it can be nested), pages reference resources held in a separate dictionary, fonts and images are deduplicated across pages, and cross-references between pages (links, bookmarks, form fields, named destinations) are stored separately from the page content.&lt;/p&gt;

&lt;p&gt;When you "replace page 12," you have to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the original document into an in-memory representation&lt;/li&gt;
&lt;li&gt;Load the replacement document&lt;/li&gt;
&lt;li&gt;Copy the replacement page's content + all its referenced resources (fonts, images, embedded files) into the original document's resource pool&lt;/li&gt;
&lt;li&gt;Insert the copied page into the original's page tree at index 11 (zero-indexed)&lt;/li&gt;
&lt;li&gt;Remove the original page 12 (now at index 12 because of step 4) from the page tree&lt;/li&gt;
&lt;li&gt;Re-serialize the entire document, updating the cross-reference table&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you skip step 3, the new page will appear blank because its fonts and images are still pointing at the wrong document. If you skip step 5, you'll end up with 48 pages instead of 47. If you skip step 6, the cross-references will be wrong and Adobe Reader will complain on open.&lt;/p&gt;

&lt;p&gt;The good news: &lt;code&gt;pdf-lib&lt;/code&gt; does all of this for you if you call the right methods in the right order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pdf-lib version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;replacePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replacementBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Load both documents&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;replacementDoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replacementBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Copy the replacement's first page into the original document&lt;/span&gt;
  &lt;span class="c1"&gt;// copyPages handles fonts, images, and all referenced resources&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;copiedPage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replacementDoc&lt;/span&gt;&lt;span class="p"&gt;,&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="c1"&gt;// 3. Insert the copied page at the target position&lt;/span&gt;
  &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;copiedPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Remove the old page (now shifted one position later)&lt;/span&gt;
  &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Serialize back to a Uint8Array&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;Usage from the browser:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;original&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;files&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;replacementFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replacement&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;files&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;originalFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;replacementBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;replacementFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// pageNumber is 1-indexed (user-facing), pageIndex is 0-indexed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;replacePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replacementBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageNumber&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Trigger download&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;resultBytes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replaced.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's the entire implementation. About 30 lines including the UI glue. Runs in the browser. No server.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;This is the section I always wish API docs included.&lt;/p&gt;
&lt;h3&gt;
  
  
  Gotcha 1: &lt;code&gt;copyPages&lt;/code&gt; is async, and people forget the &lt;code&gt;await&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG — silently fails&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replacementDoc&lt;/span&gt;&lt;span class="p"&gt;,&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="c1"&gt;// RIGHT&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replacementDoc&lt;/span&gt;&lt;span class="p"&gt;,&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;copyPages&lt;/code&gt; returns a Promise that resolves to an array of &lt;code&gt;PDFPage&lt;/code&gt; objects. Forget the &lt;code&gt;await&lt;/code&gt; and you'll try to &lt;code&gt;insertPage&lt;/code&gt; with a Promise object instead of a page object — pdf-lib will throw a confusing error about an invalid page type.&lt;/p&gt;
&lt;h3&gt;
  
  
  Gotcha 2: User-facing page numbers vs PDF page indices
&lt;/h3&gt;

&lt;p&gt;Almost every bug report I've ever seen on a PDF tool comes from this. Users think in 1-indexed numbers ("page 1, page 2, page 3"). PDF page arrays are 0-indexed ("index 0, index 1, index 2").&lt;/p&gt;

&lt;p&gt;Worse, the page numbers PRINTED on a document body often differ from the PDF's actual page numbers. A 50-page report with a cover page, TOC, and executive summary might print "Page 1" on what is PDF page 4.&lt;/p&gt;

&lt;p&gt;Your UI needs to be crystal clear about which number you're asking for. We label our input field "PDF page number (count from the very first page, including covers)". Users still occasionally get it wrong but at least the documentation is clean.&lt;/p&gt;
&lt;h3&gt;
  
  
  Gotcha 3: Form fields disappear when their page is removed
&lt;/h3&gt;

&lt;p&gt;PDFs can have interactive form fields (text inputs, checkboxes, signatures). These fields are stored in the document's AcroForm dictionary AND referenced from the pages they appear on.&lt;/p&gt;

&lt;p&gt;When you remove a page that had form fields, the page reference is gone but the AcroForm dictionary still has orphan field references. Some readers tolerate this. Adobe Reader sometimes shows a "this form is malformed" warning. pdf-lib doesn't currently clean up orphaned form references automatically — you have to do it yourself if your input PDFs have forms.&lt;/p&gt;

&lt;p&gt;Quick check before warning your users:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getForm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFields&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageToRemove&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fieldsOnThisPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acroField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWidgets&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;P&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;pageToRemove&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fieldsOnThisPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Warn user that their replacement will lose form fields&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Gotcha 4: Digital signatures invalidate the entire document
&lt;/h3&gt;

&lt;p&gt;If the original PDF was digitally SIGNED (not just visually — actually cryptographically signed with a &lt;code&gt;Sig&lt;/code&gt; field), modifying any byte of the document invalidates the signature. The replaced page works fine, the rest of the document opens fine, but the signature panel in Adobe Reader will show "Signature is invalid — document has been modified after signing."&lt;/p&gt;

&lt;p&gt;This is by design — that's literally what digital signatures are for. There is no way around it with pdf-lib (or any other PDF library, because circumventing this would defeat the purpose of digital signatures).&lt;/p&gt;

&lt;p&gt;What you CAN do is detect this case before replacing and warn the user:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signatures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getForm&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFields&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PDFSignature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signatures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Warn: replacing a page will invalidate digital signatures&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note: this is for &lt;em&gt;cryptographic&lt;/em&gt; digital signatures, not signature images. If someone just pasted an image of their signature onto page 5, replacing page 12 is fine and the image stays untouched.&lt;/p&gt;
&lt;h3&gt;
  
  
  Gotcha 5: Page sizes don't have to match, but the result might look odd
&lt;/h3&gt;

&lt;p&gt;If the original PDF is Letter (8.5 × 11") and the replacement page is A4 (8.27 × 11.69"), the inserted page will appear at A4 dimensions inside the otherwise-Letter document. PDF readers handle this fine — they just show that one page at a different size. But it might look unprofessional.&lt;/p&gt;

&lt;p&gt;If you want to normalize the size, you can scale the replacement to match:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the page being replaced&lt;/span&gt;
&lt;span class="kd"&gt;const&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="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;copiedPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSize&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="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Be aware this scales the content too — text might look squashed if the aspect ratios differ.&lt;/p&gt;
&lt;h3&gt;
  
  
  Gotcha 6: Outlines/bookmarks pointing to the replaced page
&lt;/h3&gt;

&lt;p&gt;PDF outlines (the sidebar tree in Adobe Reader) point to specific destinations — usually a (page, x, y, zoom) tuple. If you replace a page, outlines that pointed TO that page still point to the same coordinates on the new page, which may not be a sensible location anymore.&lt;/p&gt;

&lt;p&gt;pdf-lib's outline handling is limited. For most use cases you can leave outlines as-is and the user-visible result is "the bookmark goes to a slightly weird spot on the new page." If outlines are critical to your app, you'll need to walk the outline tree and fix up destinations manually.&lt;/p&gt;
&lt;h2&gt;
  
  
  Doing it without &lt;code&gt;pdf-lib&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you don't want to ship &lt;code&gt;pdf-lib&lt;/code&gt; (it's ~700KB minified), there are a few alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDF.js&lt;/strong&gt; (Mozilla's library) — read-only. Can render pages but not modify or save them. Wrong tool for this job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HummusJS / PDFKit-py / PyPDF2&lt;/strong&gt; — server-side. Requires a backend. If you have a backend, you have other options too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll your own PDF parser&lt;/strong&gt; — possible but you will lose months. PDFs have spec edge cases that took the pdf-lib team years to handle correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a service like our API&lt;/strong&gt; — outsource the bytes to someone else's pdf-lib. Same operation, just over HTTP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For browser-side without a backend, pdf-lib is the only realistic option I've found. The bundle size is fine for a tool page where the user explicitly came to do PDF work — bigger problem if you're integrating this into a 50KB landing page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Performance notes
&lt;/h2&gt;

&lt;p&gt;For typical document sizes (under 50 pages, files under 10MB):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page replace operation: 50-200ms&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;save()&lt;/code&gt; call at the end is the bottleneck — re-serializing the whole document with updated cross-references&lt;/li&gt;
&lt;li&gt;Memory: roughly 2-3x the file size in memory during operation (original + replacement + intermediate state)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For larger documents (200+ pages, 50MB+ files), things get slow. pdf-lib doesn't stream — the whole document is parsed into memory and the whole result is serialized at once. For 500MB files, you'll hit browser memory limits.&lt;/p&gt;

&lt;p&gt;If you regularly handle large PDFs and need to do many small operations, server-side with a streaming PDF library is the right answer.&lt;/p&gt;
&lt;h2&gt;
  
  
  The free tool version
&lt;/h2&gt;

&lt;p&gt;If you don't want to ship pdf-lib in your own app, or your users aren't developers, we built this as a free in-browser tool: &lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://convertkr.com/replace-pdf-page/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fconvertkr.com%2Fwp-content%2Fthemes%2Fpdf-editor-theme%2Fassets%2Fog-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://convertkr.com/replace-pdf-page/" rel="noopener noreferrer" class="c-link"&gt;
            Replace PDF Pages Online Free – Swap Pages Easily | ConvertKr
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Replace any page in a PDF with a page from another document. Preview, select, and download. Free, no signup.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fconvertkr.com%2Fwp-content%2Fthemes%2Fpdf-editor-theme%2Fassets%2Ffavicon.svg" width="64" height="64"&gt;
          convertkr.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Upload the original PDF, pick the page number, upload the replacement, download. Same pdf-lib engine under the hood, no install required. Files never upload to a server — everything runs in the browser.&lt;/p&gt;

&lt;p&gt;I use it myself when I receive contracts back with typos on non-signature pages. Same workflow as my lawyer friend.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I'd love feedback on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has anyone built page-replacement in production with pdf-lib at scale? Curious about memory pressure and any wrappers you wrote for the orphan-form-field problem.&lt;/li&gt;
&lt;li&gt;Anyone using a different browser PDF library I should know about? I keep meaning to evaluate &lt;code&gt;mupdf-js&lt;/code&gt; (the Emscripten port of MuPDF) but haven't found time.&lt;/li&gt;
&lt;li&gt;For the form-field gotcha — is there a cleaner way to detect orphaned references than the widget-walking approach in this post?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code in this post is MIT — &lt;a href="https://gist.github.com/AhmadFaraz-crypto/ea740d45b69ba614180874262b4fda68" rel="noopener noreferrer"&gt;fork it on GitHub&lt;/a&gt;, ship it, change it. If you build something useful with it, share back.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What I Learned After My Website Disappeared From Google's First Page</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Wed, 03 Jun 2026 10:08:32 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/what-i-learned-after-my-website-disappeared-from-googles-first-page-10pc</link>
      <guid>https://dev.to/ahmadfarazcrypto/what-i-learned-after-my-website-disappeared-from-googles-first-page-10pc</guid>
      <description>&lt;p&gt;When I launched &lt;a href="https://convertkr.com/" rel="noopener noreferrer"&gt;ConvertKR&lt;/a&gt;, I thought SEO was mostly about meta tags, titles, keywords, and writing blog posts.&lt;/p&gt;

&lt;p&gt;As a Senior Frontend Engineer, SEO was not my area of expertise. Like many developers, I focused on building products and writing code. But after launching my website, I realized that getting traffic from Google was a completely different challenge.&lt;/p&gt;

&lt;p&gt;So I started learning.&lt;/p&gt;

&lt;p&gt;I spent weeks reading articles about SEO fundamentals, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meta titles and descriptions&lt;/li&gt;
&lt;li&gt;Heading structure&lt;/li&gt;
&lt;li&gt;Sitemap generation&lt;/li&gt;
&lt;li&gt;Robots.txt&lt;/li&gt;
&lt;li&gt;Internal linking&lt;/li&gt;
&lt;li&gt;Page speed optimization&lt;/li&gt;
&lt;li&gt;Content creation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I implemented everything I learned on my website and started publishing blog posts regularly.&lt;/p&gt;

&lt;p&gt;To my surprise, the results came quickly.&lt;/p&gt;

&lt;p&gt;Within the first couple of months, several of my tools started appearing on Google's first page. Seeing my website rank for search queries felt amazing. It was proof that all the effort was paying off.&lt;/p&gt;

&lt;p&gt;Then something unexpected happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rankings Suddenly Dropped
&lt;/h2&gt;

&lt;p&gt;After about two months, I noticed that my pages were no longer showing on the first page for many keywords.&lt;/p&gt;

&lt;p&gt;Traffic slowed down.&lt;/p&gt;

&lt;p&gt;Impressions decreased.&lt;/p&gt;

&lt;p&gt;Some of the rankings I was excited about simply disappeared.&lt;/p&gt;

&lt;p&gt;As someone who had invested a lot of time into learning SEO, this was frustrating.&lt;/p&gt;

&lt;p&gt;Naturally, I started researching again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Google Honeymoon" Effect
&lt;/h2&gt;

&lt;p&gt;I spent hours reading SEO forums, Reddit discussions, and blog posts.&lt;/p&gt;

&lt;p&gt;One explanation kept appearing repeatedly: the "Google Honeymoon" effect.&lt;/p&gt;

&lt;p&gt;Many SEO professionals believe that Google sometimes gives new websites temporary visibility to evaluate user engagement and content quality. After collecting enough data, rankings may fluctuate before settling into more stable positions.&lt;/p&gt;

&lt;p&gt;Whether this is an official Google mechanism or not, the pattern sounded very similar to what I was experiencing.&lt;/p&gt;

&lt;p&gt;At first, I thought something was technically wrong with my website.&lt;/p&gt;

&lt;p&gt;So I reviewed everything again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meta tags&lt;/li&gt;
&lt;li&gt;Technical SEO&lt;/li&gt;
&lt;li&gt;Site performance&lt;/li&gt;
&lt;li&gt;Content quality&lt;/li&gt;
&lt;li&gt;Indexing status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything looked fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing Piece: Authority
&lt;/h2&gt;

&lt;p&gt;That's when I realized something important.&lt;/p&gt;

&lt;p&gt;Good SEO is not only about optimizing your website.&lt;/p&gt;

&lt;p&gt;It's also about building authority.&lt;/p&gt;

&lt;p&gt;You can have excellent technical SEO, but if Google doesn't view your website as a trusted source, ranking consistently becomes much harder.&lt;/p&gt;

&lt;p&gt;So instead of endlessly tweaking meta tags, I shifted my focus toward authority building.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Started Doing
&lt;/h2&gt;

&lt;p&gt;I began creating content outside of my website:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publishing articles on developer communities&lt;/li&gt;
&lt;li&gt;Creating YouTube videos&lt;/li&gt;
&lt;li&gt;Sharing my projects publicly&lt;/li&gt;
&lt;li&gt;Participating in discussions&lt;/li&gt;
&lt;li&gt;Building backlinks naturally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn't to trick search engines.&lt;/p&gt;

&lt;p&gt;The goal was to demonstrate that real people were talking about and referencing my work.&lt;/p&gt;

&lt;p&gt;Over time, I started seeing positive signals again.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small Win That Made My Day
&lt;/h2&gt;

&lt;p&gt;This morning, I searched for the keyword "QR Remover."&lt;/p&gt;

&lt;p&gt;And there it was.&lt;/p&gt;

&lt;p&gt;My website appeared near the top of the search results again.&lt;/p&gt;

&lt;p&gt;It might seem like a small achievement, but for anyone who has spent months building a website, optimizing content, and learning SEO from scratch, it's a very rewarding feeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Biggest Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're a developer launching your first website, don't obsess over meta tags forever.&lt;/p&gt;

&lt;p&gt;Technical SEO is important, but it's only part of the equation.&lt;/p&gt;

&lt;p&gt;Focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating genuinely useful tools&lt;/li&gt;
&lt;li&gt;Publishing quality content&lt;/li&gt;
&lt;li&gt;Building authority&lt;/li&gt;
&lt;li&gt;Earning backlinks naturally&lt;/li&gt;
&lt;li&gt;Being patient&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SEO is rarely a straight line.&lt;/p&gt;

&lt;p&gt;Sometimes rankings go up.&lt;/p&gt;

&lt;p&gt;Sometimes they go down.&lt;/p&gt;

&lt;p&gt;But if your product provides value and you continue building authority, the results can come back stronger than before.&lt;/p&gt;

&lt;p&gt;Have you experienced a similar ranking drop with a new website? I'd love to hear your story in the comments.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>seo</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>How Stock Photo Watermarks Actually Work — Recreating the Getty Preview Pattern in JavaScript</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Wed, 03 Jun 2026 08:34:43 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/the-getty-images-watermark-is-iconic-heres-how-to-recreate-it-in-javascript-3o4g</link>
      <guid>https://dev.to/ahmadfarazcrypto/the-getty-images-watermark-is-iconic-heres-how-to-recreate-it-in-javascript-3o4g</guid>
      <description>&lt;p&gt;You've seen the watermark. It's not the press-credit bar at the bottom of news photos — that's a different thing. I mean the preview watermark stock photo sites use on unpurchased images: "Getty Images" or "Shutterstock" repeated diagonally across the entire photo, faded to maybe 40% opacity, rotated at roughly -30 degrees, impossible to crop out.&lt;/p&gt;

&lt;p&gt;That diagonal tile pattern is one of the most effective anti-piracy designs ever shipped on the web. You can't crop it out (covers the whole image). You can't clone-stamp it out (the text is everywhere, no clean background to sample). You can barely even read what's underneath, but you can read it enough to decide whether to buy.&lt;/p&gt;

&lt;p&gt;A friend of mine needed to add this exact style to a batch of unreleased product photos he was sending to a client for review (the client had stolen design concepts before — he wanted "watermarked preview" versions until payment cleared). I'd never built one. So I spent a Saturday reverse-engineering it.&lt;/p&gt;

&lt;p&gt;This post is the technical writeup. By the end you'll have working Canvas code that produces a near-perfect Getty/Shutterstock-style diagonal tile watermark on any image, including the spacing bug that wrecked my first version.&lt;/p&gt;

&lt;p&gt;If you just want the tool without the code: &lt;a href="https://convertkr.com/getty-images-watermark/" rel="noopener noreferrer"&gt;we built one here&lt;/a&gt;. Drag in any photo, type your watermark text or upload a logo, get the watermarked image back. Runs entirely in your browser — files never upload. Otherwise, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes the diagonal tile watermark anti-piracy
&lt;/h2&gt;

&lt;p&gt;Before writing any code, it's worth understanding why this design is the standard. Other watermark styles fail for specific reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corner watermark&lt;/strong&gt; — crop it off and the photo is clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom bar&lt;/strong&gt; — same problem, plus easy to clone-stamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single centered watermark&lt;/strong&gt; — opaque enough to be useful blocks the photo, transparent enough to see through is easy to remove with content-aware fill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagonal tile&lt;/strong&gt; — covers the entire image with repeating text, so cropping leaves more text behind, and content-aware fill has nothing clean to sample from. The text is everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why Getty, Shutterstock, Adobe Stock, iStock, Alamy, and every other stock site converged on the same pattern. It's not aesthetic — it's defensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four design rules
&lt;/h2&gt;

&lt;p&gt;Studying real Getty and Shutterstock previews, four rules make the watermark look authentic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rotation between -25° and -45°.&lt;/strong&gt; Getty uses about -30°, Shutterstock about -45°. Horizontal text reads too cleanly and is easier to remove. Vertical is awkward. Diagonal forces the eye to fight the tilt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opacity around 35-45%.&lt;/strong&gt; Below 30%, content-aware fill removes it. Above 50%, you can't read the photo. The sweet spot is enough to mark but not enough to obstruct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tile spacing roughly equal to text width + a small gap.&lt;/strong&gt; Too tight and the text overlaps itself. Too loose and there are obvious gaps to crop into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color: white or near-white.&lt;/strong&gt; Dark watermarks disappear into dark photos and become camouflage. White is universally legible against most backgrounds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Get any of these wrong and the watermark either fails as protection (too easy to remove) or fails as a preview (too hard to see the photo). The proportions matter more than people realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first naive implementation
&lt;/h2&gt;

&lt;p&gt;Here's the simplest possible version. Loop across the image, draw rotated text at each position, done:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;watermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bold 48px Arial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textAlign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;middle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Draw text at every grid position, each rotated -30°&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spacing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&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;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;canvas&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="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&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;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;canvas&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="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// -30 degrees&lt;/span&gt;
          &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&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;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&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;This works, sort of. Run it and you'll immediately see two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The corners of the image are bare.&lt;/strong&gt; Rotating each tile individually leaves diagonal seams of empty space along the image edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tile positions don't line up.&lt;/strong&gt; Because each tile rotates independently around its own origin, the visual pattern feels chaotic instead of grid-aligned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is one of those mental flips that's obvious once you see it: don't rotate each tile. Rotate the entire coordinate system once, then draw tiles in straight rows and columns inside the rotated frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotate the grid, not the tiles
&lt;/h2&gt;

&lt;p&gt;Here's the technique. We translate the canvas origin to the center, rotate by -30°, then draw a regular axis-aligned grid in that rotated coordinate system. From the user's perspective, the grid appears tilted because the coordinate system is tilted.&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvasWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvasHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// no per-tile rotation&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&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;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The loop iterates from -bound to +bound&lt;/strong&gt;, not 0 to canvas size. Because we translated to the center, coordinates are now centered on (0, 0), so we need to cover both negative and positive directions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-tile rotation.&lt;/strong&gt; The tiles are drawn flat in the rotated frame. The canvas does the rotation work for us.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there's a new problem: what's &lt;code&gt;bound&lt;/code&gt;? The image is &lt;code&gt;canvasWidth × canvasHeight&lt;/code&gt;, but after rotation by -30°, the corners of the original image sit at coordinates that are further from the center than the image dimensions suggest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagonal calculation
&lt;/h2&gt;

&lt;p&gt;When you rotate a rectangle, the bounding box of the rotated shape is larger than the unrotated rectangle. To make sure tiles cover the entire image even at the rotated corners, you need to tile out to at least half the image's diagonal length.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvasWidth&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;canvasHeight&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is conservative — it covers a circle inscribing the rectangle, which is more than the rotated rectangle actually needs. But it's simpler than computing the exact rotated bounding box, and the extra tiles outside the canvas get clipped automatically when you draw to the canvas. Zero-cost insurance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spacing trap
&lt;/h2&gt;

&lt;p&gt;Here's the bug I shipped in version one and only caught in testing: my tool let users adjust the tile spacing with a slider. Default was 150px. Worked great with short text like "Getty" at 48px font.&lt;/p&gt;

&lt;p&gt;Then I tested with longer text — "ConvertKr Photography Studio © 2026" at the same font. The tiles overlapped catastrophically. Adjacent tiles drew on top of each other because the rendered text was ~600px wide while spacing was 150px.&lt;/p&gt;

&lt;p&gt;The fix is to derive a minimum spacing from the actual rendered watermark dimensions, separately for horizontal and vertical axes:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measureText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metrics&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// approximation — works for most fonts&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spacingX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userSpacing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;textWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spacingY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userSpacing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;textHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why separate X and Y? Because text is much wider than it is tall. If you use one spacing value for both axes, you either get gaps that are too big vertically (when sized for horizontal) or text that overlaps horizontally (when sized for vertical). Separate values give you tight vertical packing (rows close together) and comfortable horizontal spacing (text doesn't run into itself). That's the actual Getty/Shutterstock look.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;30&lt;/code&gt; and &lt;code&gt;15&lt;/code&gt; are gap padding. Tweak to taste — &lt;code&gt;30&lt;/code&gt; horizontal and &lt;code&gt;15&lt;/code&gt; vertical produces output that closely matches real stock previews.&lt;/p&gt;

&lt;h2&gt;
  
  
  The polished version
&lt;/h2&gt;

&lt;p&gt;Putting it all together. &lt;a href="https://gist.github.com/AhmadFaraz-crypto/9bde2b613d3e66d14406743c071f05dd" rel="noopener noreferrer"&gt;Full file as a Gist on GitHub →&lt;/a&gt;&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addStockWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sample&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="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;fontFamily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Arial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&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="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// degrees&lt;/span&gt;
    &lt;span class="nx"&gt;userSpacing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// minimum spacing in px&lt;/span&gt;
    &lt;span class="nx"&gt;paddingX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;paddingY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Set up font once so measureText returns accurate width&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`bold &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="s2"&gt;px &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textAlign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;middle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Compute non-overlapping spacing from actual text dimensions&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measureText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spacingX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userSpacing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;textWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;paddingX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spacingY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userSpacing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;textHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;paddingY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Rotate the grid, not the tiles&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Tile out to half the diagonal so rotated corners stay covered&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacingY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacingX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&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="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.92&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&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;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Image failed to load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&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;Usage:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addStockWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/photo.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR BRAND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the complete implementation. About 60 lines of JavaScript, no dependencies, runs entirely in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS gotcha
&lt;/h2&gt;

&lt;p&gt;If you're loading images from another domain, &lt;code&gt;crossOrigin = 'anonymous'&lt;/code&gt; requires the remote server to send &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; headers. Most don't. The clean workaround is to have users upload their own files using a file input — local file objects have no CORS restrictions:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watermarked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addStockWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PREVIEW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the approach we took for the tool itself. No backend involved — the file never leaves the user's device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance notes
&lt;/h2&gt;

&lt;p&gt;For a 4000×3000 image with default spacing, the tile loop draws roughly 150-200 text instances. Total render time on a mid-range laptop is 30-80ms — most of it is the &lt;code&gt;drawImage()&lt;/code&gt; for the source bitmap, not the watermark loop.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;toDataURL()&lt;/code&gt; call at the end is the slowest step (200-500ms for a 4K JPEG). If you need to display the result instantly and only save later, swap to &lt;code&gt;canvas.toBlob()&lt;/code&gt; which is async:&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// use url for preview, no parsing cost&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Image watermarks instead of text
&lt;/h2&gt;

&lt;p&gt;The same pattern works if you want to tile a logo PNG instead of text:&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="c1"&gt;// In place of fillText, use drawImage&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacingY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;spacingX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logoImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;logoW&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;logoH&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logoW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logoH&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;One detail: most user-uploaded logos arrive in black or full-color. For a stock-watermark style you usually want them tinted to a single color (typically white). The trick is &lt;code&gt;globalCompositeOperation = 'source-in'&lt;/code&gt; on an offscreen canvas:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;logoW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;logoH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;octx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;octx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logoImage&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logoW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logoH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;octx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalCompositeOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;octx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;octx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logoW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logoH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Now `off` is a white silhouette of the logo — use it like wmImage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This recolors every opaque pixel to white while preserving transparency.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on legality
&lt;/h2&gt;

&lt;p&gt;You can apply any watermark you want to your own photos. What you should NOT do is generate a "Getty Images" branded watermark on a photo and try to pass it off as being from Getty. That's brand impersonation and Getty's legal team is famously aggressive about it.&lt;/p&gt;

&lt;p&gt;Legitimate uses I've seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watermarking your own unreleased product photos for client preview&lt;/li&gt;
&lt;li&gt;Photography portfolios marking unpurchased proofs&lt;/li&gt;
&lt;li&gt;Educational content explaining how watermarks work&lt;/li&gt;
&lt;li&gt;Memes (half the internet's joke screenshots use a Getty-style overlay for comic effect)&lt;/li&gt;
&lt;li&gt;Your own stock photo business protecting previews on your site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't impersonate someone else's brand. Use your own name, business, or copyright notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free tool version
&lt;/h2&gt;

&lt;p&gt;If you don't want to ship 60 lines of Canvas code into your own project, we built this as a free in-browser tool: &lt;a href="https://convertkr.com/getty-images-watermark/" rel="noopener noreferrer"&gt;convertkr.com/getty-images-watermark&lt;/a&gt;. Drop in any image, type a watermark or upload a logo, get the result back. Files never upload anywhere — everything runs in your browser.&lt;/p&gt;

&lt;p&gt;Built during the Saturday that started this post. My friend uses it for client mockups now. A couple of meme-page admins on Twitter use it too, which I find weirdly satisfying.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I'd love to see in the comments:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have you reverse-engineered other iconic visual patterns? (The Wikipedia infobox, the Spotify gradient, the "as seen on" press bar)&lt;/li&gt;
&lt;li&gt;Cleaner approach to text dimensions than &lt;code&gt;fontSize * 1.2&lt;/code&gt; for height? Anyone done it via &lt;code&gt;actualBoundingBoxAscent&lt;/code&gt; + &lt;code&gt;actualBoundingBoxDescent&lt;/code&gt; and found gotchas?&lt;/li&gt;
&lt;li&gt;Anyone running tiled watermarks in production at scale? Curious about throughput per node and whether it's CPU or memory bound first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is MIT — &lt;a href="https://gist.github.com/AhmadFaraz-crypto/9bde2b613d3e66d14406743c071f05dd" rel="noopener noreferrer"&gt;fork it on GitHub&lt;/a&gt;, ship it, change it. If you build something interesting with it, share back.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>powerapps</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a PDF Conversion API So You Don't Have to Install Ghostscript</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Mon, 01 Jun 2026 13:51:40 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/i-built-a-pdf-conversion-api-so-you-dont-have-to-install-ghostscript-5e99</link>
      <guid>https://dev.to/ahmadfarazcrypto/i-built-a-pdf-conversion-api-so-you-dont-have-to-install-ghostscript-5e99</guid>
      <description>&lt;p&gt;A developer friend messaged me last month. He's building a SaaS for accountants — small product, growing nicely. One feature his customers kept asking for: &lt;em&gt;"Can your app just compress these PDFs before I email them?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Simple feature on paper. Until he tried to build it.&lt;/p&gt;

&lt;p&gt;He spent a weekend on Ghostscript flags trying to get the compression right. Then another evening fighting Docker images because Ghostscript wouldn't install on his Railway deployment. Then he discovered his compressed PDFs sometimes corrupted the text layer, which broke his customers' workflow. Then he realized he'd need OCR for scanned PDFs, which meant Tesseract, which meant another binary, more system dependencies, more Docker pain.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"This is supposed to be a one-line feature. I've spent four days on it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the conversation that made us build the ConvertKr API. If you've been down this rabbit hole, you know exactly what he meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Same PDF and image tools we've been running for free at &lt;a href="https://convertkr.com" rel="noopener noreferrer"&gt;convertkr.com&lt;/a&gt;, exposed as a JSON HTTP API at &lt;code&gt;api.convertkr.com&lt;/code&gt;. You send a file in, you get a processed file back. No file storage. No queue infrastructure. No system dependencies to install.&lt;/p&gt;

&lt;p&gt;Currently live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDF&lt;/strong&gt; — compress, merge, split, rotate, crop, organize (reorder pages), add page numbers, insert pages, extract text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt; — convert (any format to any format), resize, watermark, HEIC to JPG&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coming next&lt;/strong&gt; — OCR, HTML to PDF, PDF to image, PDF to Word (Office-grade via Windows VPS), background removal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The simplest possible example
&lt;/h2&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.convertkr.com/v1/compress-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 ckr_live_YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@invoice.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"quality=medium"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; invoice-compressed.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One HTTP call. Compressed PDF lands in your working directory.&lt;/p&gt;

&lt;p&gt;From Node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quality&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.convertkr.com/v1/compress-pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer ckr_live_YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice-compressed.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern in Python, Go, PHP, Ruby — anything with an HTTP client. We don't ship SDKs yet because the API is small enough that a raw &lt;code&gt;fetch&lt;/code&gt; is shorter than importing a wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why credits, not a subscription
&lt;/h2&gt;

&lt;p&gt;This was the design decision we agonized over.&lt;/p&gt;

&lt;p&gt;Most file-conversion APIs charge a flat monthly subscription — say $49/month for up to 1,000 conversions. Sounds clean. But it punishes anyone whose usage isn't perfectly steady.&lt;/p&gt;

&lt;p&gt;If you do 100 conversions in January and 2,000 in February, you either overpay in January or get throttled in February. If your business is seasonal (tax software peaks in April, e-commerce peaks in November), subscription pricing doesn't fit. Indie developers shipping side projects hate it most — you pay $49 for the privilege of doing 12 conversions a month.&lt;/p&gt;

&lt;p&gt;So we went with credits. Buy a pack up front. Each call deducts a small number of credits based on the operation. Cheap ops cost less, expensive ops cost more. Balance never expires.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Credits per call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rotate PDF&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge / Split PDF&lt;/td&gt;
&lt;td&gt;1-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compress PDF&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image convert / resize&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC to JPG&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF to text&lt;/td&gt;
&lt;td&gt;2-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCR PDF (when live)&lt;/td&gt;
&lt;td&gt;5-10 per page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;50 credits free on signup, no card. Smallest paid pack is $5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth — boring, but worth describing
&lt;/h2&gt;

&lt;p&gt;Two modes, one user record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JWT cookies&lt;/strong&gt; for the dashboard on &lt;code&gt;convertkr.com&lt;/code&gt; (signup, balance, generate API keys, view ledger)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bearer API keys&lt;/strong&gt; for the API itself — format &lt;code&gt;ckr_live_&amp;lt;32 hex chars&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keys show the full value once at creation. After that we store only the first 12 chars for display and a SHA-256 hash of the rest for verification. Same pattern Stripe uses. We copied it because it's good and there's no point reinventing it.&lt;/p&gt;

&lt;p&gt;The credit ledger is &lt;strong&gt;append-only&lt;/strong&gt;. We never &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; rows — every change is an insert with a positive or negative &lt;code&gt;delta&lt;/code&gt;. Balance = &lt;code&gt;SUM(delta) WHERE user_id = ?&lt;/code&gt;. This sounds excessive until you realize how much pain it saves when you need to investigate "why does this user have N credits."&lt;/p&gt;

&lt;p&gt;Race-safe credit consumption uses a &lt;code&gt;pg_advisory_xact_lock(hashtext('user-credits-' || user_id))&lt;/code&gt; inside the deduct transaction, and the deduction is committed only &lt;strong&gt;after&lt;/strong&gt; the conversion succeeds. So a failed conversion is a free conversion — no manual refunds, no edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  "What happens to my file?"
&lt;/h2&gt;

&lt;p&gt;Nothing. Files are never written to disk. Your PDF arrives in the request body, gets processed in worker memory, the response goes back, and the buffer is garbage-collected. There is no "uploads" folder. There is no S3 bucket. There is no temp directory.&lt;/p&gt;

&lt;p&gt;This matters more than people realize. If you're processing invoices, contracts, medical PDFs, tax forms — anything sensitive — you don't want a vendor that keeps copies "for 30 days" or "for billing purposes." We don't keep copies at all.&lt;/p&gt;

&lt;p&gt;The reason the WordPress side does everything client-side in the browser is the same reason: we don't want to be responsible for your data. The API extends that promise to the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What people are building with it
&lt;/h2&gt;

&lt;p&gt;From the early signups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SaaS apps adding "compress before email"&lt;/strong&gt; — exact use case from the opening story. One API call replaces installing Ghostscript on a managed Node host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoice processing&lt;/strong&gt; — merge invoice + statement + receipt before sending, rotate sideways scans, compress under email size limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real estate apps&lt;/strong&gt; — split 30-page scanned contracts into per-section PDFs, add page numbers, reorganize for storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image-heavy apps fighting HEIC&lt;/strong&gt; — users upload HEIC, your app needs JPG, instead of bundling a decoder you make one call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress plugins&lt;/strong&gt; — restrictive hosting usually can't install system binaries; an HTTP API sidesteps that whole problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal tooling&lt;/strong&gt; — operations teams batch-processing for compliance, archiving, or migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Office formats sidequest (why they're not in the Node API)
&lt;/h2&gt;

&lt;p&gt;One thing intentionally NOT in the Node API: Word to PDF, Excel to PDF, PowerPoint to PDF.&lt;/p&gt;

&lt;p&gt;Honest reason: open-source conversion libraries for Office formats (LibreOffice headless, Pandoc) are &lt;em&gt;okay&lt;/em&gt; for simple documents but break on real-world files with custom fonts, complex tables, embedded charts, or proprietary Microsoft features. The output looks &lt;em&gt;almost&lt;/em&gt; right — which is worse than looking obviously wrong. Your customers send the converted file to a recruiter or client, and the formatting is subtly broken.&lt;/p&gt;

&lt;p&gt;So Office conversion runs on a separate Windows VPS with actual Microsoft Office installed. Real Word renders real Word files. Same output you'd get if you opened the file in Office on your laptop and clicked "Save as PDF." When those endpoints come online, you'll call them the same way — but they're routed to that Windows worker behind the scenes.&lt;/p&gt;

&lt;p&gt;Anyone who tells you their pure-Node service produces "identical to Word" output is selling you something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things we deliberately don't do
&lt;/h2&gt;

&lt;p&gt;This is the part I never see in API docs but always wish I could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We don't store your files.&lt;/strong&gt; Already covered. Worth repeating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No webhooks for synchronous operations.&lt;/strong&gt; Compress a PDF, get it back in the same HTTP response. No "we'll send a webhook when ready" for jobs that take 2 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No billing for failures.&lt;/strong&gt; If conversion fails on our side, no credits deducted. Atomic inside one transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-renew.&lt;/strong&gt; You buy a pack manually. We don't store your card and surprise you next month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate limits as a sales tactic.&lt;/strong&gt; Technical limits exist (so one customer can't accidentally DOS the service) but they're high enough that legitimate use never hits them. We're not throttling free-tier users to upsell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; Stop using us — credits stay. Want a refund on unused credits — we'll process it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack, for the curious
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hono&lt;/strong&gt; on Node 22 (Express-style ergonomics, web-standards request/response, painless to run on Railway and edge runtimes alike)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drizzle ORM&lt;/strong&gt; + &lt;strong&gt;postgres.js&lt;/strong&gt; + &lt;strong&gt;PostgreSQL 17&lt;/strong&gt; for the user/key/ledger schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; strict mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lemon Squeezy&lt;/strong&gt; as merchant of record for payments (we're in Pakistan — direct Stripe isn't available; LS handles VAT and global tax)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Railway&lt;/strong&gt; for hosting (Postgres + app on git push)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argon2id&lt;/strong&gt; for password hashing, &lt;strong&gt;SHA-256&lt;/strong&gt; for API key hashing (keys are 128-bit random — slow hashes are unnecessary and would add latency to every request)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire codebase is around 3,000 lines of TypeScript. Small on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to start
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign up on &lt;a href="https://convertkr.com" rel="noopener noreferrer"&gt;convertkr.com&lt;/a&gt; with email + password — 50 free credits land immediately.&lt;/li&gt;
&lt;li&gt;Generate an API key from the dashboard (full key shows once — save it).&lt;/li&gt;
&lt;li&gt;Run a curl against any endpoint. Watch the balance tick down in real time.&lt;/li&gt;
&lt;li&gt;When ready, buy a credit pack via Lemon Squeezy. Credits land within seconds of payment via webhook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're integrating into a real product and need to verify the API handles your specific files before committing — just email. We'll grant test credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd love feedback on
&lt;/h2&gt;

&lt;p&gt;Honest list — would value any of these from the comments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pricing.&lt;/strong&gt; Are credits at $5 / 1,000 cheap enough? Too cheap? Wrong unit?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint coverage.&lt;/strong&gt; What's missing that you'd actually use? (We can prioritize based on demand — most additions take under two weeks.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDKs.&lt;/strong&gt; Would an official Node or Python wrapper actually save you time, or is &lt;code&gt;fetch&lt;/code&gt; + curl fine?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR.&lt;/strong&gt; When OCR comes online, would you want it as a separate endpoint or built into PDF-to-text automatically?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest answer to "what would make you actually adopt this" is the most useful comment you can leave.&lt;/p&gt;




&lt;p&gt;Source code for the API itself is closed for now (it's tied to the credit ledger and billing), but happy to talk architecture in the comments. Conversion logic underneath uses pdf-lib, sharp, and a few open-source libraries — nothing exotic.&lt;/p&gt;

&lt;p&gt;If you've been down the Ghostscript-in-Docker rabbit hole, you know the pain. We've been there. The API exists so you don't have to be.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://convertkr.com" rel="noopener noreferrer"&gt;Try it free with 50 credits →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>pdf</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Needed to Remove a QR Code from an PDF, But Every Solution Was Complicated</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Sat, 30 May 2026 09:37:05 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/i-needed-to-remove-a-qr-code-from-an-image-but-every-solution-was-complicated-7oa</link>
      <guid>https://dev.to/ahmadfarazcrypto/i-needed-to-remove-a-qr-code-from-an-image-but-every-solution-was-complicated-7oa</guid>
      <description>&lt;p&gt;A few weeks ago, I was updating some marketing assets for one of my projects.&lt;/p&gt;

&lt;p&gt;Everything looked good until I noticed a small problem.&lt;/p&gt;

&lt;p&gt;The pdf contained an old QR code.&lt;/p&gt;

&lt;p&gt;The QR code was pointing to an outdated page, and I needed to remove it before publishing the pdf again.&lt;/p&gt;

&lt;p&gt;My first thought was, "This should be easy."&lt;/p&gt;

&lt;p&gt;I opened a few pdf editing tools and quickly realized it wasn't as simple as I expected.&lt;/p&gt;

&lt;p&gt;Most solutions required installing software, learning editing techniques, or manually covering the QR code with another object. Some AI tools could do it, but they were either paid or required creating an account.&lt;/p&gt;

&lt;p&gt;For a task that should take a few seconds, I was spending far too much time.&lt;/p&gt;

&lt;p&gt;That's when I started wondering:&lt;/p&gt;

&lt;p&gt;"Why isn't there a simple tool that only removes QR codes?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With QR Codes
&lt;/h2&gt;

&lt;p&gt;QR codes are everywhere.&lt;/p&gt;

&lt;p&gt;They're on flyers, product images, posters, presentations, screenshots, and social media graphics.&lt;/p&gt;

&lt;p&gt;The problem is that QR codes don't always stay relevant.&lt;/p&gt;

&lt;p&gt;Businesses change landing pages.&lt;/p&gt;

&lt;p&gt;Campaigns expire.&lt;/p&gt;

&lt;p&gt;Links break.&lt;/p&gt;

&lt;p&gt;Sometimes you simply want to reuse an pdf without the QR code.&lt;/p&gt;

&lt;p&gt;Yet removing one often requires using software designed for professional designers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Simpler Solution
&lt;/h2&gt;

&lt;p&gt;Instead of continuing to search for a solution, I decided to build one.&lt;/p&gt;

&lt;p&gt;The goal was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload an pdf&lt;/li&gt;
&lt;li&gt;Detect QR codes automatically&lt;/li&gt;
&lt;li&gt;Remove them&lt;/li&gt;
&lt;li&gt;Download the cleaned result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No accounts.&lt;/p&gt;

&lt;p&gt;No complicated editing.&lt;/p&gt;

&lt;p&gt;No learning curve.&lt;/p&gt;

&lt;p&gt;Just a tool that solves one problem well.&lt;/p&gt;

&lt;p&gt;After several iterations, the result became the Remove QR Code tool on ConvertKR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;One thing I've learned from building developer tools is that users don't always need more features.&lt;/p&gt;

&lt;p&gt;Sometimes they just need fewer steps.&lt;/p&gt;

&lt;p&gt;The best tools are often the ones that remove friction from a small but frustrating task.&lt;/p&gt;

&lt;p&gt;Removing a QR code is not something people do every day.&lt;/p&gt;

&lt;p&gt;But when they need it, they want the process to be fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you've ever found yourself dealing with outdated QR codes in pdfs, you can try the tool here:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://convertkr.com/remove-qr-pdf/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fconvertkr.com%2Fwp-content%2Fthemes%2Fpdf-editor-theme%2Fassets%2Fog-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://convertkr.com/remove-qr-pdf/" rel="noopener noreferrer" class="c-link"&gt;
            Remove QR Code from PDF Online Free – Instant Tool | ConvertKr
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Remove watermarks, stamps, logos, QR codes, text, or any content from PDFs. Draw a box and erase. Free, no signup.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fconvertkr.com%2Fwp-content%2Fthemes%2Fpdf-editor-theme%2Fassets%2Ffavicon.svg" width="64" height="64"&gt;
          convertkr.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I'd love to hear feedback from other developers, marketers, and designers who run into this problem.&lt;/p&gt;

&lt;p&gt;Sometimes the most useful products come from solving our own small frustrations.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>pdf</category>
    </item>
    <item>
      <title>I built a free offline PDF &amp; image toolkit — 15 tools, works without internet</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Tue, 31 Mar 2026 20:59:55 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/i-built-a-free-offline-pdf-image-toolkit-15-tools-works-without-internet-52g0</link>
      <guid>https://dev.to/ahmadfarazcrypto/i-built-a-free-offline-pdf-image-toolkit-15-tools-works-without-internet-52g0</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;ConvertKr Desktop&lt;/strong&gt; — a free PDF &amp;amp; image toolkit with 15 tools that works 100% offline. Available for macOS, Windows, and Linux. No internet, no cloud uploads, no subscriptions.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://convertkr.com/desktop-app/" rel="noopener noreferrer"&gt;Download&lt;/a&gt; | &lt;a href="https://convertkr.com" rel="noopener noreferrer"&gt;Web Version&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every time I need to merge a few PDFs or compress a document before emailing, I end up on some random website that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📤 Uploads my files to their server (privacy?)&lt;/li&gt;
&lt;li&gt;💧 Adds a watermark unless I pay&lt;/li&gt;
&lt;li&gt;💰 Gives me 2 free uses per day, then asks for $12/month&lt;/li&gt;
&lt;li&gt;🐌 Takes forever to upload and download&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For something as simple as merging two PDFs, this felt ridiculous.&lt;/p&gt;




&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;ConvertKr&lt;/strong&gt; — first as a &lt;a href="https://convertkr.com" rel="noopener noreferrer"&gt;free web app&lt;/a&gt; where everything processes right in your browser (your files never leave your device), and now as a &lt;strong&gt;desktop app&lt;/strong&gt; that works completely offline.&lt;/p&gt;

&lt;p&gt;No internet. No accounts. No limits. Just open the app, pick your files, and get the result.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you can do with it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PDF Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merge&lt;/strong&gt; — combine multiple PDFs into one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split&lt;/strong&gt; — extract pages by range or individually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress&lt;/strong&gt; — reduce file size before emailing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate&lt;/strong&gt; — fix sideways scanned pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organize&lt;/strong&gt; — drag and drop to reorder or delete pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF to Image&lt;/strong&gt; — convert pages to PNG, JPG, or WebP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect&lt;/strong&gt; — add watermark protection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlock&lt;/strong&gt; — remove password from PDFs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watermark&lt;/strong&gt; — add text watermarks (supports Urdu too)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page Numbers&lt;/strong&gt; — add numbers in any position&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF Editor&lt;/strong&gt; — add text, images, shapes, drawings, and highlights&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Image Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image to PDF&lt;/strong&gt; — convert photos to PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Convert&lt;/strong&gt; — switch between PNG, JPG, WebP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Compress&lt;/strong&gt; — make images smaller&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Crop&lt;/strong&gt; — visual crop tool&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;🔒 &lt;strong&gt;Privacy&lt;/strong&gt; — your files never leave your computer. Period.&lt;/li&gt;
&lt;li&gt;📶 &lt;strong&gt;No internet needed&lt;/strong&gt; — works on a plane, in a cafe with bad wifi, anywhere&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Fast&lt;/strong&gt; — no uploading, no downloading, no waiting&lt;/li&gt;
&lt;li&gt;📁 &lt;strong&gt;No file limits&lt;/strong&gt; — handle large documents that crash browser-based tools&lt;/li&gt;
&lt;li&gt;💸 &lt;strong&gt;Free forever&lt;/strong&gt; — no trial that expires, no premium tier, no "upgrade to unlock"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Available on all platforms
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Download&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Apple Silicon)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://convertkr.com/desktop-app/" rel="noopener noreferrer"&gt;DMG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows (64-bit)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://convertkr.com/desktop-app/" rel="noopener noreferrer"&gt;EXE Installer&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux (64-bit)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://convertkr.com/desktop-app/" rel="noopener noreferrer"&gt;AppImage&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux (Debian/Ubuntu)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://convertkr.com/desktop-app/" rel="noopener noreferrer"&gt;.deb&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; If you see "app is damaged", open Terminal and run: &lt;code&gt;xattr -cr /Applications/ConvertKr.app&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; If SmartScreen appears, click "More info" → "Run anyway"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These warnings appear because the app isn't code-signed yet. It's completely safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Web version too
&lt;/h2&gt;

&lt;p&gt;Don't want to download anything? All tools also work at &lt;strong&gt;&lt;a href="https://convertkr.com" rel="noopener noreferrer"&gt;convertkr.com&lt;/a&gt;&lt;/strong&gt; — directly in your browser, with 27+ tools including OCR, background remover, QR codes, and more. Same privacy — everything processes locally.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's coming next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔍 OCR — extract text from scanned PDFs&lt;/li&gt;
&lt;li&gt;📦 Batch processing — drop a folder, process everything&lt;/li&gt;
&lt;li&gt;✍️ Digital signatures&lt;/li&gt;
&lt;li&gt;📝 PDF form filler&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;Completely free. No trial. No license. No limits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://convertkr.com/desktop-app/" rel="noopener noreferrer"&gt;Download ConvertKr Desktop&lt;/a&gt;&lt;br&gt;
👉 &lt;a href="https://convertkr.com" rel="noopener noreferrer"&gt;Use the web version&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions or feedback? &lt;strong&gt;&lt;a href="mailto:ahmadfarazjutt3@gmail.com"&gt;ahmadfarazjutt3@gmail.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you find it useful, &lt;a href="https://buymeacoffee.com/ahmadfaraz1" rel="noopener noreferrer"&gt;buy me a coffee ☕&lt;/a&gt; — it keeps the project going.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>nocode</category>
      <category>pdf</category>
      <category>freeware</category>
    </item>
    <item>
      <title>How I Built 11 Free File Tools That Run Entirely in the Browser</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Sun, 22 Feb 2026 20:16:56 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/how-i-built-11-free-file-tools-that-run-entirely-in-the-browser-2p7l</link>
      <guid>https://dev.to/ahmadfarazcrypto/how-i-built-11-free-file-tools-that-run-entirely-in-the-browser-2p7l</guid>
      <description>&lt;p&gt;Every time I needed to edit a PDF or convert an image online, the same thing happened. Upload my file to some random server, create an account, hit a paywall after two tries, and wonder who's looking at my documents.&lt;/p&gt;

&lt;p&gt;I got tired of it. So I built my own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet ConvertKr
&lt;/h2&gt;

&lt;p&gt;It's a set of 11 free browser-based tools for working with PDFs and images. The catch? There is no catch. No signups, no watermarks, no file limits.&lt;/p&gt;

&lt;p&gt;But the part I care about most — &lt;strong&gt;your files never leave your device&lt;/strong&gt;. Everything runs locally in your browser using JavaScript. My server never sees your documents. Not temporarily, not for processing, not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PDF Editor — text, images, signatures, highlights, redactions&lt;/li&gt;
&lt;li&gt;Merge PDF and Split PDF&lt;/li&gt;
&lt;li&gt;PDF to Image and Image to PDF&lt;/li&gt;
&lt;li&gt;Image Convert, Compress, Crop and Resize&lt;/li&gt;
&lt;li&gt;Image Watermark&lt;/li&gt;
&lt;li&gt;Background Remover&lt;/li&gt;
&lt;li&gt;QR Code Generator&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How is this possible without a server?
&lt;/h2&gt;

&lt;p&gt;Modern browsers are way more powerful than most people give them credit for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF.js&lt;/strong&gt; handles PDF rendering — the same library Firefox uses internally. &lt;strong&gt;pdf-lib&lt;/strong&gt; generates new PDFs with your edits baked in. The &lt;strong&gt;Canvas API&lt;/strong&gt; handles all image operations — converting formats, compressing, cropping, watermarking. For background removal, an ML model runs directly in the browser.&lt;/p&gt;

&lt;p&gt;No frameworks either. Just vanilla JavaScript. Each tool is a standalone page that loads fast and does one thing well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest part
&lt;/h2&gt;

&lt;p&gt;The PDF editor was by far the most complex. You're essentially building a layer system on top of a rendered PDF page. Every annotation — text, image, signature, drawing — needs to be positioned in CSS pixels on screen, then converted back to PDF coordinate points when generating the final file. Getting that math wrong by even a few pixels means signatures end up in the wrong spot.&lt;/p&gt;

&lt;p&gt;I also spent way too long debugging an ArrayBuffer detachment issue. When PDF.js reads your file, it detaches the buffer so pdf-lib can't use it anymore. The fix was simple once I found it — store the data as a Uint8Array immediately and pass copies to both libraries — but finding the cause was painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm sharing this
&lt;/h2&gt;

&lt;p&gt;Two reasons. First, I genuinely think more developers should consider client-side processing for file tools. The browser can handle it, your users get real privacy, and you save on server costs. It's a win all around.&lt;/p&gt;

&lt;p&gt;Second, I want feedback. I'm not a designer and I built this mostly alone. If something looks off, feels broken, or could be better — I want to know.&lt;/p&gt;

&lt;p&gt;I'm also actively adding more tools. If there's something you wish existed that doesn't upload your files to the cloud, tell me. I might build it next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check it out at convertkr.com&lt;/strong&gt; — would love to hear what you think.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Modern CV Builder with React 19, TypeScript, and Firebase: A Complete Guide</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Thu, 06 Nov 2025 11:47:03 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/building-a-modern-cv-builder-with-react-19-typescript-and-firebase-a-complete-guide-ldk</link>
      <guid>https://dev.to/ahmadfarazcrypto/building-a-modern-cv-builder-with-react-19-typescript-and-firebase-a-complete-guide-ldk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Learn how to build a professional, open-source CV/Resume builder application with real-time preview, PDF export, and Firebase integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🎯 Introduction
&lt;/h2&gt;

&lt;p&gt;In this comprehensive guide, I'll walk you through building a modern CV/Resume builder application from scratch. This project demonstrates advanced React patterns, TypeScript best practices, Firebase integration, and performance optimization techniques that you can apply to your own projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we're building:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A full-featured CV builder with 11 beautiful templates&lt;/li&gt;
&lt;li&gt;Real-time preview with side-by-side editing&lt;/li&gt;
&lt;li&gt;PDF export and import functionality&lt;/li&gt;
&lt;li&gt;Firebase authentication and data persistence&lt;/li&gt;
&lt;li&gt;Performance-optimized components&lt;/li&gt;
&lt;li&gt;A 6-step guided wizard for resume creation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/AhmadFaraz-crypto/resume-studio" rel="noopener noreferrer"&gt;GitHub - resume-studio&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Project Overview
&lt;/h2&gt;

&lt;p&gt;This CV builder is a production-ready application that showcases modern web development practices. It's built with React 19, TypeScript, and Firebase, providing a seamless user experience for creating professional resumes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✨ &lt;strong&gt;11 Professional Templates&lt;/strong&gt; - From minimalist to modern designs&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Real-time Preview&lt;/strong&gt; - See changes instantly as you type&lt;/li&gt;
&lt;li&gt;📄 &lt;strong&gt;PDF Export/Import&lt;/strong&gt; - Export your CV or upload existing PDFs&lt;/li&gt;
&lt;li&gt;🔐 &lt;strong&gt;Firebase Authentication&lt;/strong&gt; - Email, Google, and Twitter sign-in&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Auto-save&lt;/strong&gt; - Your progress is automatically saved&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Performance Optimized&lt;/strong&gt; - Memoized components prevent unnecessary re-renders&lt;/li&gt;
&lt;li&gt;📱 &lt;strong&gt;Responsive Design&lt;/strong&gt; - Works perfectly on all devices&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛠️ Tech Stack
&lt;/h2&gt;

&lt;p&gt;Here's what powers this application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 19&lt;/strong&gt; - Latest React with hooks and memoization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; - Type-safe development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite 7&lt;/strong&gt; - Lightning-fast build tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; - Utility-first styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase&lt;/strong&gt; - Authentication, Firestore, and Storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Router v7&lt;/strong&gt; - Client-side routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jsPDF &amp;amp; html2canvas&lt;/strong&gt; - PDF generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdfjs-dist&lt;/strong&gt; - PDF parsing and data extraction&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📐 Architecture &amp;amp; Design Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Component Structure
&lt;/h3&gt;

&lt;p&gt;The application follows a modular component architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── components/
│   ├── common/           # Reusable UI components
│   ├── dashboard/        # Dashboard components
│   ├── layout/           # Layout components
│   ├── templates/        # 11 CV template components
│   └── wizard/           # Wizard step components
├── hooks/                # Custom React hooks
├── services/             # Firebase services
├── contexts/             # React contexts
└── utils/                # Utility functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Custom Hooks for State Management
&lt;/h3&gt;

&lt;p&gt;We use custom hooks to encapsulate state logic and make components cleaner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/hooks/useCVData.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CVData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../types/cv.types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;saveCVData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadCVData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/dataPersistence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useCVData&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cvData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCvData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CVData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;freshCVData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadCVData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setCvData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;setIsLoading&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;saveCVData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cvData&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;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cvData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateCVData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CVData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setCvData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cvData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateCVData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&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;This hook provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic loading from localStorage&lt;/li&gt;
&lt;li&gt;Auto-save functionality&lt;/li&gt;
&lt;li&gt;Stable function references with &lt;code&gt;useCallback&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Performance Optimization with React.memo
&lt;/h3&gt;

&lt;p&gt;One of the critical challenges in a real-time editor is preventing unnecessary re-renders. Here's how we solved it:&lt;/p&gt;

&lt;h4&gt;
  
  
  The Problem
&lt;/h4&gt;

&lt;p&gt;When typing in an input field, every keystroke would trigger a state update, causing the entire component tree to re-render. This led to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input fields losing focus&lt;/li&gt;
&lt;li&gt;Poor typing performance&lt;/li&gt;
&lt;li&gt;Unnecessary DOM updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Solution
&lt;/h4&gt;

&lt;p&gt;We memoized components and used refs to maintain stable function references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/components/common/Input.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;InputHTMLAttributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;InputProps&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;InputHTMLAttributes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;helperText&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InputProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; 
  &lt;span class="nx"&gt;label&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;helperText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; 
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;`input-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ... rest of component&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  CollapsibleSection Optimization
&lt;/h4&gt;

&lt;p&gt;The biggest performance win came from moving &lt;code&gt;CollapsibleSection&lt;/code&gt; outside the parent component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Before: Defined inside component (recreated on every render)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CVEditorCollapsible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CollapsibleSection&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="o"&gt;=&amp;gt;&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;span class="c1"&gt;// Recreated!&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CollapsibleSection&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ After: Defined outside (stable component identity)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CollapsibleSection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CollapsibleSectionProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; 
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isExpanded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onToggle&lt;/span&gt; 
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-white rounded-2xl shadow-lg overflow-hidden mb-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CVEditorCollapsible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CollapsibleSection&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="o"&gt;/&amp;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;&lt;strong&gt;Why this matters:&lt;/strong&gt; When &lt;code&gt;CollapsibleSection&lt;/code&gt; was defined inside the parent, React saw it as a new component type on every render, causing all children to unmount and remount. Moving it outside gives it a stable identity, allowing React to properly optimize re-renders.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Firebase Integration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Authentication Setup
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config/firebase.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initializeApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getFirestore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/firestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firebaseConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_AUTH_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storageBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_STORAGE_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messagingSenderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_MESSAGING_SENDER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_APP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFirestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Service Layer Pattern
&lt;/h4&gt;

&lt;p&gt;We use a service layer to abstract Firebase operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/authService.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;signInWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createUserWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signInWithPopup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;GoogleAuthProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TwitterAuthProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onAuthStateChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../config/firebase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signInUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signInWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createUserWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signInWithGoogle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleAuthProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signInWithPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onAuthStateChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;onAuthStateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&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;This pattern provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Centralized Firebase logic&lt;/li&gt;
&lt;li&gt;Easy testing and mocking&lt;/li&gt;
&lt;li&gt;Consistent error handling&lt;/li&gt;
&lt;li&gt;Type safety&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎨 Key Features Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Real-time Preview
&lt;/h3&gt;

&lt;p&gt;The editor uses a two-column layout with synchronized state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/EditorPage.tsx&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grid grid-cols-1 lg:grid-cols-2 gap-12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Left: Editor */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;overflow-y-auto pr-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CVEditorCollapsible&lt;/span&gt; 
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cvData&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; 
      &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;newData&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;updateCVData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; 
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Right: Preview */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;overflow-y-auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cv-export-content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;renderTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cvData&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every change in the editor updates &lt;code&gt;cvData&lt;/code&gt;, which triggers a re-render of the preview panel.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. PDF Export
&lt;/h3&gt;

&lt;p&gt;PDF export uses &lt;code&gt;html2canvas&lt;/code&gt; and &lt;code&gt;jsPDF&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/exportPDF.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;html2canvas&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html2canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jsPDF&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jspdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exportToPDF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Element not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;html2canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;useCORS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;logging&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="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imgData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;jsPDF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imgWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// A4 width in mm&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imgHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;imgWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;canvas&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="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imgData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PNG&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imgWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imgHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&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;h3&gt;
  
  
  3. PDF Import &amp;amp; Data Extraction
&lt;/h3&gt;

&lt;p&gt;Users can upload existing PDFs and extract data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/exportPDF.ts (simplified)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pdfjsLib&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfjs-dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extractTextFromPDF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arrayBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfjsLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;arrayBuffer&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fullText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numPages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;fullText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pageText&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fullText&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;h3&gt;
  
  
  4. 6-Step Wizard
&lt;/h3&gt;

&lt;p&gt;The wizard guides users through resume creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/ResumeWizard.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;personal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Personal Information&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PersonalInfoStep&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;work&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Work Experience&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkExperienceStep&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;education&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Education&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EducationStep&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;skills&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Skills&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SkillsStep&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SummaryStep&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReviewStep&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleComplete&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;updateCVData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wizardData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedTemplate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/editor/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selectedTemplate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Each step validates input before allowing progression, ensuring data quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Performance Optimizations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Component Memoization
&lt;/h3&gt;

&lt;p&gt;We memoized all frequently re-rendered components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Input&lt;/code&gt;, &lt;code&gt;Textarea&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt; - Using &lt;code&gt;React.memo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CollapsibleSection&lt;/code&gt; - Moved outside and memoized&lt;/li&gt;
&lt;li&gt;Event handlers - Using &lt;code&gt;useCallback&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Stable Function References
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePersonalInfoChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;onChangeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;currentData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;personalInfo&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;span class="nx"&gt;currentData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;personalInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;value&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;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt; &lt;span class="c1"&gt;// Empty deps - uses refs instead&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using refs (&lt;code&gt;dataRef&lt;/code&gt;, &lt;code&gt;onChangeRef&lt;/code&gt;), we can keep the dependency array empty, ensuring the function reference never changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Local Storage Caching
&lt;/h3&gt;

&lt;p&gt;CV data is automatically cached in localStorage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;saveCVData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cvData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Auto-save on every change&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;span class="nx"&gt;cvData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instant loading on page refresh&lt;/li&gt;
&lt;li&gt;Offline capability&lt;/li&gt;
&lt;li&gt;Reduced Firebase read operations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📱 Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 16+&lt;/li&gt;
&lt;li&gt;npm or yarn&lt;/li&gt;
&lt;li&gt;Firebase account (free tier works)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/AhmadFaraz-crypto/resume-studio.git
&lt;span class="nb"&gt;cd &lt;/span&gt;resume-studio

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Set up environment variables&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;env.example .env.local

&lt;span class="c"&gt;# Add your Firebase config to .env.local&lt;/span&gt;
&lt;span class="nv"&gt;VITE_FIREBASE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-api-key
&lt;span class="nv"&gt;VITE_FIREBASE_AUTH_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-project.firebaseapp.com
&lt;span class="nv"&gt;VITE_FIREBASE_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-project-id
&lt;span class="nv"&gt;VITE_FIREBASE_STORAGE_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-project.appspot.com
&lt;span class="nv"&gt;VITE_FIREBASE_MESSAGING_SENDER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;123456789
&lt;span class="nv"&gt;VITE_FIREBASE_APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-app-id

&lt;span class="c"&gt;# Start development server&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Firebase Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create Firebase Project&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;a href="https://console.firebase.google.com/" rel="noopener noreferrer"&gt;Firebase Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create a new project&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable Services&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: Enable Email/Password, Google, Twitter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firestore&lt;/strong&gt;: Create database in test mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Create storage bucket in test mode&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Get Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project Settings → Your apps → Web app&lt;/li&gt;
&lt;li&gt;Copy config values to &lt;code&gt;.env.local&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🎯 Key Learnings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Performance Matters
&lt;/h3&gt;

&lt;p&gt;Memoization isn't just about optimization—it's about user experience. Preventing unnecessary re-renders ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smooth typing experience&lt;/li&gt;
&lt;li&gt;No focus loss&lt;/li&gt;
&lt;li&gt;Better perceived performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Component Architecture
&lt;/h3&gt;

&lt;p&gt;Moving components outside their parents when they're recreated on every render is crucial. React needs stable component identities for proper optimization.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. TypeScript is Your Friend
&lt;/h3&gt;

&lt;p&gt;Strong typing catches bugs early and makes refactoring safer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CVData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;personalInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;zipCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;linkedin&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;workExperience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkExperience&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;education&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Education&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;h3&gt;
  
  
  4. Service Layer Pattern
&lt;/h3&gt;

&lt;p&gt;Abstracting Firebase operations into services provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better testability&lt;/li&gt;
&lt;li&gt;Easier maintenance&lt;/li&gt;
&lt;li&gt;Consistent error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 Deployment
&lt;/h2&gt;

&lt;p&gt;The application can be deployed to any static hosting platform:&lt;/p&gt;

&lt;h3&gt;
  
  
  Netlify
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build command&lt;/span&gt;
npm run build

&lt;span class="c"&gt;# Publish directory&lt;/span&gt;
dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add environment variables in Netlify dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Framework: Vite&lt;/span&gt;
&lt;span class="c"&gt;# Build command: npm run build&lt;/span&gt;
&lt;span class="c"&gt;# Output directory: dist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environment variables are configured in Vercel dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤝 Contributing
&lt;/h2&gt;

&lt;p&gt;Contributions are welcome! Areas for contribution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎨 New template designs&lt;/li&gt;
&lt;li&gt;🐛 Bug fixes&lt;/li&gt;
&lt;li&gt;📚 Documentation improvements&lt;/li&gt;
&lt;li&gt;♿ Accessibility enhancements&lt;/li&gt;
&lt;li&gt;🌍 Internationalization&lt;/li&gt;
&lt;li&gt;🧪 Tests&lt;/li&gt;
&lt;li&gt;🎯 Performance optimizations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📚 Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.typescriptlang.org/docs/" rel="noopener noreferrer"&gt;TypeScript Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs" rel="noopener noreferrer"&gt;Firebase Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vitejs.dev/guide/" rel="noopener noreferrer"&gt;Vite Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/docs" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎉 Conclusion
&lt;/h2&gt;

&lt;p&gt;Building this CV builder taught me valuable lessons about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React performance optimization&lt;/li&gt;
&lt;li&gt;TypeScript best practices&lt;/li&gt;
&lt;li&gt;Firebase integration patterns&lt;/li&gt;
&lt;li&gt;Component architecture&lt;/li&gt;
&lt;li&gt;User experience design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application is fully open-source and production-ready. You can use it as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A learning resource&lt;/li&gt;
&lt;li&gt;A starting point for your own projects&lt;/li&gt;
&lt;li&gt;A portfolio showcase&lt;/li&gt;
&lt;li&gt;A contribution opportunity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Star the repository&lt;/strong&gt; if you find it helpful: &lt;a href="https://github.com/AhmadFaraz-crypto/resume-studio" rel="noopener noreferrer"&gt;GitHub - resume-studio&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 Questions?
&lt;/h2&gt;

&lt;p&gt;Have questions or suggestions? Feel free to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open an issue on GitHub&lt;/li&gt;
&lt;li&gt;Start a discussion&lt;/li&gt;
&lt;li&gt;Submit a pull request&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tutorial</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>How to Capture Web Page Screenshots with Next.js and Puppeteer</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Fri, 01 Nov 2024 15:30:44 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/how-to-capture-web-page-screenshots-with-nextjs-and-puppeteer-o7p</link>
      <guid>https://dev.to/ahmadfarazcrypto/how-to-capture-web-page-screenshots-with-nextjs-and-puppeteer-o7p</guid>
      <description>&lt;p&gt;Capturing screenshots of web pages programmatically can be incredibly useful for generating previews, creating image-based reports, and more. In this guide, we’ll build a Next.js API route that takes a URL and generates a PNG screenshot. Our setup uses Puppeteer and chrome-aws-lambda to leverage a headless Chrome browser, making it versatile and production-ready.&lt;/p&gt;

&lt;p&gt;We’ll start by setting up a new Next.js project and walking through the code step-by-step to understand how the API captures screenshots.&lt;/p&gt;

&lt;p&gt;Prerequisites&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up the Next.js app&lt;/li&gt;
&lt;li&gt;Configuring the API route with Puppeteer&lt;/li&gt;
&lt;li&gt;Creating the React component for the capture interface&lt;/li&gt;
&lt;li&gt;Explanation of local vs. deployment configurations for Puppeteer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Getting Started with a New Next.js Project&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new Next.js app:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx create-next-app@latest capture-image-app
cd capture-image-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install the necessary dependencies:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install puppeteer puppeteer-core chrome-aws-lambda busboy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Create the API Route to Generate Screenshots&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, we’ll set up an API endpoint to capture and return screenshots based on a provided URL.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;pages/api&lt;/code&gt; folder, create a new file named &lt;code&gt;generate-png.ts&lt;/code&gt; and add this code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NextApiRequest, NextApiResponse } from "next";
import busboy, { Busboy } from "busboy"; // Use busboy for multipart parsing
import chromium from "chrome-aws-lambda";
import puppeteerCore from "puppeteer-core"; // Import puppeteer-core directly
import puppeteer from "puppeteer"; // Import puppeteer directly

// Conditional import for Puppeteer based on the environment
const puppeteerModule = process.env.NODE_ENV === "production" ? puppeteerCore : puppeteer;

export const config = {
  api: {
    bodyParser: false, // Disable default body parsing to handle raw binary data (Blob)
  },
};

const delay = (ms: number): Promise&amp;lt;void&amp;gt; =&amp;gt; new Promise((resolve) =&amp;gt; setTimeout(resolve, ms));

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
): Promise&amp;lt;void&amp;gt; {
  try {
    if (req.method === "POST") {
      const bb: Busboy = busboy({ headers: req.headers });
      let width: number = 1920; // Default width
      let height: number = 0; // Default height
      let delayTime: number = 6000;
      const buffers: Buffer[] = [];

      bb.on("file", (_name: string, file: NodeJS.ReadableStream) =&amp;gt; {
        file.on("data", (data: Buffer) =&amp;gt; buffers.push(data));
      });

      bb.on("field", (name: string, value: string) =&amp;gt; {
        if (name === "width") width = parseInt(value, 10) || 1920;
        if (name === "height") height = parseInt(value, 10) || 0;
        if (name === "delay") delayTime = parseInt(value, 10) || 6000;
      });

      bb.on("finish", async () =&amp;gt; {
        const blobBuffer: Buffer = Buffer.concat(buffers);
        const htmlContent: string = blobBuffer.toString("utf-8");

        const browser = await puppeteerModule.launch({
          args: ["--start-maximized"],
          executablePath: process.env.NODE_ENV === "production"
            ? await chromium.executablePath || "/usr/bin/chromium-browser"
            : undefined,  // No custom executable path needed for local
          headless: true,
        });

        const page = await browser.newPage();

        // Load the HTML content directly
        await page.setContent(htmlContent, { waitUntil: "networkidle0" });

        //@ts-expect-error todo
        const bodyHeight = await page.evaluate(() =&amp;gt; {
          return document.body.scrollHeight; // Get the full scrollable height of the body
        });

        await page.setViewport({
          width: Number(width),
          height: height || bodyHeight, // Use the provided height or fallback to the full body height
          deviceScaleFactor: 2,
        });

        await delay(delayTime);

        const screenshotBuffer = await page.screenshot({
          fullPage: !height,
          type: "png",
          omitBackground: false,
        });

        await browser.close();

        res.setHeader("Content-Type", "image/png");
        res.setHeader(
          "Content-Disposition",
          "attachment; filename=screenshot.png"
        );
        res.status(200).end(screenshotBuffer);
      });

      req.pipe(bb); // Pipe the request stream to busboy
    } else {
      res.setHeader("Allow", ["POST"]);
      res.status(405).end(`Method ${req.method} Not Allowed`);
    }
  } catch (error) {
    console.error("ERROR", error);
    res.status(500).end("Internal Server Error");
  }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;Explanation: Choosing Puppeteer for Local vs. Production Environments&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
In this code, we’ve set up a dynamic import for puppeteer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Local Development: If &lt;code&gt;NODE_ENV&lt;/code&gt; is not &lt;code&gt;production&lt;/code&gt;, it uses &lt;code&gt;puppeteer&lt;/code&gt;, which is simpler to set up and doesn’t require &lt;code&gt;chrome-aws-lambda&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Production: For serverless deployments, the environment will detect &lt;code&gt;NODE_ENV&lt;/code&gt; as &lt;code&gt;production&lt;/code&gt; and load puppeteer-core along with c&lt;code&gt;hrome-aws-lambda&lt;/code&gt;, which allows it to work in AWS Lambda and other similar environments. In this setup, &lt;code&gt;chrome-aws-lambda&lt;/code&gt; provides the correct Chromium path, ensuring compatibility with serverless providers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Create a Simple React Component for the UI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here, we’ll create a straightforward form that lets users input values for the webpage capture. This form will trigger the generate function to capture and download the screenshot in PDF format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useState } from "react";

export default function ScreenCaptureComponent() {
  const [isProcessing, setProcessing] = useState(false);
  const [width, setWidth] = useState&amp;lt;string&amp;gt;("1920");
  const [height, setHeight] = useState&amp;lt;string&amp;gt;("1000");
  const [delay, setDelay] = useState&amp;lt;string&amp;gt;("6000");

  // Function to clone HTML and prepare for capture
  function takeScreenshot() {
    const clonedElement = document.body.cloneNode(true) as HTMLElement;
    const blob = new Blob([clonedElement.outerHTML], { type: "text/html" });
    return blob;
  }

  // Function to capture screenshot by sending cloned HTML to API
  async function generateCapture() {
    setProcessing(true);

    const htmlBlob = takeScreenshot();

    if (!htmlBlob) {
      setProcessing(false);
      return;
    }

    try {
      const formData = new FormData();
      formData.append("file", htmlBlob);
      formData.append("width", width);
      formData.append("height", height);
      formData.append("delay", delay);
      const response = await fetch("/api/generate-png", {
        method: "POST",
        body: formData,
      });

      if (!response.ok) throw new Error("Capture failed");

      const blob = await response.blob();
      const downloadUrl = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = downloadUrl;
      link.download = "capture.png";
      link.click();
      URL.revokeObjectURL(downloadUrl);
    } catch (error) {
      console.error("Failed to capture screenshot", error);
    } finally {
      setProcessing(false);
    }
  }

  return (
    &amp;lt;div
      style={{
        maxWidth: "400px",
        margin: "50px auto",
        padding: "24px",
        backgroundColor: "white",
        borderRadius: "8px",
        width: "100%",
        boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
      }}
    &amp;gt;
      &amp;lt;h2
        style={{
          fontSize: "24px",
          fontWeight: "600",
          textAlign: "center",
          marginBottom: "16px",
        }}
      &amp;gt;
        Webpage Screenshot Capture
      &amp;lt;/h2&amp;gt;
      &amp;lt;form
        onSubmit={(e) =&amp;gt; {
          e.preventDefault();
          generateCapture();
        }}
        style={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          marginBottom: "16px",
        }}
      &amp;gt;
        &amp;lt;label
          style={{ marginBottom: "8px", fontWeight: "500" }}
          htmlFor="width"
        &amp;gt;
          Width (px)
        &amp;lt;/label&amp;gt;
        &amp;lt;select
          id="width"
          value={width}
          onChange={(e) =&amp;gt; setWidth(e.target.value)}
          style={{
            width: "100%",
            padding: "8px",
            marginBottom: "16px",
            borderRadius: "4px",
            border: "1px solid #ccc",
            outline: "none",
          }}
        &amp;gt;
          &amp;lt;option value="1920"&amp;gt;1920 (Full HD)&amp;lt;/option&amp;gt;
          &amp;lt;option value="1366"&amp;gt;1366 (Laptop)&amp;lt;/option&amp;gt;
          &amp;lt;option value="1280"&amp;gt;1280 (Desktop)&amp;lt;/option&amp;gt;
          &amp;lt;option value="1024"&amp;gt;1024 (Tablet Landscape)&amp;lt;/option&amp;gt;
          &amp;lt;option value="768"&amp;gt;768 (Tablet Portrait)&amp;lt;/option&amp;gt;
          &amp;lt;option value="375"&amp;gt;375 (Mobile)&amp;lt;/option&amp;gt;
        &amp;lt;/select&amp;gt;

        &amp;lt;label
          style={{ marginBottom: "8px", fontWeight: "500" }}
          htmlFor="height"
        &amp;gt;
          Height (px)
        &amp;lt;/label&amp;gt;
        &amp;lt;input
          type="number"
          id="height"
          value={height}
          onChange={(e) =&amp;gt; setHeight(e.target.value)}
          required
          style={{
            width: "100%",
            padding: "8px",
            marginBottom: "16px",
            borderRadius: "4px",
            border: "1px solid #ccc",
            outline: "none",
          }}
        /&amp;gt;

        &amp;lt;label
          style={{ marginBottom: "8px", fontWeight: "500" }}
          htmlFor="delay"
        &amp;gt;
          Delay (ms)
        &amp;lt;/label&amp;gt;
        &amp;lt;input
          type="number"
          id="delay"
          value={delay}
          onChange={(e) =&amp;gt; setDelay(e.target.value)}
          required
          style={{
            width: "100%",
            padding: "8px",
            marginBottom: "16px",
            borderRadius: "4px",
            border: "1px solid #ccc",
            outline: "none",
          }}
        /&amp;gt;

        &amp;lt;button
          type="submit"
          disabled={isProcessing}
          style={{
            padding: "8px 16px",
            color: "white",
            borderRadius: "4px",
            transition: "background-color 0.3s",
            backgroundColor: isProcessing ? "#b0bec5" : "#2196F3",
            cursor: isProcessing ? "not-allowed" : "pointer",
          }}
        &amp;gt;
          {isProcessing ? "Capturing..." : "Capture Screenshot"}
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;

      {/* Example HTML Element to Capture */}
      &amp;lt;div id="capture-area" style={{ display: "none" }}&amp;gt;
        &amp;lt;h3
          style={{
            fontSize: "20px",
            fontWeight: "600",
          }}
        &amp;gt;
          Content to Capture
        &amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;This is an example of the HTML content that will be captured.&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This tutorial covers setting up a webpage capture tool in Next.js, handling screenshots with &lt;code&gt;Puppeteer&lt;/code&gt;, and creating an interactive frontend component. Remember to use &lt;code&gt;puppeteer&lt;/code&gt; locally and switch to &lt;code&gt;puppeteer-core&lt;/code&gt; in production to reduce bundle size and optimize for serverless environments. Happy coding!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to fix “require is not defined” in JavaScript / Node.js? How can we run HTML file on server?</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Thu, 28 Mar 2024 13:14:22 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/how-to-fix-require-is-not-defined-in-javascript-nodejs-how-can-we-run-html-file-on-server-1pga</link>
      <guid>https://dev.to/ahmadfarazcrypto/how-to-fix-require-is-not-defined-in-javascript-nodejs-how-can-we-run-html-file-on-server-1pga</guid>
      <description>&lt;p&gt;When we are working with plain javaScript, Sometimes we are getting this error on &lt;code&gt;require()&lt;/code&gt; and we are getting the &lt;strong&gt;“ReferenceError: require is not defined”&lt;/strong&gt; error in the browser environment because the require() method is supported in browsers.&lt;/p&gt;

&lt;p&gt;There are already multiple solutions on stackoverflow and also we have multiple articles related to that, we can use &lt;code&gt;type="module"&lt;/code&gt; in &lt;code&gt;&amp;lt;script type="module" /&amp;gt;&lt;/code&gt; then we can use &lt;code&gt;import&lt;/code&gt; and &lt;code&gt;export&lt;/code&gt; in JavaScript, like you can see &lt;a href="https://stackoverflow.com/questions/19059580/browser-uncaught-referenceerror-require-is-not-defined" rel="noopener noreferrer"&gt;stackoverflow here&lt;/a&gt; multiple solutions.&lt;/p&gt;

&lt;p&gt;but after that if we get another error like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access to script at 'file:///Users/ahmadfaraz/Documents/app.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This error is coming because we are running the HTML file locally and if you are using VS Code IDE there is a &lt;a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer" rel="noopener noreferrer"&gt;HTML live server&lt;/a&gt; has an extension for I'm not sure if this could help you or not. But this is my solution how you can solve this above error.&lt;/p&gt;

&lt;p&gt;If we run HTML file on server that error can be solved, let's see how can we run HTML file on local server.&lt;/p&gt;

&lt;p&gt;Note: Make sure you must have installed &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;node.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;create file &lt;code&gt;package.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then run this command &lt;a href="https://www.npmjs.com/package/http-server" rel="noopener noreferrer"&gt;http-server&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i http-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you will see in &lt;code&gt;package.json&lt;/code&gt; that you have installed the http-server library.&lt;/p&gt;

&lt;p&gt;create file &lt;code&gt;src/data.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const DATA = [
    {
        "id": "1",
        "name": "Ahmad"
    }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;create file &lt;code&gt;src/index.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {DATA} from './data.js'
console.log(DATA)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;create file &lt;code&gt;src/index.html&lt;/code&gt;&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;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset="UTF-8"&amp;gt;
    &amp;lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;script type="module" src="./index.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;update file &lt;code&gt;package.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "scripts": {
    "start": "npx http-server src/"
  },
  "dependencies": {
    "http-server": "^14.1.1"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fl7s25v9g6w2wmx74wmmm.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.amazonaws.com%2Fuploads%2Farticles%2Fl7s25v9g6w2wmx74wmmm.png" alt="Server start" width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Error should resolved now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/jXKc88W60QQ" rel="noopener noreferrer"&gt;DEMO VIDEO&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>beginners</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>NextJs 14 with antd 5.11</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Tue, 21 Nov 2023 13:51:01 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/nextjs-14-with-antd-511-11h9</link>
      <guid>https://dev.to/ahmadfarazcrypto/nextjs-14-with-antd-511-11h9</guid>
      <description>&lt;p&gt;Hello everyone,&lt;/p&gt;

&lt;p&gt;Today I will guide you how we can implement antd in NextJs 14.&lt;/p&gt;

&lt;p&gt;Recently I was working on a client project, there we are using NextJs 12 version and antd 4.20.2 version, I got a task, I have to update our NextJs version to 14, because our web app NextJs 12 was very slow and we have to upgrade the version.&lt;/p&gt;

&lt;p&gt;There is a lot of fantastic updates in NextJs 14, you can see on this &lt;a href="https://nextjs.org/blog/next-14" rel="noopener noreferrer"&gt;link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First I have a problem with antd because Ant Design's CSS is loading after the page is rendered, causing a flash of unstyled content in next Js 14. I searched a lot of articles but couldn't find anything right related to this.&lt;/p&gt;

&lt;p&gt;I saw few updates in antd on &lt;a href="https://ant.design/docs/react/use-with-next" rel="noopener noreferrer"&gt;antd documentation&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Here is the steps, how we can implement antd in our NextJs 14 app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx create-next-app@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fjesd21x3vawt7b6u29t9.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.amazonaws.com%2Fuploads%2Farticles%2Fjesd21x3vawt7b6u29t9.png" alt=" " width="800" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install antd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @ant-design/cssinjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;lib/AntdRegistry.tsx&lt;/code&gt; in your src directory&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'use client';

import React from 'react';
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import type Entity from '@ant-design/cssinjs/es/Cache';
import { useServerInsertedHTML } from 'next/navigation';

const StyledComponentsRegistry = ({ children }: React.PropsWithChildren) =&amp;gt; {
  const cache = React.useMemo&amp;lt;Entity&amp;gt;(() =&amp;gt; createCache(), []);
  const isServerInserted = React.useRef&amp;lt;boolean&amp;gt;(false);
  useServerInsertedHTML(() =&amp;gt; {
    // avoid duplicate css insert
    if (isServerInserted.current) {
      return;
    }
    isServerInserted.current = true;
    return &amp;lt;style id="antd" dangerouslySetInnerHTML={{ __html: extractStyle(cache, true) }} /&amp;gt;;
  });
  return &amp;lt;StyleProvider cache={cache}&amp;gt;{children}&amp;lt;/StyleProvider&amp;gt;;
};

export default StyledComponentsRegistry;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Update your &lt;code&gt;layout.tsx&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React from 'react';
import { Inter } from 'next/font/google';

import StyledComponentsRegistry from '../lib/AntdRegistry';

import '@/globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

const RootLayout = ({ children }: React.PropsWithChildren) =&amp;gt; (
  &amp;lt;html lang="en"&amp;gt;
    &amp;lt;body className={inter.className}&amp;gt;
      &amp;lt;StyledComponentsRegistry&amp;gt;{children}&amp;lt;/StyledComponentsRegistry&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
);

export default RootLayout;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NextJs documentation: &lt;a href="https://nextjs.org/docs/app/building-your-application/routing" rel="noopener noreferrer"&gt;https://nextjs.org/docs/app/building-your-application/routing&lt;/a&gt;&lt;br&gt;
Github repo: &lt;a href="https://github.com/AhmadFaraz-crypto/NextJs-14-with-antd-5.11" rel="noopener noreferrer"&gt;https://github.com/AhmadFaraz-crypto/NextJs-14-with-antd-5.11&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>antd</category>
      <category>beginners</category>
    </item>
    <item>
      <title>ERPNext-installation-Guide-Mac</title>
      <dc:creator>Ahmad Faraz</dc:creator>
      <pubDate>Fri, 21 Oct 2022 11:19:21 +0000</pubDate>
      <link>https://dev.to/ahmadfarazcrypto/erpnext-installation-guide-mac-1p79</link>
      <guid>https://dev.to/ahmadfarazcrypto/erpnext-installation-guide-mac-1p79</guid>
      <description>&lt;p&gt;&lt;strong&gt;The complete guide to install ERPNext in your Mac system&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Pre-requisites&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python 3.6+
Homebrew 3.6+
Node.js 14
Redis 5                                       (caching and real time updates)
MariaDB                                       (to run database driven apps)
yarn 1.12+                                    (js dependency manager)
pip 20+                                       (py dependency manager)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 1 Install brew&lt;/strong&gt;&lt;br&gt;
Homebrew is an app for macOS that allows you to install a huge variety of UNIX software on your Mac with one simple command (on the command line).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 2 Install git&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Git is the most commonly used version control system. Git tracks the changes you make to files, so you have a record of what has been done, and you can revert to specific versions should you ever need to. Git also makes collaboration easier, allowing changes by multiple people to all be merged into one source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 3 Install setuptools and pip (Python's Package Manager).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Setuptools is a collection of enhancements to the Python distutils that allow developers to more easily build and distribute Python packages, especially ones that have dependencies on other packages. Packages built and distributed using setuptools look to the user like ordinary Python packages based on the distutils.&lt;/p&gt;

&lt;p&gt;pip is a package manager for Python. It's a tool that allows you to install and manage additional libraries and dependencies that are not distributed as part of the standard library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo pip3 install setuptools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 4 Install virtualenv&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;virtualenv is a tool for creating isolated Python environments containing their own copy of python , pip , and their own place to keep libraries installed from PyPI. It's designed to allow you to work on multiple projects with different dependencies at the same time on the same machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo pip3 install virtualenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 5 Install MariaDB&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MariaDB is developed as open source software and as a relational database it provides an SQL interface for accessing data.&lt;/p&gt;

&lt;p&gt;open this link&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mariadb.com/resources/blog/installing-mariadb-10-1-16-on-mac-os-x-with-homebrew/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Mariadb&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
brew install mariadb
mysql_install_db
mariadb-secure-installation
mariadb -u root -p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IMPORTANT :During this installation you'll be prompted to set the MySQL root password. If you are not prompted for the same You can initialize the MySQL server setup by executing the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 6 MySQL database development files&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install mysql-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 7 Edit the mariadb configuration ( unicode character encoding )&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /opt/homebrew/mariadb/my.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;add this to the my.cnf file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

[mysql]
default-character-set = utf8mb4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now press (Ctrl-X) to exit&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew services start mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 8 install Redis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 9 install Node.js 14.X package&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Node.js is an open source, cross-platform runtime environment for developing server-side and networking applications. Node.js applications are written in JavaScript, and can be run within the Node.js runtime on OS X, Microsoft Windows, and Linux.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will be install latest version of Node.&lt;/p&gt;

&lt;p&gt;If you want to change the node version you can use NVM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://tecadmin.net/install-nvm-macos-with-homebrew/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 10 install Yarn&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yarn is a JavaScript package manager that aims to be speedy, deterministic, and secure. See how easy it is to drop yarn in where you were using npm before, and get faster, more reliable installs. Yarn is a package manager for JavaScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo npm install -g yarn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 11 install wkhtmltopdf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wkhtmltopdf is an open source simple and much effective command-line shell utility that enables user to convert any given HTML (Web Page) to PDF document or an image (jpg, png, etc)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install wkhtmltopdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 12 install frappe-bench&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo -H pip3 install frappe-bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IMPORTANT: you may wish to log out and log back into your terminal before next step and You must login.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bench --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 13 initilise the frappe bench &amp;amp; install frappe latest version&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bench init frappe-bench --frappe-branch version-13

cd frappe-bench/
bench start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STEP 14 create a site in frappe bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bench new-site site1.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NOTE: If you face this type of issue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For key collation_server. Expected value utf8mb4_unicode_ci, found value utf8mb4_general_ci
================================================================================
Creation of your site - site11.locale failed because MariaDB is not properly
configured. If using version 10.2.x or earlier, make sure you use the
the Barracuda storage engine.
Please verify the settings above in MariaDB's my.cnf. Restart MariaDB. And
then run `bench new-site site1.locale` again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then you have to go in mariadb shell and run this commands&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET GLOBAL collation_server = 'utf8mb4_unicode_ci';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and restart the mariadb&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew services restart mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 15 install ERPNext latest version in bench &amp;amp; site&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bench get-app erpnext --branch version-13
###OR
bench get-app https://github.com/frappe/erpnext --branch version-13

bench --site site1.local install-app erpnext

bench --site site1.local add-to-hosts

bench start

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server will run on this host&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://site1.local:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>python</category>
    </item>
  </channel>
</rss>
