<?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: Shahzaib</title>
    <description>The latest articles on DEV Community by Shahzaib (@shahzaib11).</description>
    <link>https://dev.to/shahzaib11</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4171589%2Ffaa56799-77a3-456e-9b5f-594cbbadb8c2.png</url>
      <title>DEV Community: Shahzaib</title>
      <link>https://dev.to/shahzaib11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shahzaib11"/>
    <language>en</language>
    <item>
      <title>5 PDF Tools Nobody Else Gives You for Free (All Running 100% in Your Browser)</title>
      <dc:creator>Shahzaib</dc:creator>
      <pubDate>Fri, 09 Oct 2026 18:22:36 +0000</pubDate>
      <link>https://dev.to/shahzaib11/5-pdf-tools-nobody-else-gives-you-for-free-all-running-100-in-your-browser-32hf</link>
      <guid>https://dev.to/shahzaib11/5-pdf-tools-nobody-else-gives-you-for-free-all-running-100-in-your-browser-32hf</guid>
      <description>&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; Building PdfWord — a free, no-backend PDF tools site (Part 6)&lt;/p&gt;

&lt;p&gt;Most free PDF sites give you the same 10 tools: merge, split, compress, convert. I have those too. But this week I shipped &lt;strong&gt;5 tools I couldn't find on any free PDF site&lt;/strong&gt; — all running entirely in the browser, no uploads, no accounts.&lt;/p&gt;

&lt;p&gt;Here's what they are and how they work.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Visual PDF Compare — differences glow red
&lt;/h2&gt;

&lt;p&gt;Upload two PDFs (original + revised). Every page pair is rendered and pixel-compared; differences glow &lt;strong&gt;red&lt;/strong&gt;, identical content fades. You get a similarity score per page plus a downloadable diff report.&lt;/p&gt;

&lt;p&gt;The trick: render both pages to canvas with pdf.js at the same scale, walk the pixels, flag any with channel difference &amp;gt; 32 (ignores anti-aliasing noise), and paint a diff visualization. ~40 lines of JS. Adobe charges for this; mine's free.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Listen to PDF — your documents read aloud
&lt;/h2&gt;

&lt;p&gt;Upload a PDF, and the browser's built-in speech synthesis reads it to you — with voice picker and speed control. Zero libraries, zero cost: it's just &lt;code&gt;speechSynthesis&lt;/code&gt; + pdf.js text extraction, chunked at sentence boundaries so pause/resume works naturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Split by Size — beat Gmail's 25MB limit
&lt;/h2&gt;

&lt;p&gt;Nobody does this: pick a max part size (10/25/50/100 MB) and the tool splits your PDF into chunks &lt;strong&gt;guaranteed&lt;/strong&gt; under the limit. It adds pages one by one with pdf-lib and byte-measures every part — no estimation, no overshoot.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Grayscale PDF — save your color ink
&lt;/h2&gt;

&lt;p&gt;One click turns every page black &amp;amp; white for cheap printing. Rendered via canvas &lt;code&gt;grayscale(1)&lt;/code&gt; filter, re-embedded at high resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Scanned PDF to Word (OCR) — the hard one
&lt;/h2&gt;

&lt;p&gt;This took real work: Tesseract.js running &lt;strong&gt;in the browser&lt;/strong&gt; via WebAssembly. Scanned pages are rendered to images, OCR'd page by page, and assembled into an editable &lt;code&gt;.docx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The gotcha that cost me 3 failed QA runs: Tesseract.js v5 requests &lt;code&gt;eng.traineddata.gz&lt;/code&gt; by default — a file that doesn't exist unless you create it. The fix was embarrassingly simple: ship both &lt;code&gt;eng.traineddata&lt;/code&gt; and the &lt;code&gt;.gz&lt;/code&gt; version. The worker gunzips transparently.&lt;/p&gt;

&lt;p&gt;And the privacy angle no other free OCR site can claim: &lt;strong&gt;your scans never leave your device.&lt;/strong&gt; Every other free OCR uploads your documents to their servers. Mine can't — there is no server.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Every one of these follows the same playbook: find a task people assume needs a backend, then prove it doesn't. pdf.js renders, pdf-lib edits, Tesseract reads, speechSynthesis speaks, JSZip bundles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try them:&lt;/strong&gt; &lt;a href="https://pdfwordtools.pages.dev" rel="noopener noreferrer"&gt;PdfWord&lt;/a&gt; — 34 free tools, no signup, no watermark.&lt;/p&gt;

&lt;p&gt;What impossible-in-browser tool should I build next?&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Made My Web App Receive Files From WhatsApp — No Backend, Just Web APIs</title>
      <dc:creator>Shahzaib</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:43:48 +0000</pubDate>
      <link>https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md</link>
      <guid>https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md</guid>
      <description>&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; Building PdfWord — a free, no-backend PDF tools site (Part 5)&lt;/p&gt;

&lt;p&gt;My PDF tools site is a PWA — installable, works offline. But last night I asked myself: &lt;em&gt;am I actually using the "app" part, or is it just a bookmark with a nice icon?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I upgraded it with three real platform integrations. All client-side. Zero backend. Here's how.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Share Target: receive PDFs from WhatsApp
&lt;/h2&gt;

&lt;p&gt;This is the big one. On Android, when you share a PDF from WhatsApp, Gmail, or Files, &lt;strong&gt;PdfWord now appears in the share sheet&lt;/strong&gt; — and the file opens pre-loaded in the app.&lt;/p&gt;

&lt;p&gt;It takes two pieces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The manifest&lt;/strong&gt; declares what you accept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"share_target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/share.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enctype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multipart/form-data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pdfs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"application/pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".pdf"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The service worker&lt;/strong&gt; intercepts the POST (a static host can't parse multipart forms, but the SW can):&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&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="o"&gt;&amp;amp;&amp;amp;&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;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/share.html&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&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;fd&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfs&lt;/span&gt;&lt;span class="dl"&gt;'&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;size&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="k"&gt;if &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="nx"&gt;length&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;cache&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;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfword-shared-v1&lt;/span&gt;&lt;span class="dl"&gt;'&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared-0&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;Response&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Filename&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&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="nx"&gt;name&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/share.html?shared=1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;303&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;share.html&lt;/code&gt; reads the file from the cache, shows it, and offers tool buttons (Compress, Merge, PDF to Word…). Clicking one stashes the file in IndexedDB and navigates to the tool page, where a small script in my shared &lt;code&gt;common.js&lt;/code&gt; injects it into the file input via &lt;code&gt;DataTransfer&lt;/code&gt; — so the tool's existing upload handler just works, no per-tool changes needed.&lt;/p&gt;

&lt;p&gt;The whole flow feels native. The user never picks a file twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. App shortcuts: long-press the icon
&lt;/h2&gt;

&lt;p&gt;Three quick actions in the manifest — Merge PDF, Compress PDF, PDF to Word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"shortcuts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Merge PDF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/merge-pdf.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assets/icons/icon-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long-press the installed app icon and jump straight to the tool. Five minutes of work, genuinely useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. iOS install hint (because Apple won't give us the prompt)
&lt;/h2&gt;

&lt;p&gt;iOS Safari doesn't fire &lt;code&gt;beforeinstallprompt&lt;/code&gt;, so my install button did nothing there. Now iOS users get the button too — tapping it shows: &lt;em&gt;"Tap Share ⬆️ then 'Add to Home Screen' to install PdfWord."&lt;/em&gt; Small, honest, better than a dead button.&lt;/p&gt;

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

&lt;p&gt;A PWA isn't an "install button." It's the moment your web app starts behaving like it belongs on the device — receiving shares, offering shortcuts, working offline. Every one of these is a web standard, no app store, no review process, no backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; install &lt;a href="https://pdfwordtools.pages.dev" rel="noopener noreferrer"&gt;PdfWord&lt;/a&gt; on Android, share any PDF from WhatsApp to it, and watch it open ready to compress. That's the demo I'm proudest of.&lt;/p&gt;

&lt;p&gt;What platform API should I wire up next? File Handling? Protocol handlers? Tell me in the comments.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Shipped 2 New Features Without a Backend: PDF to PNG + Flatten PDF</title>
      <dc:creator>Shahzaib</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:11:13 +0000</pubDate>
      <link>https://dev.to/shahzaib11/how-i-shipped-2-new-features-without-a-backend-pdf-to-png-flatten-pdf-5c4b</link>
      <guid>https://dev.to/shahzaib11/how-i-shipped-2-new-features-without-a-backend-pdf-to-png-flatten-pdf-5c4b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; Building PdfWord — a free, no-backend PDF tools site (Part 4)&lt;/p&gt;

&lt;p&gt;Last week I cut my homepage from 4.8MB to ~70KB and made my PWA open instantly (Part 2). This week I shipped &lt;strong&gt;two brand-new tools&lt;/strong&gt; — taking the site from 27 to &lt;strong&gt;29 free tools&lt;/strong&gt; — still with zero backend, zero database, and zero server bills.&lt;/p&gt;

&lt;p&gt;The tools: &lt;strong&gt;PDF to PNG&lt;/strong&gt; (lossless page images) and &lt;strong&gt;Flatten PDF&lt;/strong&gt; (lock form fields permanently).&lt;/p&gt;

&lt;p&gt;Here's exactly how they work in the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. PDF to PNG: pdf.js + canvas + JSZip
&lt;/h2&gt;

&lt;p&gt;The trick is the same one I used for PDF to JPG: render each page to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; with pdf.js, then &lt;code&gt;canvas.toBlob()&lt;/code&gt; — but with the &lt;code&gt;image/png&lt;/code&gt; MIME type instead of &lt;code&gt;image/jpeg&lt;/code&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;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;viewport&lt;/span&gt; &lt;span class="o"&gt;=&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;getViewport&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="c1"&gt;// 2x for crisp text&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;viewport&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;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;viewport&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="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;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;canvasContext&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;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;viewport&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;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;await&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;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&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;-page-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&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="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything runs on the main thread (canvas needs the DOM), with a progress bar per page and &lt;code&gt;page.cleanup()&lt;/code&gt; to free memory between pages — important for 100+ page PDFs on phones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why PNG and not just JPG?&lt;/strong&gt; PNG is lossless. For text documents, screenshots, and graphics, JPG's compression artifacts make text fuzzy. Users kept asking — so now they have both.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Flatten PDF: pdf-lib's &lt;code&gt;form.flatten()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This one's a one-liner with &lt;a href="https://pdf-lib.js.org/" rel="noopener noreferrer"&gt;pdf-lib&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;const&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="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PDFLib&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;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;buffer&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;form&lt;/span&gt; &lt;span class="o"&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;getForm&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;flatten&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// burns every field into the page&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;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* no form fields — still saves a clean copy */&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;out&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;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flattening is the thing lawyers and HR departments do before emailing contracts: the filled values stay visible but &lt;strong&gt;can never be edited or cleared&lt;/strong&gt;. Before this, my users had to print-to-PDF and lose quality. Now it's one click, in the browser.&lt;/p&gt;

&lt;p&gt;The whole tool is ~25 lines of JS. The page, SEO copy, and FAQ took longer than the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The playbook so far (all free, all client-side)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Ship real tools that solve one problem well&lt;/li&gt;
&lt;li&gt;Give every tool its own page: SEO title, how-to steps, FAQ with JSON-LD schema&lt;/li&gt;
&lt;li&gt;Add it to the homepage grid + sitemap, ping IndexNow&lt;/li&gt;
&lt;li&gt;Write about it here on dev.to — the articles are my #1 traffic source after directories&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Try them:&lt;/strong&gt; &lt;a href="https://pdfwordtools.pages.dev/pdf-to-png" rel="noopener noreferrer"&gt;PDF to PNG&lt;/a&gt; · &lt;a href="https://pdfwordtools.pages.dev/flatten-pdf" rel="noopener noreferrer"&gt;Flatten PDF&lt;/a&gt; — no signup, no watermark, files never leave your browser.&lt;/p&gt;

&lt;p&gt;What tool should I build next? Drop a comment — I read every one.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Fill PDF Forms in the Browser with pdf-lib (No Server Needed)</title>
      <dc:creator>Shahzaib</dc:creator>
      <pubDate>Fri, 09 Oct 2026 11:23:35 +0000</pubDate>
      <link>https://dev.to/shahzaib11/how-to-fill-pdf-forms-in-the-browser-with-pdf-lib-no-server-needed-3icg</link>
      <guid>https://dev.to/shahzaib11/how-to-fill-pdf-forms-in-the-browser-with-pdf-lib-no-server-needed-3icg</guid>
      <description>&lt;p&gt;I recently added a "Fill PDF Form" tool to my free PDF toolkit &lt;a href="https://pdfwordtools.pages.dev/fill-pdf-form" rel="noopener noreferrer"&gt;PdfWord&lt;/a&gt;. Everything runs client-side — the PDF never leaves the user's device. Here's how it works, in case you want to build something similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading form fields
&lt;/h2&gt;

&lt;p&gt;pdf-lib exposes AcroForm fields directly:&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfDoc&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;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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pdfDoc&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="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nf"&gt;getName&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="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="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// PDFTextField, PDFCheckBox, PDFDropdown, PDFRadioGroup, PDFButton&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Filling them
&lt;/h2&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;textField&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;getTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full_name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;textField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shahzaib&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;checkBox&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;getCheckBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agree&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;checkBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&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;dropdown&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;getDropdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;dropdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pakistan&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;h2&gt;
  
  
  The minification gotcha
&lt;/h2&gt;

&lt;p&gt;In production builds, &lt;code&gt;field.constructor.name&lt;/code&gt; becomes something like &lt;code&gt;e&lt;/code&gt; or &lt;code&gt;t&lt;/code&gt; — minifiers rename classes. My first version used &lt;code&gt;constructor.name&lt;/code&gt; checks and silently failed on the live site while working in dev.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use &lt;code&gt;instanceof&lt;/code&gt; checks against the pdf-lib classes instead:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFTextField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDFCheckBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDFDropdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDFRadioGroup&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PDFTextField&lt;/span&gt;&lt;span class="p"&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="k"&gt;else&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;field&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PDFCheckBox&lt;/span&gt;&lt;span class="p"&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="c1"&gt;// ... etc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;instanceof&lt;/code&gt; survives minification. &lt;code&gt;constructor.name&lt;/code&gt; doesn't. This one burned me in production — test your built bundle, not just dev.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flattening (optional)
&lt;/h2&gt;

&lt;p&gt;If you want the filled values burned into the page so they can't be edited:&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&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;bytes&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;pdfDoc&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Updating appearances
&lt;/h2&gt;

&lt;p&gt;One more gotcha: after setting values programmatically, call &lt;code&gt;form.updateFieldAppearances()&lt;/code&gt; (or the default font embedding pdf-lib does) so the filled text actually renders in viewers. Without it, some PDF readers show blank fields despite the values being set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side?
&lt;/h2&gt;

&lt;p&gt;For a privacy-sensitive feature like form filling (think job applications, tax forms, medical paperwork), "your file never leaves your device" isn't just a technical detail — it's the whole selling point. pdf-lib makes it genuinely feasible: parse, fill, flatten, download, all in the browser.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PdfWord is a free, no-signup PDF toolkit — 27 tools, installable as a PWA, works offline. The fill-form tool and this writeup both came out of building it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Cut My Homepage from 4.8MB to 70KB (and Made My PWA Open Instantly)</title>
      <dc:creator>Shahzaib</dc:creator>
      <pubDate>Fri, 09 Oct 2026 10:50:39 +0000</pubDate>
      <link>https://dev.to/shahzaib11/how-i-cut-my-homepage-from-48mb-to-70kb-and-made-my-pwa-open-instantly-3673</link>
      <guid>https://dev.to/shahzaib11/how-i-cut-my-homepage-from-48mb-to-70kb-and-made-my-pwa-open-instantly-3673</guid>
      <description>&lt;p&gt;My free PDF tools site &lt;a href="https://pdfwordtools.pages.dev" rel="noopener noreferrer"&gt;PdfWord&lt;/a&gt; had a problem: the installed PWA took forever to open on phones. Users complained. Here's the 20-minute fix that cut &lt;strong&gt;98.5%&lt;/strong&gt; of the weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis
&lt;/h2&gt;

&lt;p&gt;I measured what the homepage actually downloads:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;html2pdf.bundle.min.js&lt;/td&gt;
&lt;td&gt;884 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xlsx.full.min.js&lt;/td&gt;
&lt;td&gt;861 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docx.min.js&lt;/td&gt;
&lt;td&gt;725 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdf-lib.min.js&lt;/td&gt;
&lt;td&gt;512 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pptxgen.bundle.js&lt;/td&gt;
&lt;td&gt;466 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jspdf.umd.min.js&lt;/td&gt;
&lt;td&gt;410 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdf.min.js&lt;/td&gt;
&lt;td&gt;312 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;…plus 25 more scripts&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total: 4.8 MB of JavaScript — on a landing page.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The homepage is just cards that link to tool pages. It needed &lt;em&gt;none&lt;/em&gt; of these. Every library was loaded "just in case," plus 22 dead inline tool panels from an older design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deleted the 22 dead tool panels&lt;/strong&gt; from the homepage (dedicated tool pages already existed — 25 KB of dead HTML gone).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removed all 33 heavy &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags&lt;/strong&gt; from the homepage. Each tool page still loads only the libraries it needs (selective per-page loading).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kept one file:&lt;/strong&gt; &lt;code&gt;common.js&lt;/code&gt; (15 KB) for navigation, theme toggle, and filters.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: &lt;strong&gt;54 KB HTML + 15 KB JS = ~70 KB total.&lt;/strong&gt; Down from 4.8 MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The service worker lesson
&lt;/h2&gt;

&lt;p&gt;I also found the service worker was precaching ~2 MB of libraries on install. Trimmed the precache to the app shell only (~120 KB); heavy libs now cache on-demand when their tool page is first opened. And HTML uses a network-first strategy so updates reach installed apps without reinstalls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule I now follow:&lt;/strong&gt; bump the SW cache version on every deploy, or installed apps serve stale files forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Page load: &lt;strong&gt;422 ms&lt;/strong&gt; (Cloudflare Web Analytics, real devices)&lt;/li&gt;
&lt;li&gt;Core Web Vitals: &lt;strong&gt;100% Good&lt;/strong&gt; across LCP, INP, CLS&lt;/li&gt;
&lt;li&gt;PWA opens instantly on 4G&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The takeaway: audit what your landing page &lt;em&gt;actually&lt;/em&gt; needs. "Just in case" script tags are the silent killer of mobile performance.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PdfWord is a free, no-signup, client-side PDF toolkit — 27 tools, installable as a PWA, works offline. Built with vanilla JS and pdf-lib.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>javascript</category>
      <category>pwa</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a free iLovePDF alternative with zero backend</title>
      <dc:creator>Shahzaib</dc:creator>
      <pubDate>Thu, 08 Oct 2026 14:58:29 +0000</pubDate>
      <link>https://dev.to/shahzaib11/i-built-a-free-ilovepdf-alternative-with-zero-backend-30j8</link>
      <guid>https://dev.to/shahzaib11/i-built-a-free-ilovepdf-alternative-with-zero-backend-30j8</guid>
      <description>&lt;p&gt;I kept hitting "free limit reached" on PDF tools while job hunting, so I built my own: &lt;a href="https://pdfwordtools.pages.dev" rel="noopener noreferrer"&gt;PdfWord&lt;/a&gt; — 22 PDF tools, 100% free, no signup, no watermarks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The interesting part: there is no backend.&lt;/strong&gt; Everything runs client-side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pdf-lib&lt;/strong&gt; for merge/split/compress/rotate/watermark — runs in a Web Worker so the UI never freezes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdf.js&lt;/strong&gt; for rendering and text extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero server, zero database, zero tracking&lt;/strong&gt; — files never leave your device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture has a beautiful side effect: &lt;strong&gt;no server costs means it can genuinely stay free forever.&lt;/strong&gt; Most "free" PDF sites need paywalls because they pay for server processing per file. No servers → no per-file cost → no limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance lesson:&lt;/strong&gt; my first version loaded every JS library on every page — 4.6MB, 4.1s load on 4G. I switched to selective per-page loading (each tool page only loads what it needs). Now pages are 40–500KB loading in ~0.9s. Same tools, 100x lighter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO lesson:&lt;/strong&gt; I gave every tool its own page (&lt;code&gt;/merge-pdf&lt;/code&gt;, &lt;code&gt;/compress-pdf&lt;/code&gt;...) instead of one SPA — 22 indexable pages plus 8 how-to guides, all static.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest limitations:&lt;/strong&gt; no OCR, no password unlock, no PDF/A — those need server-side processing or heavy models. I left them out rather than faking them.&lt;/p&gt;

&lt;p&gt;Stack: static HTML + vanilla JS, hosted on Cloudflare Pages (free). Total hosting cost: $0.&lt;/p&gt;

&lt;p&gt;It's my first launched project (I'm actually an electrical engineer, not a web dev). Roast it — what's missing?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>pdf</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
