<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[WebsiteGeek Base]]></title><description><![CDATA[WebsiteGeek Base]]></description><link>https://websitegeek.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a8e512594ab8869936424d5/c527e339-2015-4001-9af5-0d5364aef809.png</url><title>WebsiteGeek Base</title><link>https://websitegeek.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 31 Aug 2026 04:43:20 GMT</lastBuildDate><atom:link href="https://websitegeek.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Embedding a Hand-Drawn Signature Into a PDF, Entirely in the Browser]]></title><description><![CDATA[E-signing a PDF sounds like it should be simple: let someone draw a signature, stick it on the page, done. The actual implementation has one genuinely tricky part that has nothing to do with drawing —]]></description><link>https://websitegeek.hashnode.dev/embedding-a-hand-drawn-signature-into-a-pdf-entirely-in-the-browser</link><guid isPermaLink="true">https://websitegeek.hashnode.dev/embedding-a-hand-drawn-signature-into-a-pdf-entirely-in-the-browser</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[pdf]]></category><category><![CDATA[chrome extension]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[WebsiteGeek]]></dc:creator><pubDate>Thu, 27 Aug 2026 02:57:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8e512594ab8869936424d5/db8ffa85-0a2c-4f5c-8dbd-ac3d0de68d17.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>E-signing a PDF sounds like it should be simple: let someone draw a signature, stick it on the page, done. The actual implementation has one genuinely tricky part that has nothing to do with drawing — it's reconciling two coordinate systems that don't agree with each other, and getting that wrong silently places the signature in the wrong spot instead of throwing an error.</p>
<p>Here's how <a href="https://websitegeek.net/deskramp/">DeskRamp</a>'s e-Sign feature actually works, entirely client-side, no server involved.</p>
<h2>Step one: capture the signature</h2>
<p>The drawing surface is a plain HTML <code>&lt;canvas&gt;</code>, tracking pointer events to build a path:</p>
<pre><code class="language-plaintext">canvas.addEventListener('pointermove', (e) =&gt; {
  if (!isDrawing) return;
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
});
</code></pre>
<p>Once the user's done, the canvas exports as a PNG via <code>canvas.toBlob()</code> — this part is unremarkable, it's the same pattern every signature-pad library uses.</p>
<h2>Step two: the coordinate system mismatch</h2>
<p>This is the part that isn't obvious until it bites you. Canvas coordinates start at the <strong>top-left</strong>: <code>(0, 0)</code> is the top-left corner, and Y increases downward. PDF page coordinates start at the <strong>bottom-left</strong>: <code>(0, 0)</code> is the bottom-left corner, and Y increases upward.</p>
<p>If you take the canvas coordinates where a user placed their signature and hand them directly to a PDF library's <code>drawImage(x, y)</code> call, the signature lands in the mirror-image vertical position — a signature placed near the bottom of the visible page ends up drawn near the top of the actual PDF page. Nothing throws an error. It just silently looks wrong, which is worse than a crash because it's easy to miss in a quick test with a signature placed near the vertical center, where the visual difference is smaller.</p>
<p>The fix is a straightforward flip, but you have to remember to do it:</p>
<pre><code class="language-plaintext">const pdfY = pageHeight - canvasY - signatureHeight;
</code></pre>
<p><code>pageHeight</code> comes from the actual PDF page's dimensions (<code>page.getHeight()</code> in pdf-lib), not the canvas's — the canvas is just a UI surface at whatever CSS size it's rendered, while the PDF page has its own coordinate space in points, and the two aren't the same scale either. Both the flip and the scale factor have to account for the ratio between the canvas's pixel dimensions and the PDF page's point dimensions, or the signature places correctly on one but drifts on the other as page sizes vary.</p>
<h2>Step three: embedding, not just drawing</h2>
<p>The signature PNG gets embedded as an actual image object in the PDF, not composited onto a rasterized version of the page:</p>
<pre><code class="language-plaintext">const pngImage = await pdfDoc.embedPng(signatureBytes);
page.drawImage(pngImage, {
  x: placementX,
  y: pdfY,
  width: scaledWidth,
  height: scaledHeight,
});
</code></pre>
<p>This matters for output quality — embedding preserves the rest of the page as real vector/text content instead of flattening the whole document into an image, which is what you'd get if you rendered the PDF to a canvas, drew the signature on top, and re-exported the canvas as a new PDF. That approach is simpler to reason about but produces a much larger file and loses text selectability on every page, not just the signed one.</p>
<h2>Why none of this needs a server</h2>
<p>Every step here — canvas capture, the coordinate math, the embed call — runs on data that's already local: the PDF file the user opened, and the signature they just drew. There's no reason any of it needs to leave the browser tab, which is the same principle behind the rest of DeskRamp's toolkit. The signature workflow just makes the "why local processing is enough" case more concretely, since a signature is about as sensitive a thing to hand to a third-party server as a document gets.</p>
<hr />
<p>DeskRamp is free on the <a href="https://chromewebstore.google.com/detail/deskramp-pdf-toolkit-shee/fahhfkcilhjedkdcbeekfipjbmgnmnbk">Chrome Web Store</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Chrome Extension That Survives a Site's Constantly-Changing DOM]]></title><description><![CDATA[If you've ever built a browser extension that injects into a page you don't control — Facebook, Instagram, YouTube, anything built as a modern single-page app — you've probably hit a version of this b]]></description><link>https://websitegeek.hashnode.dev/building-a-chrome-extension-that-survives-a-site-s-constantly-changing-dom</link><guid isPermaLink="true">https://websitegeek.hashnode.dev/building-a-chrome-extension-that-survives-a-site-s-constantly-changing-dom</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[chrome extension]]></category><category><![CDATA[browser extensions]]></category><category><![CDATA[debugging]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[WebsiteGeek]]></dc:creator><pubDate>Wed, 26 Aug 2026 02:46:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8e512594ab8869936424d5/5282a9f7-09a1-4734-bfba-e3d4985a363e.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've ever built a browser extension that injects into a page you don't control — Facebook, Instagram, YouTube, anything built as a modern single-page app — you've probably hit a version of this bug: a feature works perfectly when you test it, then silently stops working a few minutes later, with no error in the console.</p>
<p>I ran into exactly this while building the feed-hiding logic for <a href="https://websitegeek.net/focusramp/">FocusRamp</a>, a Chrome extension that hides distracting social feeds instead of blocking entire sites. The bug took a while to track down, and the fix ended up reshaping how every DOM-dependent feature in the extension is built.</p>
<h2>The setup</h2>
<p>Feed-hiding sounds simple on paper: find the feed's wrapper element, toggle its <code>display</code> style, done. Something like:</p>
<pre><code class="language-plaintext">const feedWrapper = document.querySelector('[role="feed"]');
if (feedWrapper) feedWrapper.style.display = 'none';
</code></pre>
<p>Cache that reference once, toggle it whenever the user flips focus mode on or off. Works great in testing. Then, after toggling an unrelated feature (in my case, a dark-mode-style CSS filter), the feed would silently reappear — and toggling focus mode again wouldn't hide it back.</p>
<h2>The actual bug: your reference goes stale</h2>
<p>The root cause wasn't the toggle logic at all. It was that Facebook's feed is virtualized — the actual DOM node behind that <code>[role="feed"]</code> selector gets <strong>unmounted and remounted</strong> whenever the page repaints in certain ways (in my case, triggered by the CSS filter change). Your cached <code>feedWrapper</code> variable still points to a real DOM node — it just isn't the one on the page anymore. Setting <code>.style.display = 'none'</code> on it does nothing visible, because the visible element is a <em>different</em> node that happens to match the same selector.</p>
<p>This is easy to miss because nothing throws. <a href="http://feedWrapper.style"><code>feedWrapper.style</code></a><code>.display = 'none'</code> on a detached node is completely valid JavaScript — it just has no effect on what the user sees.</p>
<h2>The fix: stop trusting references, re-verify instead</h2>
<p>The fix isn't a smarter selector — no selector survives a full remount. The fix is to stop caching a single reference at all, and instead treat every mutation-observer tick as an opportunity to re-verify state against whatever's actually on the page right now:</p>
<pre><code class="language-plaintext">const hiddenWrappers = new Map(); // wrapper -&gt; previous display value

function applyFeedState(shouldHide) {
  const wrapper = document.querySelector('[role="feed"]');
  if (!wrapper) return;

  if (shouldHide &amp;&amp; !hiddenWrappers.has(wrapper)) {
    hiddenWrappers.set(wrapper, wrapper.style.display);
    wrapper.style.display = 'none';
  } else if (!shouldHide &amp;&amp; hiddenWrappers.has(wrapper)) {
    wrapper.style.display = hiddenWrappers.get(wrapper);
    hiddenWrappers.delete(wrapper);
  }
}

new MutationObserver(() =&gt; applyFeedState(currentFocusModeState))
  .observe(document.body, { childList: true, subtree: true });
</code></pre>
<p>The <code>Map</code> keyed by wrapper element means the code self-heals: if the page swaps in a new wrapper node, the next mutation-observer tick re-queries, finds the new node, and applies state to it — no stale reference, nothing to go silently out of sync. The old wrapper, if it's still floating around detached, is simply irrelevant; nothing holds a reference to it anymore.</p>
<h2>Why this pattern generalizes</h2>
<p>This isn't specific to feed-hiding — it's the shape of every bug you'll hit injecting into a page you don't control. Any extension feature built on "find the element once, remember it, act on it later" has an implicit assumption baked in: that the element you found is the element that'll still be there. On a static page, that assumption mostly holds. On a modern SPA — and nearly every major site is one now — it doesn't, and the failure mode is always the same: no error, no crash, the feature just quietly stops doing anything.</p>
<p>The reliable pattern is re-verification, not memoization: query fresh on every relevant tick, and use a <code>Map</code> (or <code>WeakMap</code>, if you don't need to enumerate keys) if you need to track per-element state across those re-queries rather than a single top-level variable. It costs a little more CPU than caching once, but a <code>querySelector</code> call is cheap, and "slightly more CPU" beats "silently broken" every time.</p>
<p>If you're building anything that hooks into a site you don't control, it's worth assuming from day one that every DOM reference you hold has an expiration date you can't predict — and designing for that instead of discovering it the hard way.</p>
<hr />
<p>If you're curious what this looks like in a shipped product: <a href="https://chromewebstore.google.com/detail/focusramp-site-blocker-hi/pjeddidcdnhkgioednidaafafgnblikm">FocusRamp</a> is free on the Chrome Web Store.</p>
]]></content:encoded></item></channel></rss>