<?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: Yu'an Zhou</title>
    <description>The latest articles on DEV Community by Yu'an Zhou (@yuanjzhou).</description>
    <link>https://dev.to/yuanjzhou</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%2F3997330%2F4386bed3-fe4a-4e5a-946a-520ea9954fbf.jpg</url>
      <title>DEV Community: Yu'an Zhou</title>
      <link>https://dev.to/yuanjzhou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuanjzhou"/>
    <language>en</language>
    <item>
      <title>Shipping a Chrome Extension That Works Across Sites You Do Not Control</title>
      <dc:creator>Yu'an Zhou</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:21:11 +0000</pubDate>
      <link>https://dev.to/yuanjzhou/shipping-a-chrome-extension-that-works-across-sites-you-do-not-control-b64</link>
      <guid>https://dev.to/yuanjzhou/shipping-a-chrome-extension-that-works-across-sites-you-do-not-control-b64</guid>
      <description>&lt;p&gt;Building a Chrome extension that saves items from arbitrary third-party sites means every site is a hostile, unstable environment. Here is what actually broke, and what held.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manifest V3 changes the shape of everything
&lt;/h2&gt;

&lt;p&gt;The persistent background page is gone. Service workers terminate — aggressively, after roughly 30 seconds of inactivity — and any in-memory state dies with them.&lt;/p&gt;

&lt;p&gt;Anything that must survive goes into &lt;code&gt;chrome.storage&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ dies with the worker&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ survives&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;local&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="nx"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the single most common source of "works when I test it, fails in the wild" bugs. Your worker is alive during active testing and dead during real usage patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content script isolation cuts both ways
&lt;/h2&gt;

&lt;p&gt;Content scripts run in an isolated world: you see the DOM, not the page's JavaScript. You cannot read the page's variables, and the page cannot see yours.&lt;/p&gt;

&lt;p&gt;Good for safety, awkward when the data you want lives in a framework's state rather than the DOM. Options are injecting into the main world (more risk, more capability) or scraping rendered DOM (fragile but isolated). I default to DOM scraping and accept the fragility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selectors rot, so degrade instead of breaking
&lt;/h2&gt;

&lt;p&gt;Third-party markup changes without warning. A selector working today breaks next week with no notice and no error you will see.&lt;/p&gt;

&lt;p&gt;What helps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Layered fallbacks&lt;/strong&gt; — semantic markup first (&lt;code&gt;article&lt;/code&gt;, &lt;code&gt;[itemprop]&lt;/code&gt;, OpenGraph tags), site-specific selectors only as a last resort&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail visibly, not silently&lt;/strong&gt; — if extraction fails, tell the user rather than saving an empty record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never assume shape&lt;/strong&gt; — &lt;code&gt;el?.textContent?.trim() ?? ''&lt;/code&gt; everywhere; a null deref in a content script can break the host page, which is much worse than your feature not working&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Permissions: ask for less
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt; triggers heavier review and scares users at install time. &lt;code&gt;activeTab&lt;/code&gt; plus &lt;code&gt;optional_host_permissions&lt;/code&gt; requested at first use is a better trade — fewer install-time objections, and review goes faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store review realities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Justify &lt;strong&gt;every&lt;/strong&gt; permission in the listing, specifically. Vague justifications get rejected.&lt;/li&gt;
&lt;li&gt;Privacy policy required if you touch user data at all.&lt;/li&gt;
&lt;li&gt;First review is slow; updates are usually much faster.&lt;/li&gt;
&lt;li&gt;A permission added later triggers full re-review — worth batching permission changes rather than shipping them one at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I shipped one of these for &lt;a href="https://babynameai.org" rel="noopener noreferrer"&gt;BabyNameAi&lt;/a&gt; — cross-site saving, a daily classical-poetry card, and offline lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;Extension APIs are still shifting under MV3. Anything you read from before 2023 — including plenty of still-top-ranked results — may describe APIs that no longer exist.&lt;/p&gt;

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