<?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: Alexander Burnett</title>
    <description>The latest articles on DEV Community by Alexander Burnett (@10aburnett).</description>
    <link>https://dev.to/10aburnett</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%2F4081952%2Faae709d6-a9c0-49b0-a7fe-7f50ce1f2bb8.png</url>
      <title>DEV Community: Alexander Burnett</title>
      <link>https://dev.to/10aburnett</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/10aburnett"/>
    <language>en</language>
    <item>
      <title>A settings page for a Manifest V3 extension with chrome.storage.sync (and no framework)</title>
      <dc:creator>Alexander Burnett</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:16:41 +0000</pubDate>
      <link>https://dev.to/10aburnett/a-settings-page-for-a-manifest-v3-extension-with-chromestoragesync-and-no-framework-e82</link>
      <guid>https://dev.to/10aburnett/a-settings-page-for-a-manifest-v3-extension-with-chromestoragesync-and-no-framework-e82</guid>
      <description>&lt;p&gt;When I built the options page for &lt;a href="https://promodrop.codes" rel="noopener noreferrer"&gt;Promo Drop&lt;/a&gt;, I wanted three toggles, instant saves, and settings that follow the user to every Chrome they sign in to. No React, no build step. Here is the whole pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Declare the page in the manifest
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"options_ui"&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;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"options.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"open_in_tab"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"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;"storage"&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;code&gt;storage&lt;/code&gt; is the only permission this page needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. One source of defaults
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;showPanel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;showCashback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;compactMode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep this in one place and pass it to every &lt;code&gt;get&lt;/code&gt; call, so a first-time user never sees &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Load the saved values
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&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;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DEFAULTS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&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;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing the defaults object as the key means Chrome fills in anything missing for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Save on change, not on a Save button
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[type="checkbox"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One listener covers every toggle, and users never lose a change because they forgot to click Save.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. React to changes everywhere else
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onChanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;showPanel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;togglePanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;showPanel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newValue&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;Your content script updates live, in every open tab, the moment the user flips a switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that caught me out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;storage.sync&lt;/code&gt; has quotas (about 100 KB total and 8 KB per item). Settings fit easily; anything bigger belongs in &lt;code&gt;storage.local&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The sync write rate is limited, so never call &lt;code&gt;set&lt;/code&gt; inside a slider's &lt;code&gt;input&lt;/code&gt; event. Use &lt;code&gt;change&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test with a second Chrome profile signed in to the same account to watch sync happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the entire options page: one HTML file, about 30 lines of JavaScript, and nothing to build. The extension it belongs to lists tested Whop promo codes; you can see it at &lt;a href="https://promodrop.codes" rel="noopener noreferrer"&gt;promodrop.codes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>chromeextension</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What I Learned Shipping My First Chrome Extension to the Web Store</title>
      <dc:creator>Alexander Burnett</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:23:02 +0000</pubDate>
      <link>https://dev.to/10aburnett/what-i-learned-shipping-my-first-chrome-extension-to-the-web-store-42lb</link>
      <guid>https://dev.to/10aburnett/what-i-learned-shipping-my-first-chrome-extension-to-the-web-store-42lb</guid>
      <description>&lt;p&gt;I spent the last few months building and shipping a Chrome extension called Promo Drop, a free tool that finds verified promo codes and cashback for products on Whop. It is live on the Chrome Web Store now. This is a write-up of what I learned getting it there, minus the one part I cannot share (more on that below).&lt;/p&gt;

&lt;p&gt;Quick background on me: I studied at Lancaster University, spent a while day trading, and ended up buying courses and communities on Whop. I kept noticing two things. Working discount codes existed but were painful to find, and when there was no code, there was often no way to claw anything back. So I built the tool I wished existed.&lt;/p&gt;

&lt;p&gt;Here is what actually taught me something.&lt;/p&gt;

&lt;p&gt;Manifest V3 is a genuinely different mental model&lt;/p&gt;

&lt;p&gt;If your knowledge of extensions is a few years old, throw it out. There is no persistent background page anymore. You get a service worker that Chrome spins up and tears down whenever it feels like it, so you cannot lean on in-memory state surviving between events. Anything that needs to persist has to go into storage, and you have to design around the worker being asleep most of the time. Half my early bugs were just me assuming a variable would still be there. It would not be.&lt;/p&gt;

&lt;p&gt;The Web Store review process rewards being boring and honest&lt;/p&gt;

&lt;p&gt;The single biggest thing that smoothed my reviews: request the absolute minimum permissions, and write a plain-English justification for every single one. Reviewers are looking for extensions that ask for more than they need. If your permissions map cleanly onto features a user can see, and your privacy policy actually matches your behaviour, you make their job easy and you get through faster. I also kept the extension scoped to only run where it needs to, rather than on every site, which is both better for users and an easier review.&lt;/p&gt;

&lt;p&gt;Move your secret sauce off the client, on purpose&lt;/p&gt;

&lt;p&gt;Here is the part I will not detail: how Promo Drop actually determines the codes. That logic is the whole value of the product, so it does not live in the extension at all. The client is deliberately thin. It sends a URL to my server, and the server responds with the result. The extension itself contains nothing that would let someone reverse-engineer the method.&lt;/p&gt;

&lt;p&gt;This started as IP protection but turned out to be good engineering regardless. Anything shipped in an extension is fully readable by anyone who unzips the package, there is no such thing as a client-side secret. Pushing the real work server-side meant I could change the logic without shipping an update, cache results, rate-limit abuse, and keep the client tiny. If any part of your product is a competitive advantage, assume the client is public and design accordingly.&lt;/p&gt;

&lt;p&gt;The ethics line was a feature decision, not an afterthought&lt;/p&gt;

&lt;p&gt;Promo Drop earns an affiliate commission, and I made a rule early: attribution only ever happens on a genuine user action, and it never overwrites an affiliate link someone else set. No silent background cookie-dropping, no hijacking. Beyond it being the right thing, it is also just more durable, the aggressive stuff is exactly what gets extensions pulled and brands angry. Building the honest version from day one meant I never had to walk anything back.&lt;/p&gt;

&lt;p&gt;Nobody wants your UI in their face&lt;/p&gt;

&lt;p&gt;My first version threw a banner up the moment it had something. It worked, and it was annoying. I moved to a quieter model: a small indicator, and the details only when the user chooses to look. Engagement went up when I got out of the way. For anything that lives on top of someone else's page, restraint is the feature.&lt;/p&gt;

&lt;p&gt;Would I do it again?&lt;/p&gt;

&lt;p&gt;Yes. Shipping something real to a store with a review gate forces a discipline that side projects usually let you skip: least privilege, honest copy, a privacy policy you actually honour. Even if the extension went nowhere, I would have learned more than from ten unfinished repos.&lt;/p&gt;

&lt;p&gt;If you want to see the finished thing, it is at &lt;a href="https://promodrop.codes/" rel="noopener noreferrer"&gt;https://promodrop.codes/&lt;/a&gt; . Happy to answer questions about the MV3 or Web Store side in the comments.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
