<?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: GlenKit</title>
    <description>The latest articles on DEV Community by GlenKit (@glenkit).</description>
    <link>https://dev.to/glenkit</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%2F4146503%2F1e546bec-ae0f-4475-9d59-5496586039ef.png</url>
      <title>DEV Community: GlenKit</title>
      <link>https://dev.to/glenkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/glenkit"/>
    <language>en</language>
    <item>
      <title>Chrome Web Store rejected my extension in 10 minutes — the line that caused it</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:26:10 +0000</pubDate>
      <link>https://dev.to/glenkit/chrome-web-store-rejected-my-extension-in-10-minutes-the-line-that-caused-it-nip</link>
      <guid>https://dev.to/glenkit/chrome-web-store-rejected-my-extension-in-10-minutes-the-line-that-caused-it-nip</guid>
      <description>&lt;p&gt;I had spent two weeks on a browser extension. The package passed the validation, the screenshots were ready, the privacy policy was written. I hit submit at 9 pm and went to make tea.&lt;/p&gt;

&lt;p&gt;Ten minutes later the email arrived: &lt;strong&gt;rejected — "keyword spam in the description"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not a code problem. Not a permissions problem. A copywriting problem, caught by an automated reviewer before any human ever looked at it.&lt;/p&gt;

&lt;p&gt;Here is exactly what I did wrong, because it is a mistake almost every indie extension developer makes, and the fix takes five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rejected description looked like
&lt;/h2&gt;

&lt;p&gt;The extension is a cookie-banner tool. I wanted the listing to be searchable, so I wrote a description that listed every consent platform the extension supports:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Automatically rejects cookie banners on OneTrust, Cookiebot, Quantcast, Didomi, Usercentrics, Iubenda, Sourcepoint, TrustArc and more...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eight product names in a single sentence. It reads like a feature list. To a search engine it looks like keyword stuffing. To the reviewer's classifier it looks like exactly what it was: an attempt to rank for other companies' brand names.&lt;/p&gt;

&lt;p&gt;The rejection reason was not vague either. It did not say "your extension may violate policy." It named the section and the violation type: &lt;strong&gt;keyword spam&lt;/strong&gt; in the description. And it happened in under ten minutes, which tells you the first pass is purely automated — embedding similarity against known spam patterns, plus a brand-name frequency check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is such an easy mistake
&lt;/h2&gt;

&lt;p&gt;Because listing integrations &lt;em&gt;feels&lt;/em&gt; honest and useful. If your tool works with twelve platforms, saying so is not a lie. And in a README, listing dependencies is normal practice.&lt;/p&gt;

&lt;p&gt;But the store listing is not documentation. It is an ad, and the ad has a search index behind it. When your description contains eight well-known competitor-ish brand names, you are not describing compatibility — you are bidding on their traffic, and the store knows it.&lt;/p&gt;

&lt;p&gt;There is a second-order problem nobody talks about: &lt;strong&gt;the brand names do not help you anyway&lt;/strong&gt;. Someone searching "OneTrust cookie banner" wants OneTrust's documentation. Someone searching "auto reject cookie banners" is your actual buyer, and that phrase is nowhere in the list of brand names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rewrite
&lt;/h2&gt;

&lt;p&gt;I deleted every brand name and described the capability instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CookieGuard recognises the consent platforms most sites run on rather than a list of individual domains, plus a text-matching fallback that reads the banner in 35 languages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same information about coverage. Zero third-party trademarks. It passed on resubmission.&lt;/p&gt;

&lt;p&gt;Note what changed structurally: the original sentence was a &lt;em&gt;list of nouns&lt;/em&gt;, and the rewrite is a &lt;em&gt;description of behaviour&lt;/em&gt;. That is the pattern worth remembering — describe what the software does, not who it interoperates with.&lt;/p&gt;

&lt;p&gt;If you genuinely need to name a third party, say "works with major consent platforms" and put the specifics in your own documentation, where no store classifier is reading them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more rejection triggers I hit later
&lt;/h2&gt;

&lt;p&gt;The keyword-spam one was the most expensive, but not the only one. While shipping the rest of the portfolio, the same automated reviewer and the package validator produced three more:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;extDescription&lt;/code&gt; has a hard 132-character limit in the package manifest.&lt;/strong&gt; Not the store listing — the manifest field, which gets validated at upload. I had a description at 148 characters in six locales and it failed with a message about a field "conversion being too long" in a locale I was not even looking at. Fix: count characters per locale, not per language, and remember that accents and non-Latin scripts count as one character each in the JSON but may render differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Naming another browser in a localized description triggers a warning.&lt;/strong&gt; When you submit to Microsoft Edge, writing "works in Chrome" in a German or Japanese description gets flagged as referencing a competing browser. I had to rewrite eight locales to say "the browser's built-in model" instead of naming anything. It is a fair rule and it is easy to trip if you translate from an English original that mentions Chrome naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Search terms have a hard cap, and it is per locale.&lt;/strong&gt; Seven terms per language, twenty-one unique terms total across the whole listing. I exceeded it by two words in Russian and one in French and the form simply refused to save. Nobody tells you this in the UI until you hit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;If I were starting the listing today, the checklist would be four lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the description with zero third-party proper nouns.&lt;/strong&gt; If you catch yourself typing a brand name, replace it with what it does for the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the manifest description per locale before packaging.&lt;/strong&gt; 132 characters, hard limit, validated at upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never name another browser in a localized field.&lt;/strong&gt; Say "your browser" and move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget your search terms before writing them.&lt;/strong&gt; They are a scarce resource, not a wishlist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The uncomfortable part is that all four are copywriting constraints, not engineering ones. I had unit tests for the rejection logic and none for the listing text, because I thought of the listing as packaging rather than as part of the product. Store reviewers disagreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that still surprises me
&lt;/h2&gt;

&lt;p&gt;The rejection was, in the end, a good outcome. The rewritten description converts better than the original, because it explains the tool instead of name-dropping. The version that tried hardest to be found for other companies' keywords was also the version that told the fewest people what the product actually does.&lt;/p&gt;

&lt;p&gt;If you are waiting on a review right now and you listed a bunch of platform names in your description — that is probably why. You can edit the copy without resubmitting the package, and the fix is usually a five-minute rewrite.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build small privacy-first extensions at &lt;a href="https://glenkit.com" rel="noopener noreferrer"&gt;GlenKit&lt;/a&gt;. The cookie tool mentioned above is &lt;a href="https://glenkit.com/cookieguard" rel="noopener noreferrer"&gt;CookieGuard&lt;/a&gt; — the description in this post is the one that finally shipped.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>browsers</category>
    </item>
    <item>
      <title>Hiding a cookie banner is not consent — what actually happens when you click "Reject all"</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Tue, 27 Oct 2026 07:29:44 +0000</pubDate>
      <link>https://dev.to/glenkit/hiding-a-cookie-banner-is-not-consent-what-actually-happens-when-you-click-reject-all-2le7</link>
      <guid>https://dev.to/glenkit/hiding-a-cookie-banner-is-not-consent-what-actually-happens-when-you-click-reject-all-2le7</guid>
      <description>&lt;p&gt;Most banner blockers do the same thing: they hide the banner with CSS and call it a day. The banner is gone from your screen — but nothing was ever told to the site. The tracking scripts you were shown a choice about? They load anyway. You got the &lt;em&gt;appearance&lt;/em&gt; of consent without the mechanism.&lt;/p&gt;

&lt;p&gt;I spent the last few months building a browser extension that takes the other approach: instead of hiding banners, it presses the real &lt;strong&gt;"Reject all"&lt;/strong&gt; button — or calls the consent platform's own API when one exists. Here's what I learned about how consent actually works under the hood, and why "hiding" and "rejecting" are legally very different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Reject all" actually triggers
&lt;/h2&gt;

&lt;p&gt;When you click a real reject button, one of several things happens depending on the consent management platform (CMP) the site uses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A consent cookie gets written with an explicit deny state.&lt;/strong&gt; The site's tag manager reads it on every subsequent page load and withholds non-essential scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A JavaScript API is called&lt;/strong&gt; — most major CMPs expose an API of their own, which propagates into an event that third-party scripts listen for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In some setups, a server-side endpoint is hit&lt;/strong&gt; to log the opt-out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point: rejection is a &lt;em&gt;state change&lt;/em&gt;, visible to the site and to every script that respects the framework. Hiding the banner is a &lt;em&gt;visual change&lt;/em&gt;, visible only to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The long tail is the hard part
&lt;/h2&gt;

&lt;p&gt;The big consent platforms are easy — each has documented APIs and stable DOM structures. A couple dozen platforms by name covers a huge share of the web.&lt;/p&gt;

&lt;p&gt;The long tail is everything else: regional CMPs, self-hosted script snippets, and sites that roll their own banner. For those, the extension ships a text-matching fallback that recognizes reject-style buttons across &lt;strong&gt;35 languages&lt;/strong&gt; — because "Refuser tout", "Alle ablehnen" and "拒绝全部" all mean the same thing, and your blocker should speak all of them.&lt;/p&gt;

&lt;p&gt;It also sends the &lt;strong&gt;Global Privacy Control (GPC) signal&lt;/strong&gt; on every page, which several US state laws now treat as a binding opt-out.&lt;/p&gt;

&lt;p&gt;One more detail that matters: after clicking, the extension &lt;em&gt;verifies the banner is actually gone&lt;/em&gt; — because a click that silently failed is worse than no click: you think you opted out, and you didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just block everything?
&lt;/h2&gt;

&lt;p&gt;Because that breaks sites, and because it's not what you asked for. A real consent framework gives you a choice; the respectful default is the one privacy law actually contemplates: non-essential tracking off, site functional, zero data sent anywhere.&lt;/p&gt;

&lt;p&gt;That last part is the design constraint I'm proudest of: the extension itself is 100% local. No account, no analytics, no telemetry. It stores exactly two things per site — the hostname and your choice — and you can wipe it anytime.&lt;/p&gt;

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

&lt;p&gt;CookieGuard runs on Chrome and Edge. The free tier gives you &lt;strong&gt;40 successful rejections a week&lt;/strong&gt; with every detection feature enabled; Pro ($19.9 lifetime or $11.99/year) lifts the cap and adds per-site defaults.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://glenkit.com/cookieguard" rel="noopener noreferrer"&gt;glenkit.com/cookieguard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Edge Add-ons store: &lt;a href="https://microsoftedge.microsoft.com/addons/detail/cookieguard/lifnjhkjijgenlkmcdeefffkbhclmfkd" rel="noopener noreferrer"&gt;CookieGuard&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're also launching on &lt;a href="https://www.producthunt.com/products/cookieguard-2" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt; — if you've ever wondered whether your "Reject all" click actually did anything, it's a fun rabbit hole.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about consent-platform internals in the comments — it's a weirder ecosystem than you'd expect.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>chrome</category>
      <category>gdpr</category>
    </item>
    <item>
      <title>Your screenshot tool is the leak — doing redaction entirely client-side</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:23:32 +0000</pubDate>
      <link>https://dev.to/glenkit/your-screenshot-tool-is-the-leak-doing-redaction-entirely-client-side-38gn</link>
      <guid>https://dev.to/glenkit/your-screenshot-tool-is-the-leak-doing-redaction-entirely-client-side-38gn</guid>
      <description>&lt;p&gt;The most common way people redact a screenshot is still this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the screenshot.&lt;/li&gt;
&lt;li&gt;Search "blur image online".&lt;/li&gt;
&lt;li&gt;Upload it to the first result.&lt;/li&gt;
&lt;li&gt;Blur the part that matters, download, paste into Slack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is the problem. You just handed an unredacted screenshot — session tokens, customer emails, API keys, production data — to a website you found ninety seconds ago. The blur happens &lt;em&gt;after&lt;/em&gt; the upload, and the "delete" button on that page deletes your copy, not theirs.&lt;/p&gt;

&lt;p&gt;It's a small, boring leak, and it happens thousands of times a day, because the tool that solves the aesthetic problem (make this text unreadable) creates a security problem (it made a copy first).&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I'd propose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Redaction must happen on the machine the screenshot was taken on, before the image goes anywhere.&lt;/strong&gt; If the pixels reaching any network request are already covered, the rest of the pipeline stops mattering — you can paste it into a chat, a ticket, a bug report, and the sensitive part isn't in the file.&lt;/p&gt;

&lt;p&gt;That's the whole design constraint. Everything below falls out of taking it seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a leak, in the browser
&lt;/h2&gt;

&lt;p&gt;A screenshot in a browser is a compositor output. The extension API is &lt;code&gt;chrome.tabs.captureVisibleTab&lt;/code&gt;, which returns a data URL of the visible viewport:&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;dataUrl&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captureVisibleTab&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;format&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point you hold a faithful copy of everything on screen, including the parts nobody wants in a public issue: the token in the address bar, the &lt;code&gt;Authorization&lt;/code&gt; header value in some debug panel, the customer list scrolled halfway down.&lt;/p&gt;

&lt;p&gt;The naive version of a redaction feature is a UI on top of that image. The important version redacts &lt;strong&gt;before you decide to keep the file&lt;/strong&gt; — i.e. the extension should never write the raw capture anywhere durable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "covered" should actually mean
&lt;/h2&gt;

&lt;p&gt;Two choices worth being deliberate about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blur vs solid fill.&lt;/strong&gt; Blur looks more polite, but for text it's a lossy encoding, not a one-way function. Low-radius blurs of high-contrast text (a monospace token on a dark terminal, for instance) have been reconstructed with deconvolution, and text heights are highly constrained, which shrinks the search space. For secrets, use an opaque fill. Blur is for faces and plates, where the content is continuous, not discrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redraw vs overlay.&lt;/strong&gt; Overlaying a rectangle in an HTML layer is easy and completely useless — the underlying pixels are still in the export. You need to composite (draw the image, then draw the fill on top) and export the re-encoded result:&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OffscreenCanvas&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="nx"&gt;h&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;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;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;imgBitmap&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;// the capture&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;#111&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;fillRect&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;w0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                     &lt;span class="c1"&gt;// irreversible at this point&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="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convertToBlob&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;image/png&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;Everything downstream operates on that blob. If a cover is wrong, the user redraws; there is no "uncover" and no original in the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually reduces user error: pre-detection
&lt;/h2&gt;

&lt;p&gt;Manual redaction fails the same way manual everything fails — people miss one spot, usually the boring one. So the useful feature isn't a nicer brush; it's &lt;strong&gt;candidate detection before the user looks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DOM-matched regions.&lt;/strong&gt; Elements with &lt;code&gt;type="password"&lt;/code&gt;, &lt;code&gt;autocomplete="cc-number"&lt;/code&gt;, &lt;code&gt;inputmode="numeric"&lt;/code&gt;, or a &lt;code&gt;name&lt;/code&gt;/&lt;code&gt;id&lt;/code&gt; matching &lt;code&gt;/token|secret|api[-_]?key|otp|ssn/i&lt;/code&gt;. The element's bounding box maps directly to image coordinates via &lt;code&gt;getBoundingClientRect()&lt;/code&gt; plus scroll offsets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text-pattern regions.&lt;/strong&gt; Run a small regex set over the visible text nodes and highlight matches: JWT shape (&lt;code&gt;eyJ[A-Za-z0-9_-]{10,}\.&lt;/code&gt;), &lt;code&gt;sk-&lt;/code&gt;/&lt;code&gt;ghp_&lt;/code&gt;/&lt;code&gt;AKIA&lt;/code&gt; prefixes, IBAN-ish and card-number-ish digit runs, private-key headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structural guesses.&lt;/strong&gt; Terminal/console panes, &lt;code&gt;.env&lt;/code&gt;-looking blocks, admin tables — worth flagging, not auto-covering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output should be a &lt;em&gt;proposed&lt;/em&gt; set of covers the user can delete in one click. Auto-covering without review is its own failure mode: over-covering quietly destroys the screenshot's purpose, and the user then posts a useless image or, worse, turns the feature off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where detection honestly fails
&lt;/h2&gt;

&lt;p&gt;I'd rather list the limits than imply a guarantee:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Canvas / WebGL / video.&lt;/strong&gt; If the secret is rendered into a canvas or a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; frame, there is no DOM node and no text node. Regex detection sees nothing. Only a manual cover catches these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images of text.&lt;/strong&gt; A screenshot of a screenshot, a scanned invoice, a chart with labelled series. No OCR, no detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything virtualised.&lt;/strong&gt; Table cells recycled by a virtual list can be measured before or after they exist in the DOM, depending on scroll timing. Off-by-one-row covers are a real bug class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow DOM.&lt;/strong&gt; Closed shadow roots are invisible to the traversal; you get a bounding box with no children to inspect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the honest product claim is "detection reduces misses; the manual cover is still the guarantee." Any tool claiming otherwise is describing a best case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for the architecture
&lt;/h2&gt;

&lt;p&gt;Client-side redaction has one more consequence, and it's the reason I'd pick this shape again: &lt;strong&gt;there's nothing to operate.&lt;/strong&gt; No upload endpoint, no object storage, no retention policy to write, no "we delete after 24h" claim to make and be audited on. The security property is structural rather than promised.&lt;/p&gt;

&lt;p&gt;For a small tool that's the difference between a feature you can ship and a small compliance project. The trade is that you can't do the heavy stuff on a server — no models, no OCR on the secret-detection step, no history sync. In exchange, the failure mode of your infrastructure is "nothing happens," which is the failure mode I want.&lt;/p&gt;

&lt;p&gt;I built this into a small extension — &lt;a href="https://glenkit.com/veilshot?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-5" rel="noopener noreferrer"&gt;Veilshot&lt;/a&gt;. Pricing, stated plainly: the free tier does the manual covers (no account, no network requests at all — that's checked in CI on every build, not just promised in a policy); Pro is a one-off $19 for pattern detection, keyword rules and per-site rules, no subscription. It's a paid product and I'm not going to pretend otherwise. But the pattern is the more useful takeaway, and it generalises well past screenshots: &lt;strong&gt;if the data is sensitive, transform it before it moves, not after.&lt;/strong&gt; Blur tools on the web got the order wrong, and that's the entire bug.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turning what you read into Anki cards without the typing</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Tue, 13 Oct 2026 04:12:21 +0000</pubDate>
      <link>https://dev.to/glenkit/turning-what-you-read-into-anki-cards-without-the-typing-4gbh</link>
      <guid>https://dev.to/glenkit/turning-what-you-read-into-anki-cards-without-the-typing-4gbh</guid>
      <description>&lt;h1&gt;
  
  
  Turning what you read into Anki cards without the typing
&lt;/h1&gt;

&lt;p&gt;Here's a workflow that fails quietly, every day, for a lot of people:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You read something good — a paper, a lecture PDF, a long article, a YouTube lecture.&lt;/li&gt;
&lt;li&gt;You think "this belongs in Anki."&lt;/li&gt;
&lt;li&gt;You open Anki, create a deck, and start typing cards by hand.&lt;/li&gt;
&lt;li&gt;You make six cards, it's been twenty minutes, and you close the window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The material never makes it in. Not because spaced repetition doesn't work — it obviously does — but because the &lt;em&gt;input&lt;/em&gt; step is manual while the &lt;em&gt;review&lt;/em&gt; step is automated. Anki's scheduler is a piece of software. Your card creation is you, typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck is card creation, not review
&lt;/h2&gt;

&lt;p&gt;Most "I fell off Anki" stories I've heard are really "I stopped making cards" stories. Reviewing thirty cards takes a few minutes and has an algorithm pushing you along. Writing thirty cards takes an hour of undifferentiated typing, and nothing pushes you along at all.&lt;/p&gt;

&lt;p&gt;That asymmetry is worth naming, because it tells you where a tool can actually help. Nobody needs help being nagged to review. What's missing is a low-friction path from "the thing I just read" to "a card in my deck."&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to close the gap — and what each one costs
&lt;/h2&gt;

&lt;p&gt;If you look at how people have tried to solve this, there are basically three shapes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSV / export pipelines.&lt;/strong&gt; Generate cards somewhere, export to a file, import into Anki. This works, and it's the most common DIY approach. The costs show up later: field mapping breaks when your template changes, the import dialog steals focus, and there's &lt;em&gt;no deduplication&lt;/em&gt; — run it twice on the same article and you get two copies of every card. In a deck you'll keep for years, duplicate cards are worse than missing cards, because they double your review load silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anki add-ons.&lt;/strong&gt; Running inside Anki is convenient if that's where you already are. But by definition an add-on starts &lt;em&gt;after&lt;/em&gt; the text exists somewhere — it can't see the PDF you have open in a browser tab, and it can't see the article you're reading. It also only runs on desktop Anki, and it breaks on Anki version bumps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A browser extension.&lt;/strong&gt; This is the shape I ended up building. The argument for it: the content arrives in the browser first — pages, PDFs, YouTube transcripts. If you intercept it there, you're working at the actual point of friction, and you don't need the user to leave what they're doing to go make cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing through AnkiConnect instead of exporting files
&lt;/h2&gt;

&lt;p&gt;The technical decision that mattered most was to write cards into the collection through &lt;a href="https://foosoft.net/projects/anki-connect/" rel="noopener noreferrer"&gt;AnkiConnect&lt;/a&gt; rather than generating an import file.&lt;/p&gt;

&lt;p&gt;AnkiConnect is a local HTTP server exposed by an Anki add-on (code &lt;code&gt;2055492159&lt;/code&gt;, free). Because it can query the collection, you can do two things a file export fundamentally cannot:&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;// ask the collection what already exists before writing&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;findNotes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deck:"Reading" "{{text}}"&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="c1"&gt;// then write only the notes that aren't there yet&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;addNotes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;notes&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;The first call is the interesting one. A dedupe check against the live collection turns "generate 40 cards" into "generate 40 cards, 12 of which you already have" — and the second group never gets written. It also means cards land with the right note type and tags, in the deck you chose, with no import dialog and no field remapping.&lt;/p&gt;

&lt;p&gt;The honest cost of this approach: &lt;strong&gt;it requires desktop Anki running locally.&lt;/strong&gt; That's a real constraint — no AnkiWeb-only setup, no mobile. I took the trade because a card that lands in the wrong deck with the wrong template isn't really a card.&lt;/p&gt;

&lt;h2&gt;
  
  
  On-device inference, or your own key
&lt;/h2&gt;

&lt;p&gt;The second decision was where inference runs. The default is Chrome's built-in on-device model, which means the selected text never leaves the machine — no upload, no account, no telemetry. If you want a stronger model, you paste your own API key (OpenAI / Anthropic / Google) and the request goes browser → provider directly.&lt;/p&gt;

&lt;p&gt;Two things fall out of that architecture, and I'd rather state them plainly than let them be discovered later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Because inference runs on the user's machine or the user's key, there's no per-generation cost sitting on a server of mine. That's &lt;em&gt;why&lt;/em&gt; a one-time price is possible instead of a subscription — not a marketing stance.&lt;/li&gt;
&lt;li&gt;The only outbound request the extension makes is license activation. Nothing in the pipeline requires a server I operate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What generation gets wrong (and why "prune" is the real feature)
&lt;/h2&gt;

&lt;p&gt;LLMs will happily produce forty mediocre cards from a page of prose. Forty mediocre cards are worse than twelve good ones: they're more review load, lower retention per minute, and they train you to skim.&lt;/p&gt;

&lt;p&gt;So the workflow the tool is built around is &lt;strong&gt;generate → prune → write&lt;/strong&gt;, not generate-and-accept. Cards come back in a side panel where you edit or delete them before any of them touch the collection. The list-splitting template exists for the same reason: an enumeration in the source ("five causes of X") is a place where one card per item beats one card asking for all five.&lt;/p&gt;

&lt;p&gt;If there's one habit I'd recommend regardless of tooling: cap what you write per reading session. Twenty cards from an article you understood beats eighty from an article you skimmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full disclosure, and a question
&lt;/h2&gt;

&lt;p&gt;I'm the developer of &lt;a href="https://glenkit.com/ankimate?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-4" rel="noopener noreferrer"&gt;AnkiMate&lt;/a&gt; — a Chrome/Edge extension that does the above. Pricing, stated plainly: free tier is 3 generations a day and writing cards into Anki is always free and unlimited; Pro is $19.9 one-time (Cloze, batch generation, future templates), no subscription. It's a paid product and I'm not going to pretend otherwise.&lt;/p&gt;

&lt;p&gt;I'm not claiming it's better than what you use — I don't have the user base to make that claim honestly. What I'd genuinely like to know is where your own pipeline breaks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the friction in capturing the source, in generating the cards, or in the pruning/editing step afterwards?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My assumption while building was that pruning is the part people underrate. If you're a heavy Cloze user and that assumption is wrong, I'd rather find out now.&lt;/p&gt;

</description>
      <category>anki</category>
      <category>productivity</category>
      <category>ai</category>
      <category>learning</category>
    </item>
    <item>
      <title>Why most cookie-banner blockers don't actually work</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Thu, 08 Oct 2026 14:58:51 +0000</pubDate>
      <link>https://dev.to/glenkit/why-most-cookie-banner-blockers-dont-actually-work-o41</link>
      <guid>https://dev.to/glenkit/why-most-cookie-banner-blockers-dont-actually-work-o41</guid>
      <description>&lt;h1&gt;
  
  
  Why most cookie-banner blockers don't actually work
&lt;/h1&gt;

&lt;p&gt;Install a typical cookie-banner blocker, then watch the network tab while you visit a site. Here is what actually happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The page loads. The consent management platform (CMP) script executes and builds the banner.&lt;/li&gt;
&lt;li&gt;The blocker's CSS sets &lt;code&gt;display: none&lt;/code&gt; on the banner element.&lt;/li&gt;
&lt;li&gt;The banner disappears from your screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CMP never receives a "reject" signal.&lt;/strong&gt; Non-essential scripts keep loading exactly as before.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fourth step is the part almost nobody talks about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiding is a cosmetic operation
&lt;/h2&gt;

&lt;p&gt;A cookie banner is not a decorative overlay you're allowed to delete. It's the UI of a consent platform — a script that is supposed to &lt;em&gt;record a decision&lt;/em&gt; and hand that decision to the tag manager, which then decides whether to fire analytics, ads, session replay, and so on.&lt;/p&gt;

&lt;p&gt;If you remove the banner with CSS, you have removed the UI and left the machinery untouched. The page has no idea what you wanted, so it defaults to its own default, which is usually "everything on until told otherwise."&lt;/p&gt;

&lt;p&gt;The visible result is identical. The legal and technical result is the opposite of what the user intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real opt-out actually requires
&lt;/h2&gt;

&lt;p&gt;Consent platforms are designed to accept a decision from a few specific entry points. To genuinely reject, a tool has to use one of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger the platform's own reject control&lt;/strong&gt; — the same element a human would click, selected per platform (&lt;code&gt;#onetrust-reject-all-handler&lt;/code&gt;, &lt;code&gt;[data-testid="reject-all"]&lt;/code&gt;, &lt;code&gt;button[aria-label*="reject"]&lt;/code&gt;, …). It has to be the &lt;em&gt;actual&lt;/em&gt; control, not a look-alike button the tool injects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call the platform's public API&lt;/strong&gt;, e.g. OneTrust's &lt;code&gt;OneTrust.RejectAll()&lt;/code&gt;, or Cookiebot's consent submit methods. These write the consent record that the tag manager reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post a proper TCF v2 string&lt;/strong&gt; for IAB-compliant platforms, so downstream vendors see a "no consent for purposes 1–10" state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a reason most blockers skip this: it's platform-specific, the DOM changes constantly, and each CMP has its own naming. A CSS rule works on every site forever. An API call breaks next Tuesday. So the ecosystem settled on the version that's easy to maintain instead of the one that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal almost nobody sends: GPC
&lt;/h2&gt;

&lt;p&gt;Global Privacy Control is a legally recognised opt-out preference signal under the CCPA/CPRA and several US state privacy laws. It has two parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A request header, &lt;code&gt;Sec-GPC: 1&lt;/code&gt;&lt;/strong&gt; — trivial to add from an MV3 extension via a declarative rule:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"modifyHeaders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"requestHeaders"&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;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sec-GPC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&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;span class="nl"&gt;"condition"&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;"urlFilter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"resourceTypes"&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;"main_frame"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sub_frame"&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;2. The JS property &lt;code&gt;navigator.globalPrivacyControl&lt;/code&gt;&lt;/strong&gt;, which sites can read directly. Chrome doesn't ship it (yet), so an extension has to define it in a content script running before page scripts:&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;globalPrivacyControl&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;get&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;configurable&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="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;/* already defined */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's a &lt;em&gt;legal&lt;/em&gt; signal in some jurisdictions, a site that receives it is supposed to stop selling or sharing your data even without a banner interaction. Yet a quick audit of the popular CSS-hiding blockers shows most of them never send it. Hiding a banner is easy to demo; sending a header that no screenshot can show isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this category went stale
&lt;/h2&gt;

&lt;p&gt;Here's the part that made me actually build something.&lt;/p&gt;

&lt;p&gt;The extension with the largest install base in this category was acquired a few years ago and — as of this writing — has not shipped an update since mid-2024. The store reviews have shifted accordingly: a large share of the nearly 1,900 ratings are now one star, and the top-voted comments read like &lt;em&gt;"stopped working on many sites"&lt;/em&gt; and &lt;em&gt;"breaks scrolling"&lt;/em&gt;. Meanwhile a community fork that just keeps the rules updated has picked up a couple hundred thousand users.&lt;/p&gt;

&lt;p&gt;That tells you two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users in this category &lt;strong&gt;do&lt;/strong&gt; switch tools — the switching cost people assume exists isn't really there.&lt;/li&gt;
&lt;li&gt;The category's problem is &lt;strong&gt;maintenance&lt;/strong&gt;, not novelty. Consent platforms change their DOM; a blocker that isn't updated decays into "hides some banners, silently fails on others."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three constraints I built around
&lt;/h2&gt;

&lt;p&gt;Full disclosure: I'm the developer of a blocker in this space, &lt;a href="https://glenkit.com/cookieguard" rel="noopener noreferrer"&gt;CookieGuard&lt;/a&gt;. I'm not going to claim it's "better" — I have too few users to make that claim honestly. What I can describe is the shape of the problem I constrained myself to, because each constraint comes straight from what the reviews complain about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Press the real button.&lt;/strong&gt; Identifies the CMP by name (27 of them), triggers its genuine reject action or calls its own API; unknown banners fall back to a phrase library (224 don't-sell phrases across 35 languages); verifies the banner is actually gone rather than assuming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't break the page.&lt;/strong&gt; The rejection is scoped to the banner it dismissed. No page rewriting, and it never clicks "Accept" on your behalf — a consent tool that accepts on your behalf is worse than no tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing leaves the machine.&lt;/strong&gt; No account, no analytics, no telemetry. The optional per-site memory stores a hostname and the action you took, and clears in one click.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's a manifest V3 extension, rules ship as a versioned JSON file (because that's the part that has to stay current), and the only outbound requests it makes are an optional daily rules fetch and licence activation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I'd actually like answered
&lt;/h2&gt;

&lt;p&gt;Treat the tool as irrelevant to this post. The design question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should a consent tool be judged by whether the opt-out legally registers, or is hiding the banner good enough?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "hiding is fine," then the entire GPC layer and the CMP-specific API calls are wasted complexity, and the whole category can stay six lines of CSS. If the answer is "it should actually register," then most of the popular tools are shipping the cosmetic version of a legal feature — and that's worth saying out loud.&lt;/p&gt;

&lt;p&gt;Either way: if you've hit banners that neither approach handles, the shape of the failure is more useful to me than a rating. Tell me what the site was and what happened.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>browsers</category>
    </item>
    <item>
      <title>From web page to Anki deck in one click: a flashcard workflow that stays in your browser</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Wed, 30 Sep 2026 02:09:21 +0000</pubDate>
      <link>https://dev.to/glenkit/from-web-page-to-anki-deck-in-one-click-a-flashcard-workflow-that-stays-in-your-browser-2dln</link>
      <guid>https://dev.to/glenkit/from-web-page-to-anki-deck-in-one-click-a-flashcard-workflow-that-stays-in-your-browser-2dln</guid>
      <description>&lt;p&gt;If you use &lt;a href="https://apps.ankiweb.net/" rel="noopener noreferrer"&gt;Anki&lt;/a&gt; seriously, you already know the workflow: read an article, spot a fact worth remembering, switch windows, paste it into a note template, reformat it, repeat. The reading and the card-making happen in two different worlds, and the friction quietly kills half the cards you intended to make.&lt;/p&gt;

&lt;p&gt;This post walks through a workflow that keeps everything in the browser tab: select text, generate flashcards, review them, and write them straight into your Anki decks. It uses &lt;a href="https://glenkit.com/ankimate?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankimate" rel="noopener noreferrer"&gt;AnkiMate&lt;/a&gt;, a Chrome and Edge extension my team built — but the setup (AnkiConnect + a capture habit) works with other tools too, so most of this post applies even if you use something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-second setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install the &lt;a href="https://chromewebstore.google.com/detail/bloicodjmkmfdobmefmkijmlfkjpjjjf?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankimate" rel="noopener noreferrer"&gt;AnkiMate extension&lt;/a&gt; (also &lt;a href="https://microsoftedge.microsoft.com/addons/detail/cbinjkcachcgacclkpemaogiefhjgjag?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankimate" rel="noopener noreferrer"&gt;on Edge&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;In desktop Anki, install the AnkiConnect add-on (code &lt;code&gt;2055492159&lt;/code&gt;) and restart Anki.&lt;/li&gt;
&lt;li&gt;That's it. The extension talks to your local Anki over AnkiConnect — there is no account, no cloud sync step, no middleman.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The workflow: select → generate → review → write
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Select any text&lt;/strong&gt; on an article, lecture note, or documentation page (or use full page / PDF / YouTube transcript as input).&lt;/li&gt;
&lt;li&gt;Right-click → &lt;strong&gt;Generate flashcards&lt;/strong&gt;. A side panel opens with draft cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit or delete&lt;/strong&gt; anything you don't like. Nothing is final here — every card is editable, removable, and draft-saved.&lt;/li&gt;
&lt;li&gt;Pick a deck and hit &lt;strong&gt;Write to Anki&lt;/strong&gt;. Cards land in your collection tagged and deduplicated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last step is the one that saves the most time. Cards are written directly into the deck you choose, deduplicated by content hash — and if the source page changed, the card gets updated instead of duplicated. No more discovering three near-identical cards about the same fact a month later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Templates tuned by subject, not one generic prompt
&lt;/h2&gt;

&lt;p&gt;A generic "make flashcards from this text" prompt produces generic cards. AnkiMate ships nine templates, each tuned for a subject:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medicine (USMLE)&lt;/strong&gt; — expects clinical detail, not trivia&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GRE vocabulary&lt;/strong&gt; — word, definition, usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language learning&lt;/strong&gt; — front/back with context, plus a reversed-vocabulary variant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Law&lt;/strong&gt; — element-based structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programming&lt;/strong&gt; — concept and code-focused cards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List breakdown&lt;/strong&gt; — turns enumerations into atomic cards (the classic "one fact per card" rule, enforced for you)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;General&lt;/strong&gt; — everything else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eight templates are free. Pro adds &lt;strong&gt;Cloze deletions&lt;/strong&gt; (fill-in-the-blank), which is the right format for definitions, dates, and anatomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy: on-device AI by default, BYOK if you want more
&lt;/h2&gt;

&lt;p&gt;This matters to us, so it's the design center: &lt;strong&gt;on-device AI handles generation by default, and your selected text never leaves the browser&lt;/strong&gt;. If you want a stronger cloud model, you plug in &lt;strong&gt;your own API key&lt;/strong&gt; — and then your text goes directly from the extension to that provider, not through our servers. We never see your selections, your cards, or your key. (We wrote up &lt;a href="https://glenkit.com/blog/summarize-articles-byok-privacy?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankimate" rel="noopener noreferrer"&gt;why this matters&lt;/a&gt; in more detail — most "AI readers" proxy every page you visit through their own backend.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free, every day, no card required&lt;/strong&gt;: 3 generations per day (roughly 45–90 cards), all 8 non-Pro templates, all input types, unlimited writes to Anki. For most students this is genuinely enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro · Lifetime $19.9&lt;/strong&gt; (launch price, $29.9 after): unlimited generations, batch generation of 20+ cards from long pages, and the Cloze template. One payment, no subscription — the price of two coffees for the tool you'll use every study session.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Desktop Anki (with AnkiConnect) needs to be running when you write cards — AnkiMobile/AnkiDroid alone won't receive them.&lt;/li&gt;
&lt;li&gt;Generation quality tracks source quality: a well-structured page produces great cards; a rambling forum thread needs more editing.&lt;/li&gt;
&lt;li&gt;It makes cards, not understanding. Spaced repetition still requires you to actually do the reviews.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Install from the &lt;a href="https://chromewebstore.google.com/detail/bloicodjmkmfdobmefmkijmlfkjpjjjf?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankimate" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt; or &lt;a href="https://microsoftedge.microsoft.com/addons/detail/cbinjkcachcgacclkpemaogiefhjgjag?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankimate" rel="noopener noreferrer"&gt;Edge Add-ons&lt;/a&gt;, or read more at &lt;a href="https://glenkit.com/ankimate?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankimate" rel="noopener noreferrer"&gt;glenkit.com/ankimate&lt;/a&gt;. If you have questions about the AnkiConnect setup or template design, leave a comment — I reply to all of them.&lt;/p&gt;

</description>
      <category>anki</category>
      <category>productivity</category>
      <category>learning</category>
      <category>browsers</category>
    </item>
    <item>
      <title>How to summarize 2-hour videos and 50-page articles without leaving the browser tab</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Tue, 29 Sep 2026 07:39:22 +0000</pubDate>
      <link>https://dev.to/glenkit/how-to-summarize-2-hour-videos-and-50-page-articles-without-leaving-the-browser-tab-ak9</link>
      <guid>https://dev.to/glenkit/how-to-summarize-2-hour-videos-and-50-page-articles-without-leaving-the-browser-tab-ak9</guid>
      <description>&lt;p&gt;A 40-minute conference talk. A 90-minute podcast. A 12-minute tutorial that could have been a blog post. Most of what's valuable in a video is its &lt;em&gt;content&lt;/em&gt; — and content can be read.&lt;/p&gt;

&lt;p&gt;I've been building &lt;a href="https://glenkit.com/glensum.html?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=summarize-long-content" rel="noopener noreferrer"&gt;GlenSum&lt;/a&gt;, a browser extension that summarizes long videos and articles directly in the tab. This post covers the three working approaches to media summarization in a browser, the privacy trade-off most people don't notice they're making, and what "long content" actually means for an LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to summarize a YouTube video (slowest to fastest)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The transcript, by hand
&lt;/h3&gt;

&lt;p&gt;Every video with captions has a transcript: expand the description → &lt;em&gt;Show transcript&lt;/em&gt; → select all → paste into any AI chat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; 3–5 minutes of fiddly copying, and raw transcripts are messy — no punctuation, no speaker labels, timestamps everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; one-off use when you can't install anything&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Paste the URL into an AI chat
&lt;/h3&gt;

&lt;p&gt;Some assistants accept a YouTube URL directly and fetch the transcript themselves. Paste the link, ask for key points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; often limited by your plan's usage quota&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caveat:&lt;/strong&gt; availability varies by model and region — and now the video passes through yet another service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; occasional videos when you already pay for an assistant&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. A browser extension, one click
&lt;/h3&gt;

&lt;p&gt;Extensions built for this read the transcript from the page you already have open and generate a structured summary in a side panel. The workflow becomes: open the video → click the extension → read the summary. No copying, no tab-switching.&lt;/p&gt;

&lt;p&gt;The extras that matter: adjustable summary length, follow-up questions about the content ("what did they say about X?"), bilingual output when the talk isn't in your first language, and Markdown/PDF export so the summary lands in your notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy trade-off most people miss
&lt;/h2&gt;

&lt;p&gt;Here's the part that motivated building this in the first place. When you paste a URL or transcript into a hosted summarizer, your &lt;strong&gt;reading and watching history becomes someone's dataset&lt;/strong&gt;. Article by article, video by video, a profile of what you consume accumulates on a server you don't control. That's a real cost that never shows up on the pricing page.&lt;/p&gt;

&lt;p&gt;The alternative is BYOK — bring your own key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You paste an API key from your own AI provider (OpenAI, Google, a local gateway, anything OpenAI-compatible) into the extension once.&lt;/li&gt;
&lt;li&gt;The extension reads the page content in your browser and sends it &lt;strong&gt;directly to the provider you chose&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;There is no middleman server. No account. No logs on my side, because there is no "my side" — the extension ships as static code, and the network calls go from your browser to your provider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is honest: you manage your own key, and free-tier users can still use built-in free models without one. But nobody in the middle ever sees what you read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "long" actually means for an LLM
&lt;/h2&gt;

&lt;p&gt;Summarizing a 2-hour talk isn't just "paste transcript into prompt". A two-hour transcript is roughly 25,000–40,000 words — technically it may fit a modern context window, but quality degrades: the model skims, drops the middle, and blends sections together.&lt;/p&gt;

&lt;p&gt;The approach that works is &lt;strong&gt;map-reduce over chunks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Map:&lt;/strong&gt; split the transcript (or article) into overlapping chunks at natural boundaries — chapters, speaker turns, section headings — and summarize each chunk separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce:&lt;/strong&gt; merge the chunk summaries into a final structured summary, keeping section-level detail instead of a mushy average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why page-native tools beat copy-paste: the extension already knows the page structure, so it can split at real boundaries instead of arbitrary token counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a summary is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Summaries are ideal for &lt;strong&gt;inverted-pyramid content&lt;/strong&gt;: talks, podcasts, news analysis, tutorials — where information density is low and predictable. They're poor substitutes for &lt;strong&gt;demonstration content&lt;/strong&gt;: if someone is showing you how to do a three-point turn, no summary captures the steering.&lt;/p&gt;

&lt;p&gt;A good rule: summarize first to decide &lt;em&gt;whether&lt;/em&gt; to watch — not to avoid watching things worth watching.&lt;/p&gt;

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

&lt;p&gt;GlenSum is free to install and works out of the box (built-in free models, or your own key for unlimited use):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Site &amp;amp; docs: &lt;a href="https://glenkit.com/glensum.html?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=summarize-long-content" rel="noopener noreferrer"&gt;glenkit.com/glensum&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧩 &lt;a href="https://microsoftedge.microsoft.com/addons/detail/glensum-web-summary-ass/bpiieipdednngkkgdhmgmfbbbncepdpd" rel="noopener noreferrer"&gt;Edge Add-ons&lt;/a&gt; · &lt;a href="https://chromewebstore.google.com/detail/glensum-web-summary-assis/ppljcihchddoaoebcgkjobinhhcloaek" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about the architecture — especially around BYOK flows or chunking strategies for long transcripts — happy to answer in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>productivity</category>
      <category>chrome</category>
    </item>
  </channel>
</rss>
