<?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>What Chrome Web Store review time actually looks like in 2026 (measured on 6 submissions)</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Sun, 08 Nov 2026 14:01:31 +0000</pubDate>
      <link>https://dev.to/glenkit/what-chrome-web-store-review-time-actually-looks-like-in-2026-measured-on-6-submissions-310k</link>
      <guid>https://dev.to/glenkit/what-chrome-web-store-review-time-actually-looks-like-in-2026-measured-on-6-submissions-310k</guid>
      <description>&lt;p&gt;Every few months someone asks "how long does Chrome Web Store review take now?" and the answers range from "two hours" to "three weeks". Both are true, because review time is not one number — it depends on what changed in the package. I keep timestamps for every submission of my extensions, so here is the real spread.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you submitted&lt;/th&gt;
&lt;th&gt;Observed time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rejection (policy violation in the listing text)&lt;/td&gt;
&lt;td&gt;~10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update, same permissions, small feature set&lt;/td&gt;
&lt;td&gt;same day – 1 day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update that adds a permission&lt;/td&gt;
&lt;td&gt;several days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First submission of a new extension&lt;/td&gt;
&lt;td&gt;days to weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is boring and consistent: &lt;strong&gt;review time tracks the permission delta, not the amount of code.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The submissions I'm drawing from
&lt;/h2&gt;

&lt;p&gt;Two extensions, both MV3, both already published:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CookieGuard&lt;/strong&gt; — rejects cookie consent banners. Permissions: &lt;code&gt;storage&lt;/code&gt; + host permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AnkiMate&lt;/strong&gt; — turns web pages into Anki cards. Permissions: &lt;code&gt;contextMenus&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt;, &lt;code&gt;sidePanel&lt;/code&gt;, &lt;code&gt;scripting&lt;/code&gt;, &lt;code&gt;tabs&lt;/code&gt;, host permission.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither runs remote code, and neither has widened its permission string since it was first approved. That last part matters more than anything else below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same-day approvals
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CookieGuard 1.3.0 → 1.4.0.&lt;/strong&gt; The change: the upgrade button stopped opening my website and started opening a Creem checkout URL, plus a yearly-plan link. Zero permission changes, a handful of new locale keys. Submitted on a Friday afternoon, live by the evening. The public listing page already showed the new version number a few hours later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AnkiMate 1.3.0 → 1.4.0.&lt;/strong&gt; Bigger in code — a fallback export path (TSV instead of AnkiConnect), a keyboard command (&lt;code&gt;Alt+Shift+D&lt;/code&gt;), a trio of read-only AnkiConnect stats calls. Still the same permission string as 1.3.0. Submitted in the evening, live later that night.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CookieGuard 1.4.0 → 1.4.1.&lt;/strong&gt; Per-site action overrides, a first-run welcome page, a reworked popup diagnostics line, and a rewritten store description submitted in the same pass. Approved within a day.&lt;/p&gt;

&lt;p&gt;Three updates, three approvals, no extra review rounds. The only thing all three have in common: &lt;strong&gt;the permissions string did not change.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten-minute rejection
&lt;/h2&gt;

&lt;p&gt;Earlier, on a different extension of mine, one submission came back in about ten minutes. It was not a code problem. The listing description listed competitor product names as a comparison — a trademark issue, not a functionality issue. The package was never inspected.&lt;/p&gt;

&lt;p&gt;This is the failure mode people underrate: a policy rejection can arrive before a human ever looks at your extension, because listing text is checked mechanically. If you get a rejection in minutes, assume the description, not the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why permission changes cost days
&lt;/h2&gt;

&lt;p&gt;Chrome's review process escalates when the permission warning shown to users changes. The reviewer now has to judge whether the new capability is justified by what your description promises. That is a subjective call, so it goes into a slower queue.&lt;/p&gt;

&lt;p&gt;Practical consequence: &lt;strong&gt;batch your permission changes.&lt;/strong&gt; If you are going to add host access for one more API domain, ship it together with every other permission-adjacent change you have queued. Paying the slow review twice is the expensive mistake.&lt;/p&gt;

&lt;p&gt;Corollary: if a release is genuinely permission-neutral, do not be afraid to ship it. Same-day approval is normal, not lucky.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that do not seem to affect review time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Number of locales.&lt;/strong&gt; I ship 9 languages on both extensions. English-only builds and 9-language builds both got approved the same day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listing text changes bundled with a code update.&lt;/strong&gt; One review covers both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots and icon changes.&lt;/strong&gt; No visible effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description length&lt;/strong&gt;, as long as it stays under the 16,000-character cap and does not read like keyword stuffing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things that do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A permission string that changes.&lt;/strong&gt; Even one added host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote code or remotely hosted logic.&lt;/strong&gt; Anything that pulls executable instructions from your server is its own queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A description that makes claims the package does not implement.&lt;/strong&gt; This one is not just slow, it is the thing people get rejected for. If your store page lists a Pro feature, assume a reviewer can install the extension and check — which is why I now grep my own codebase for every feature name that appears in a listing before submitting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touching the listing while a submission is in review.&lt;/strong&gt; Wait. Editing mid-review can reset the state, and you lose the timestamp you were using to estimate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to actually measure this for yourself
&lt;/h2&gt;

&lt;p&gt;Keep a small log: submission timestamp, permission diff (yes/no), what changed, approval timestamp. After four or five entries you will have a better estimate for your own extension than any blog post, including this one — review behaviour varies by category and by account history.&lt;/p&gt;

&lt;p&gt;Two things I wish I had done from the start: write down the exact permission string of every released version (so "did this change?" is a diff, not a memory exercise), and diff the store description against the code before every submission.&lt;/p&gt;

&lt;p&gt;The extensions in this log are &lt;a href="https://glenkit.com/cookieguard" rel="noopener noreferrer"&gt;CookieGuard&lt;/a&gt; and &lt;a href="https://glenkit.com/ankimate" rel="noopener noreferrer"&gt;AnkiMate&lt;/a&gt; — both local-first, no telemetry, no account. I'm the developer, so treat the timings as one person's dataset, not an industry average.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Writing cards straight into Anki from a browser extension (AnkiConnect, without the hand-waving)</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Wed, 04 Nov 2026 13:37:54 +0000</pubDate>
      <link>https://dev.to/glenkit/writing-cards-straight-into-anki-from-a-browser-extension-ankiconnect-without-the-hand-waving-g2h</link>
      <guid>https://dev.to/glenkit/writing-cards-straight-into-anki-from-a-browser-extension-ankiconnect-without-the-hand-waving-g2h</guid>
      <description>&lt;p&gt;If you have ever built a tool that generates flashcards, you have probably stopped at the export step: produce a CSV or an APKG, hand it to the user, and let them import it into Anki themselves. That works, but it leaks users at every hop — the download, the import dialog, the field mapping, and the duplicate pile-up when they run your tool twice.&lt;/p&gt;

&lt;p&gt;There is a better option, and it is an add-on most people have heard of but few have wired up: &lt;strong&gt;AnkiConnect&lt;/strong&gt;. This post is the practical version of what that integration actually looks like from inside a browser extension — the parts the README does not cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AnkiConnect actually is
&lt;/h2&gt;

&lt;p&gt;AnkiConnect (add-on code &lt;code&gt;2055492159&lt;/code&gt;) runs a small local HTTP server inside desktop Anki, bound to &lt;code&gt;127.0.0.1:8765&lt;/code&gt;. It speaks JSON: you POST a payload like:&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;"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;"addNote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&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;"note"&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;"deckName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Med school::Lectures"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modelName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Basic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&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;"Front"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is X?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Back"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"It is Y"&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;"tags"&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;"lecture-07"&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;and Anki replies with a note id — or an error if the deck or model does not exist. That is the whole protocol. No cloud, no account, no file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about: CORS
&lt;/h2&gt;

&lt;p&gt;Here is the catch that turns a one-hour integration into a two-day one. AnkiConnect sets &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;, which sounds permissive — but a browser extension's content script is not a web page. Depending on where you issue the fetch from, you may hit mixed-content rules, or the request may go out with the page's origin rather than your extension's.&lt;/p&gt;

&lt;p&gt;The reliable pattern is to never fetch from a content script at all. Do it in the &lt;strong&gt;service worker&lt;/strong&gt; (or background page), where you can use the extension's host permissions to talk to &lt;code&gt;127.0.0.1&lt;/code&gt; directly:&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;"host_permissions"&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;"http://127.0.0.1:8765/*"&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;Then the content script just passes card data over &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt;, and the worker makes the call. One message channel, one place where networking happens, no CORS surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicates: the problem import never solves
&lt;/h2&gt;

&lt;p&gt;The silent killer of generated decks is the second run. User converts chapter 3, imports, studies. Two weeks later they re-run it with better settings — and now every card exists twice, because a file has no idea what is already in the collection.&lt;/p&gt;

&lt;p&gt;AnkiConnect gives you the pieces to fix this, but you have to assemble them. The two calls that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;findNotes&lt;/code&gt; with a query like &lt;code&gt;"deck:Lectures"&lt;/code&gt;, which returns note ids you can then fetch with &lt;code&gt;notesInfo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;canAddNotes&lt;/code&gt;, which takes a batch of note payloads and tells you which ones would be duplicates of existing notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheap and effective approach: hash the normalized front text (lowercase, whitespace-collapsed) and keep the hash in a dedicated field or tag. Before writing, check &lt;code&gt;canAddNotes&lt;/code&gt; and skip anything already present. AnkiMate does exactly this — re-running the same source updates instead of duplicating, which is the difference between a deck people keep and a deck they quietly abandon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that bite in production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Desktop Anki must be running.&lt;/strong&gt; This is the whole trade-off of the direct-write approach, and you should say it plainly in your UI instead of letting users discover it through a failed fetch. A graceful "Anki isn't open" state converts better than a console error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deck and model names are user-defined.&lt;/strong&gt; Never hardcode &lt;code&gt;"Basic"&lt;/code&gt; — fetch &lt;code&gt;modelNames&lt;/code&gt; and &lt;code&gt;deckNames&lt;/code&gt; on connect, and let the user pick. A card written to a nonexistent model fails silently in some versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port 8765 is configurable.&lt;/strong&gt; Users who run other tooling occasionally change it. Persist the port in settings and fail loudly with instructions rather than retrying forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The local server is trust boundary.&lt;/strong&gt; Anything on the user's machine can talk to it, which is why AnkiConnect ships with a &lt;code&gt;webApiKey&lt;/code&gt; option. If your extension runs in a shared-machine context, support the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond extensions
&lt;/h2&gt;

&lt;p&gt;The same pattern works for any local tool that wants to talk to Anki: scripts, editors, note apps. The export-file workflow made sense when Anki was closed; with a local API, "generate then import" is a choice, not a requirement. The users who care most — medical students, language learners, anyone with a deck they actually maintain — notice the difference immediately, because the file step is where their workflow used to break.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the integration behind &lt;a href="https://glenkit.com/ankimate?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ankiconnect" rel="noopener noreferrer"&gt;AnkiMate&lt;/a&gt;, a browser extension that turns web pages, PDFs and YouTube transcripts into Anki cards and writes them in over AnkiConnect. It runs three generations a day free; writing to Anki is unlimited free. Disclosure: I built it — the architecture above is the real one, not the brochure version.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>browser</category>
    </item>
    <item>
      <title>I shipped my extension in 9 languages to two stores. Here's what broke.</title>
      <dc:creator>GlenKit</dc:creator>
      <pubDate>Wed, 04 Nov 2026 12:30:44 +0000</pubDate>
      <link>https://dev.to/glenkit/i-shipped-my-extension-in-9-languages-to-two-stores-heres-what-broke-b5d</link>
      <guid>https://dev.to/glenkit/i-shipped-my-extension-in-9-languages-to-two-stores-heres-what-broke-b5d</guid>
      <description>&lt;p&gt;I shipped the same extension update to the Microsoft Edge Add-ons store and the Chrome Web Store in nine languages last week. The code change was maybe forty lines. The listing work took two days and broke in three different ways.&lt;/p&gt;

&lt;p&gt;If you're about to localize a browser extension, here's what the consoles do not warn you about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother localizing at all
&lt;/h2&gt;

&lt;p&gt;When you have no audience, store search is the only distribution channel you get for free. A monolingual listing only competes in English — the most crowded query space in both stores. A localized listing competes in a much thinner pool, because most indie extensions never translate anything.&lt;/p&gt;

&lt;p&gt;That's the whole argument. It is not about "global reach". It's about being one of four results instead of one of four hundred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: every locale has its own character limit, and one of them is brutal
&lt;/h2&gt;

&lt;p&gt;In Edge Add-ons, the short description stored in your manifest (&lt;code&gt;extDescription&lt;/code&gt;) is capped at &lt;strong&gt;132 characters&lt;/strong&gt; — per locale, and it's a hard package-level validation, not a warning. I had five of nine locales over the limit and did not find out until the upload refused the package.&lt;/p&gt;

&lt;p&gt;German and Russian blew past it almost immediately, because the same sentence is simply longer in those languages. "Summarize any page with your own AI key" is 41 characters in English and comfortably over budget once translated with any precision.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous: write the translation, then cut it to fit, then accept that the German short description is going to be a slightly different sentence than the English one. Don't translate; rewrite for the constraint.&lt;/p&gt;

&lt;p&gt;The long description has a floor as well as a ceiling — 250 to 2000 characters. A too-short translation fails validation just like a too-long one, which is easy to hit in languages where you are being terse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: search terms are a budget, not a field
&lt;/h2&gt;

&lt;p&gt;Edge gives you up to &lt;strong&gt;7 search terms per language&lt;/strong&gt;, and caps you at &lt;strong&gt;21 unique terms across the whole listing&lt;/strong&gt;. Not 21 per language — 21 total, deduplicated across all nine.&lt;/p&gt;

&lt;p&gt;That means the term you use in English ("cookie banner") and the term you use in German ("Cookie-Banner") count separately, but repeating "cookie" in six languages does not give you six shots at it. Two of my listings blew the global budget — 23 and 22 terms — because I had been treating each language as an independent listing.&lt;/p&gt;

&lt;p&gt;Plan the term set once, in a spreadsheet, before you touch the console. The console will let you type the eighth term and only complain at submit time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 3: the language dropdown is asynchronous and it will silently eat your work
&lt;/h2&gt;

&lt;p&gt;This one cost me an entire product's listing.&lt;/p&gt;

&lt;p&gt;In the Chrome Web Store dev console, the "edit language" control is a custom dropdown that loads its options asynchronously. If you switch language and start typing into the description field within a second or two, you are still writing into the &lt;em&gt;previous&lt;/em&gt; language's field. Nothing visually indicates this. The field looks empty because it re-renders, and your input lands wherever the model currently points.&lt;/p&gt;

&lt;p&gt;The result: every locale ended up holding the text of its neighbour. English field held German, German held Russian, and so on down the list. The store accepted it without complaint, because each field was a valid description of the right length.&lt;/p&gt;

&lt;p&gt;Two rules came out of that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After switching, poll the page until the visible "currently editing language" indicator actually names the language you asked for. Do not rely on a fixed sleep.&lt;/li&gt;
&lt;li&gt;Verify by reading the saved text back, per locale, with a &lt;strong&gt;page refresh between switches&lt;/strong&gt;. One page load can reliably switch language once; a second switch in the same load is where it goes wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read-back verification is the only thing that caught this. A screenshot would not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 4: naming other companies' products gets you rejected for keyword spam
&lt;/h2&gt;

&lt;p&gt;My first submission was rejected by an automated check within ten minutes.&lt;/p&gt;

&lt;p&gt;The cause was a sentence in the description listing the consent-management platforms the extension supports — eight third-party trademarks in a row. To a human that reads as a feature list. To the automated reviewer it reads as keyword stuffing.&lt;/p&gt;

&lt;p&gt;I rewrote it as a capability statement — what the extension does to a banner, not whose banner it is — and it went through. The same wording problem had already burned me once on a different product and a different store, so this is a pattern, not bad luck:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Describe what your software does. Do not enumerate other companies' names to prove coverage.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 5: in Edge, editing the Properties page saves a draft and nothing else
&lt;/h2&gt;

&lt;p&gt;The Partner Center has a page where you can edit your listing properties. Editing there and hitting save does &lt;em&gt;not&lt;/em&gt; publish anything. The change sits as a draft until you walk the full update flow — Properties, then Privacy, then Listings, then Publish, then Submit — and the certification notes field must be filled or the submit is blocked.&lt;/p&gt;

&lt;p&gt;Also, re-rendering between steps clears fields you already filled. In my case the certification notes textarea was wiped by clicking a radio button after typing. Fill the free-text explanation last.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification loop that actually works
&lt;/h2&gt;

&lt;p&gt;After all of that, the process I'd keep is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precheck locally.&lt;/strong&gt; Character counts, term counts, and a scan for third-party brand names — all checkable before upload, none checkable after rejection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fill one language at a time&lt;/strong&gt;, confirming the active language before typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read back every field&lt;/strong&gt;, refreshing between languages, and diff against your source text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish last&lt;/strong&gt;, and treat any field with free text as something that may be cleared by the next click.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is interesting engineering. It's just the difference between shipping in two days and shipping in two weeks.&lt;/p&gt;




&lt;p&gt;I build small, privacy-first browser extensions — everything runs locally, and when an AI feature needs a model you bring your own key. If the workflow above saved you a rejection, the extensions are at &lt;a href="https://glenkit.com" rel="noopener noreferrer"&gt;glenkit.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>browser</category>
      <category>productivity</category>
    </item>
    <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>
