<?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: evisu-dev</title>
    <description>The latest articles on DEV Community by evisu-dev (@evisu-dev).</description>
    <link>https://dev.to/evisu-dev</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%2F3676171%2F0ffb571f-358d-4f22-ab35-ddf695ae2adb.png</url>
      <title>DEV Community: evisu-dev</title>
      <link>https://dev.to/evisu-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/evisu-dev"/>
    <language>en</language>
    <item>
      <title>Designing a Fail-Safe Announcement Widget for Third-Party Websites</title>
      <dc:creator>evisu-dev</dc:creator>
      <pubDate>Mon, 27 Jul 2026 12:01:11 +0000</pubDate>
      <link>https://dev.to/evisu-dev/designing-a-fail-safe-announcement-widget-for-third-party-websites-17mp</link>
      <guid>https://dev.to/evisu-dev/designing-a-fail-safe-announcement-widget-for-third-party-websites-17mp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvd69ntco2d9yp3xi6n1m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvd69ntco2d9yp3xi6n1m.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A third-party widget runs inside someone else’s product.&lt;/p&gt;

&lt;p&gt;That changes the engineering priority.&lt;/p&gt;

&lt;p&gt;If the widget API is slow, unavailable, misconfigured, or returns unexpected data, the customer’s website must continue working normally. The safest failure mode is often to render nothing at all.&lt;/p&gt;

&lt;p&gt;I encountered these constraints while building the announcement widget for SoloOps Dock, a lightweight public ops tool for solo SaaS founders.&lt;/p&gt;

&lt;p&gt;The widget is intentionally small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no framework runtime&lt;/li&gt;
&lt;li&gt;no external dependencies&lt;/li&gt;
&lt;li&gt;no arbitrary HTML rendering&lt;/li&gt;
&lt;li&gt;no requirement for the host application to wait for it&lt;/li&gt;
&lt;li&gt;no assumption that the network, browser, or configuration is perfect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to make failure impossible.&lt;/p&gt;

&lt;p&gt;The goal is to contain failure inside the widget boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The host website must come first
&lt;/h2&gt;

&lt;p&gt;An embedded widget has a different trust boundary from application code you fully control.&lt;/p&gt;

&lt;p&gt;It may run on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Laravel application&lt;/li&gt;
&lt;li&gt;a static site&lt;/li&gt;
&lt;li&gt;a React or Vue application&lt;/li&gt;
&lt;li&gt;a WordPress site&lt;/li&gt;
&lt;li&gt;a page with aggressive global CSS&lt;/li&gt;
&lt;li&gt;a page with a restrictive Content Security Policy&lt;/li&gt;
&lt;li&gt;a slow mobile connection&lt;/li&gt;
&lt;li&gt;a browser with limited API support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The widget also depends on a separate public API. That API may temporarily fail even when the host application is healthy.&lt;/p&gt;

&lt;p&gt;If the widget throws an uncaught exception, blocks rendering, injects unsafe markup, or leaves stale incident information visible, users may blame the host product rather than the widget provider.&lt;/p&gt;

&lt;p&gt;For that reason, I defined a few rules before implementing the UI.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A widget failure must not become a host-page failure.&lt;/li&gt;
&lt;li&gt;Missing configuration should be treated as a normal condition.&lt;/li&gt;
&lt;li&gt;Network requests must have hard time limits.&lt;/li&gt;
&lt;li&gt;Remote content must not be rendered as trusted HTML.&lt;/li&gt;
&lt;li&gt;Widget styles should not leak into the host page.&lt;/li&gt;
&lt;li&gt;Temporary network failures and stale operational content must be handled differently.&lt;/li&gt;
&lt;li&gt;The host application must never depend on the widget being available.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those rules shaped almost every implementation decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a defensive bootstrap
&lt;/h2&gt;

&lt;p&gt;The widget is loaded with a script tag similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/widget.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-project=&lt;/span&gt;&lt;span class="s"&gt;"public_project_key"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bootstrap code does as little as possible before validating its environment.&lt;/p&gt;

&lt;p&gt;A simplified version looks like this:&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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&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;script&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="nx"&gt;currentScript&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&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;publicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-project&lt;/span&gt;&lt;span class="dl"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;fetchAndSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicKey&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="c1"&gt;// Contain the failure inside the widget.&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;There are a few deliberate choices here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use an IIFE
&lt;/h3&gt;

&lt;p&gt;An immediately invoked function expression keeps internal variables out of the global scope.&lt;/p&gt;

&lt;p&gt;Third-party scripts should avoid creating names that may conflict with the host application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate configuration before doing work
&lt;/h3&gt;

&lt;p&gt;A copied embed snippet may be incomplete. A template may accidentally remove a data attribute. A developer may paste the script before creating the corresponding project.&lt;/p&gt;

&lt;p&gt;These are expected operational mistakes, not exceptional events that justify breaking the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catch at the outer boundary
&lt;/h3&gt;

&lt;p&gt;Catching everything at the top is not a replacement for proper error handling inside individual functions.&lt;/p&gt;

&lt;p&gt;It is the final containment boundary.&lt;/p&gt;

&lt;p&gt;If an unexpected browser behavior or coding mistake escapes lower-level handling, the widget should still fail without affecting the host application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a hard time limit on network requests
&lt;/h2&gt;

&lt;p&gt;A normal &lt;code&gt;fetch()&lt;/code&gt; call does not give the product-level guarantee I wanted.&lt;/p&gt;

&lt;p&gt;The widget is optional UI. It should not keep waiting indefinitely for a remote service.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;AbortController&lt;/code&gt; to enforce a timeout:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchAnnouncement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;controller&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;AbortController&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;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&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 exact timeout is a product decision, not a universal constant.&lt;/p&gt;

&lt;p&gt;For this widget, 2.5 seconds is already generous. The announcement bar is not required for the host page to function, so waiting much longer provides little value.&lt;/p&gt;

&lt;p&gt;The important behavior is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;abort slow requests&lt;/li&gt;
&lt;li&gt;treat non-2xx responses as unavailable data&lt;/li&gt;
&lt;li&gt;treat invalid JSON as unavailable data&lt;/li&gt;
&lt;li&gt;do not show a widget-specific error screen&lt;/li&gt;
&lt;li&gt;do not throw into the host page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A failed announcement request should not produce a large red error banner on the customer’s product. That would turn an optional communication feature into a visible outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporary failure and stale content are different problems
&lt;/h2&gt;

&lt;p&gt;Immediately removing a widget after one failed request creates another problem: flicker.&lt;/p&gt;

&lt;p&gt;A user may briefly lose connectivity. A CDN edge may have a short error. A browser may abort a request while the tab is changing state.&lt;/p&gt;

&lt;p&gt;If the widget disappears after every isolated failure, the interface becomes unstable.&lt;/p&gt;

&lt;p&gt;The implementation therefore keeps track of the last successful synchronization.&lt;/p&gt;

&lt;p&gt;A simplified version:&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;let&lt;/span&gt; &lt;span class="nx"&gt;lastSuccessfulSyncAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;REVALIDATE_INTERVAL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&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;STALE_MAX_AGE_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSuccessfulSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lastSuccessfulSyncAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;syncWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleFailedSync&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;staleFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastSuccessfulSyncAt&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;staleFor&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;STALE_MAX_AGE_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;removeWidget&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 flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successful API response
        ↓
Render or update the widget
        ↓
A temporary request fails
        ↓
Keep the current widget for a short grace period
        ↓
The API remains unreachable
        ↓
Remove the stale widget
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction matters for operational messages.&lt;/p&gt;

&lt;p&gt;A short network failure should not immediately hide a useful announcement.&lt;/p&gt;

&lt;p&gt;But an old message such as “Major outage in progress” must not remain visible indefinitely after the widget can no longer confirm that it is current.&lt;/p&gt;

&lt;p&gt;The widget revalidates every 60 seconds and removes mounted content after three minutes without a successful response.&lt;/p&gt;

&lt;p&gt;These values can change, but the broader principle is stable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Preserve the current state during brief uncertainty, then remove it when it can no longer be trusted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why the announcement response uses &lt;code&gt;no-store&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Caching is usually desirable for a public read-only API.&lt;/p&gt;

&lt;p&gt;Announcement visibility, however, is highly state-sensitive.&lt;/p&gt;

&lt;p&gt;The response may change because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a scheduled announcement reaches its start time&lt;/li&gt;
&lt;li&gt;an announcement reaches its end time&lt;/li&gt;
&lt;li&gt;the project is made private&lt;/li&gt;
&lt;li&gt;the active announcement changes&lt;/li&gt;
&lt;li&gt;the widget is disabled&lt;/li&gt;
&lt;li&gt;the account loses eligibility&lt;/li&gt;
&lt;li&gt;the owner removes or edits the announcement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An earlier version allowed the response to remain cached. That introduced two opposite failure modes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A previously visible announcement could remain visible after it should have been hidden.&lt;/li&gt;
&lt;li&gt;A previously hidden response could delay a newly activated announcement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For operational communication, both are trust problems.&lt;/p&gt;

&lt;p&gt;The current public endpoint returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: no-store
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser request also uses:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This increases API traffic compared with a cached endpoint, but it keeps visibility decisions current.&lt;/p&gt;

&lt;p&gt;The trade-off is intentional:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For operational messages, correctness of the current state is more important than maximizing cacheability.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A more advanced implementation could safely reintroduce caching by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;limiting TTL to the next scheduled state transition&lt;/li&gt;
&lt;li&gt;purging cache entries when project or announcement state changes&lt;/li&gt;
&lt;li&gt;separating public content from eligibility state&lt;/li&gt;
&lt;li&gt;using versioned response URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small MVP, &lt;code&gt;no-store&lt;/code&gt; is the simpler and safer boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolate styles with Shadow DOM
&lt;/h2&gt;

&lt;p&gt;A third-party widget has two CSS problems.&lt;/p&gt;

&lt;p&gt;First, its styles may affect the host page.&lt;/p&gt;

&lt;p&gt;Second, the host page may break the widget.&lt;/p&gt;

&lt;p&gt;Global selectors such as these are common:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&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;A widget that assumes a clean environment may render differently on every site.&lt;/p&gt;

&lt;p&gt;When available, the widget attaches a closed Shadow DOM:&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;container&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sod-widget&lt;/span&gt;&lt;span class="dl"&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;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachShadow&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&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;container&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;style&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getWidgetStyles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildWidgetContent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isolates the widget’s style tree from most host-page CSS.&lt;/p&gt;

&lt;p&gt;The fallback uses the normal DOM for older environments, so the widget can still render even when Shadow DOM is unavailable.&lt;/p&gt;

&lt;p&gt;There is an important distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shadow DOM isolates styling. It does not sandbox JavaScript execution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The script still runs in the host page’s JavaScript context. It can access browser APIs and the DOM because it is not inside an iframe sandbox.&lt;/p&gt;

&lt;p&gt;The implementation deliberately limits itself to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reading attributes from its own script element&lt;/li&gt;
&lt;li&gt;creating its own container&lt;/li&gt;
&lt;li&gt;reading its own local storage keys&lt;/li&gt;
&lt;li&gt;calling its own public API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a behavioral restriction in the code, not a browser-enforced security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render text, not HTML
&lt;/h2&gt;

&lt;p&gt;The API response is produced by my own backend, but I still treat it as untrusted at the rendering boundary.&lt;/p&gt;

&lt;p&gt;The widget does not assign remote content to &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, it creates elements and uses &lt;code&gt;textContent&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;widget-title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&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;body&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;widget-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;||&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;This prevents strings such as the following from becoming executable markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;alert(1)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, Markdown is converted into a plain-text excerpt before it reaches the widget payload.&lt;/p&gt;

&lt;p&gt;The public page can render sanitized Markdown, but the embedded announcement bar has a narrower responsibility. It only needs a short text summary.&lt;/p&gt;

&lt;p&gt;The payload also applies explicit limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;title: up to 80 characters&lt;/li&gt;
&lt;li&gt;body excerpt: up to 160 characters&lt;/li&gt;
&lt;li&gt;link label: up to 40 characters&lt;/li&gt;
&lt;li&gt;link URL: only &lt;code&gt;http&lt;/code&gt; or &lt;code&gt;https&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Links open in a new tab with:&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="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_blank&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noopener noreferrer&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;The general lesson is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A public API response should still be treated as untrusted input when it is rendered inside a customer’s page.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Avoid duplicate mounts and unnecessary re-renders
&lt;/h2&gt;

&lt;p&gt;Embed scripts may be included twice by mistake.&lt;/p&gt;

&lt;p&gt;Single-page applications may execute lifecycle code more than once.&lt;/p&gt;

&lt;p&gt;Periodic revalidation may return the same announcement repeatedly.&lt;/p&gt;

&lt;p&gt;The widget therefore checks for an existing container before mounting:&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;function&lt;/span&gt; &lt;span class="nf"&gt;renderWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sod-widget&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Create and append the widget.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also tracks the current announcement identifier and update timestamp.&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;let&lt;/span&gt; &lt;span class="nx"&gt;currentAnnouncementId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentUpdatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a response arrives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;show: false&lt;/code&gt; removes the current widget&lt;/li&gt;
&lt;li&gt;a different announcement ID triggers replacement&lt;/li&gt;
&lt;li&gt;a changed &lt;code&gt;updated_at&lt;/code&gt; value triggers replacement&lt;/li&gt;
&lt;li&gt;an unchanged response does nothing&lt;/li&gt;
&lt;li&gt;a missing widget with &lt;code&gt;show: true&lt;/code&gt; mounts it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That prevents duplicate UI and unnecessary DOM churn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make dismissals version-aware
&lt;/h2&gt;

&lt;p&gt;A dismissible announcement needs persistence.&lt;/p&gt;

&lt;p&gt;The obvious implementation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dismissed:{announcement_id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is incomplete.&lt;/p&gt;

&lt;p&gt;Suppose the owner edits the announcement after a user dismisses it. The updated message may contain important new information, but the user will never see it because the old dismissal still applies.&lt;/p&gt;

&lt;p&gt;The widget includes the announcement version in the storage key:&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;dismissKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;widget:dismissed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;projectKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;announcement&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;announcement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&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;The user’s dismissal applies only to that specific version.&lt;/p&gt;

&lt;p&gt;If the owner edits the message, &lt;code&gt;updated_at&lt;/code&gt; changes and the new version can appear again.&lt;/p&gt;

&lt;p&gt;This is a small implementation detail, but it makes the difference between “dismiss this message” and “never show this announcement again.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep installation verification separate from display delivery
&lt;/h2&gt;

&lt;p&gt;The public announcement key is intentionally safe to expose. It allows read-only access to the announcement payload.&lt;/p&gt;

&lt;p&gt;Installation verification is a different concern.&lt;/p&gt;

&lt;p&gt;The widget may need to prove that it has been installed on the configured site, but a copied public key alone should not be enough to mark an installation as verified.&lt;/p&gt;

&lt;p&gt;The implementation separates the two workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public project key
→ read-only announcement delivery

Short-lived installation challenge
→ one-time installation verification

Heartbeat credential
→ ongoing last-seen updates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installation workflow uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a short-lived, one-time challenge&lt;/li&gt;
&lt;li&gt;browser-generated random credentials&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crypto.getRandomValues&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;periodic heartbeats after successful verification&lt;/li&gt;
&lt;li&gt;host validation on the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not fall back to &lt;code&gt;Math.random()&lt;/code&gt; for credential generation.&lt;/p&gt;

&lt;p&gt;The full verification flow involves retry safety, cross-tab coordination, challenge consumption, token rotation, and transactional server updates. That deserves a separate article.&lt;/p&gt;

&lt;p&gt;The important architectural decision here is that display delivery remains simple even when verification cannot run.&lt;/p&gt;

&lt;p&gt;A browser that cannot complete installation verification should still be able to display an announcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features I deliberately did not add
&lt;/h2&gt;

&lt;p&gt;A third-party widget can easily grow into a small frontend platform.&lt;/p&gt;

&lt;p&gt;For the first version, I deliberately avoided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React or Vue runtime dependencies&lt;/li&gt;
&lt;li&gt;arbitrary custom HTML&lt;/li&gt;
&lt;li&gt;customer-provided JavaScript callbacks&lt;/li&gt;
&lt;li&gt;advanced targeting rules&lt;/li&gt;
&lt;li&gt;per-visitor analytics&lt;/li&gt;
&lt;li&gt;complex animation&lt;/li&gt;
&lt;li&gt;multiple visual widget types&lt;/li&gt;
&lt;li&gt;automatic incident creation&lt;/li&gt;
&lt;li&gt;deep access to the host application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These features may be useful later, but each increases one or more of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bundle size&lt;/li&gt;
&lt;li&gt;security surface&lt;/li&gt;
&lt;li&gt;support load&lt;/li&gt;
&lt;li&gt;integration complexity&lt;/li&gt;
&lt;li&gt;risk of breaking customer sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product goal was not to build the most customizable announcement system.&lt;/p&gt;

&lt;p&gt;It was to provide a small, predictable operational communication layer for solo SaaS products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure paths, not only the happy path
&lt;/h2&gt;

&lt;p&gt;The most valuable widget tests are often the cases where nothing should happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bootstrap cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the script element cannot be resolved&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-project&lt;/code&gt; is missing&lt;/li&gt;
&lt;li&gt;the public key is malformed&lt;/li&gt;
&lt;li&gt;the script is loaded twice&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Network cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the API returns 500&lt;/li&gt;
&lt;li&gt;the response body is invalid JSON&lt;/li&gt;
&lt;li&gt;the request times out&lt;/li&gt;
&lt;li&gt;the browser is offline&lt;/li&gt;
&lt;li&gt;CORS blocks the request&lt;/li&gt;
&lt;li&gt;the API remains unavailable beyond the stale threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rendering cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the title is empty&lt;/li&gt;
&lt;li&gt;the body is empty&lt;/li&gt;
&lt;li&gt;the payload contains HTML-like text&lt;/li&gt;
&lt;li&gt;the link uses an unsupported URL scheme&lt;/li&gt;
&lt;li&gt;the host page has aggressive global CSS&lt;/li&gt;
&lt;li&gt;Shadow DOM is unavailable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  State transition cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;show: true&lt;/code&gt; becomes &lt;code&gt;show: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the active announcement changes&lt;/li&gt;
&lt;li&gt;the message is edited&lt;/li&gt;
&lt;li&gt;the end time is reached&lt;/li&gt;
&lt;li&gt;the user dismisses a message and the owner later edits it&lt;/li&gt;
&lt;li&gt;the widget is disabled while a message is visible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The expected result is not always “the widget is visible.”&lt;/p&gt;

&lt;p&gt;Often, the correct result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nothing is rendered.
No exception escapes.
The host application continues normally.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trade-offs and current limitations
&lt;/h2&gt;

&lt;p&gt;This design contains failures, but it does not eliminate every risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shadow DOM is not a sandbox
&lt;/h3&gt;

&lt;p&gt;The widget script runs in the host page context. Style isolation should not be described as full execution isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Revalidation is not real-time
&lt;/h3&gt;

&lt;p&gt;The client checks for changes periodically. Updates may take up to one polling interval to appear.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;no-store&lt;/code&gt; increases API traffic
&lt;/h3&gt;

&lt;p&gt;Fresh visibility state is prioritized over browser and intermediary caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local storage may be unavailable
&lt;/h3&gt;

&lt;p&gt;The main announcement can still render, but dismiss persistence and installation heartbeat behavior may be limited.&lt;/p&gt;

&lt;h3&gt;
  
  
  The host site must allow the connections
&lt;/h3&gt;

&lt;p&gt;A restrictive Content Security Policy may need to allow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the widget script origin in &lt;code&gt;script-src&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the public API origin in &lt;code&gt;connect-src&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  JavaScript can always fail
&lt;/h3&gt;

&lt;p&gt;The design goal is not “this code can never fail.”&lt;/p&gt;

&lt;p&gt;The goal is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the widget fails, the failure should remain optional, local, and invisible to the host application’s core workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Third-party widgets live in a trust-sensitive environment: someone else’s product.&lt;/p&gt;

&lt;p&gt;Their most important behavior is not what happens when everything works.&lt;/p&gt;

&lt;p&gt;It is what happens when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the API is slow&lt;/li&gt;
&lt;li&gt;the configuration is wrong&lt;/li&gt;
&lt;li&gt;the browser lacks a feature&lt;/li&gt;
&lt;li&gt;the network disappears&lt;/li&gt;
&lt;li&gt;the response is malformed&lt;/li&gt;
&lt;li&gt;cached operational state becomes stale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design principles I would reuse are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Validate early.
Time out quickly.
Render text, not HTML.
Isolate styles.
Do not trust stale operational state.
Keep temporary failures temporary.
Remove UI when it can no longer be trusted.
Never make the host application depend on the widget.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The widget itself is not the core application. That is precisely why it needs to behave responsibly when everything around it goes wrong.&lt;/p&gt;




&lt;p&gt;Disclosure: I used AI assistance to review the English wording and article structure. The technical decisions and implementation details are based on my own development work and were reviewed by me before publication.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>OpenAI Changelog Slack Alerts (Low-Noise Setup)</title>
      <dc:creator>evisu-dev</dc:creator>
      <pubDate>Wed, 24 Dec 2025 09:26:20 +0000</pubDate>
      <link>https://dev.to/evisu-dev/openai-changelog-slack-alerts-low-noise-setup-1nfc</link>
      <guid>https://dev.to/evisu-dev/openai-changelog-slack-alerts-low-noise-setup-1nfc</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlh06wktt6h5a18p3qdw.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlh06wktt6h5a18p3qdw.jpeg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: QuietWatch is no longer actively maintained. This article remains available as an archive of the product’s design and low-noise notification approach.&lt;/p&gt;

&lt;p&gt;If you’re building on OpenAI, you probably have the changelog open in a browser tab somewhere.&lt;/p&gt;

&lt;p&gt;I did too.&lt;/p&gt;

&lt;p&gt;It also lived in my “I’ll check it later” pile… right up until the day it mattered in production.&lt;/p&gt;

&lt;p&gt;So I set up something boring—but reliable: &lt;strong&gt;one Slack ping only when the changelog changes.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
No dashboards. No “still alive” messages. No noisy alert fatigue.&lt;/p&gt;

&lt;p&gt;This post shows how to do it in a few minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a Slack Incoming Webhook.&lt;/li&gt;
&lt;li&gt;Paste it into QuietWatch.&lt;/li&gt;
&lt;li&gt;Get a Slack ping only when the OpenAI changelog updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Change happens → ping.&lt;br&gt;&lt;br&gt;
No change → silence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why bother?
&lt;/h2&gt;

&lt;p&gt;Changelog updates don’t wait for your schedule.&lt;/p&gt;

&lt;p&gt;Here are the usual ways this goes wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You hear about an API change from a teammate… or a user.&lt;/li&gt;
&lt;li&gt;Something breaks (or feels “off”), and the changelog becomes the late answer key.&lt;/li&gt;
&lt;li&gt;You try to monitor everything, Slack gets loud, and the channel gets muted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn’t more alerts.&lt;br&gt;&lt;br&gt;
It’s &lt;strong&gt;a signal you’ll actually notice&lt;/strong&gt; when it matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  What “quiet by default” means
&lt;/h2&gt;

&lt;p&gt;This setup is intentionally minimal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detect change, then notify.&lt;/strong&gt; Otherwise, do nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack-first&lt;/strong&gt;, because teams already live there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No details in Slack.&lt;/strong&gt; Slack is the signal; the site is the context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a notification needs paragraphs to understand, it will eventually get ignored. That’s just reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;QuietWatch checks the OpenAI changelog on a schedule.&lt;/li&gt;
&lt;li&gt;When it detects an update, it sends a short, fixed Slack message: “something changed.”&lt;/li&gt;
&lt;li&gt;You click through to review details on the site.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup (about 5 minutes)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create a Slack Incoming Webhook
&lt;/h3&gt;

&lt;p&gt;In Slack, create an Incoming Webhook for the channel you want. Example: &lt;code&gt;#openai-changelog-alerts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You’ll get a URL like:&lt;br&gt;&lt;br&gt;
&lt;a href="https://hooks.slack.com/services/XXX/YYY/ZZZ" rel="noopener noreferrer"&gt;https://hooks.slack.com/services/XXX/YYY/ZZZ&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add it to QuietWatch
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open QuietWatch: &lt;a href="https://openai.quietwatch.io/add" rel="noopener noreferrer"&gt;https://openai.quietwatch.io/add&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create a monitor for the OpenAI changelog.&lt;/li&gt;
&lt;li&gt;Paste your webhook URL into the notification destination.&lt;/li&gt;
&lt;li&gt;Save.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Confirm it works
&lt;/h3&gt;

&lt;p&gt;If the UI supports a test send, use it. Otherwise, just wait for the next scheduled check.&lt;/p&gt;

&lt;p&gt;Tip: use a dedicated channel (e.g. &lt;code&gt;#vendor-openai-changelog&lt;/code&gt;) so these pings don’t mix with incident alerts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a “good” Slack alert looks like (for this use case)
&lt;/h2&gt;

&lt;p&gt;A notification should be readable in ~3 seconds.&lt;/p&gt;

&lt;p&gt;For changelog alerts, you really only need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change&lt;/strong&gt;: did something update?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target&lt;/strong&gt;: what changed (OpenAI changelog)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next&lt;/strong&gt;: click through and review details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything more tends to become noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Will this spam Slack?
&lt;/h3&gt;

&lt;p&gt;No. If it’s working correctly, you only get messages when the changelog changes. Silence is the default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I choose the channel or workspace?
&lt;/h3&gt;

&lt;p&gt;Yes. Incoming Webhooks are tied to a channel, so you control where it posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Email notifications?
&lt;/h3&gt;

&lt;p&gt;Not at the moment. QuietWatch focuses on Slack for the fastest “I’ll notice changes” loop.&lt;br&gt;&lt;br&gt;
If enough people ask for email later, it can be considered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this catch every breaking change?
&lt;/h3&gt;

&lt;p&gt;It catches changelog updates. It’s not a replacement for staging/tests—but it &lt;em&gt;is&lt;/em&gt; a solid early heads-up without living on the changelog page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Any tips to avoid alert fatigue?
&lt;/h3&gt;

&lt;p&gt;Dedicated channel, short format, and don’t mix it with incident alerts. Separate streams stay readable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Done. Now forget about it.
&lt;/h2&gt;

&lt;p&gt;If you want OpenAI changelog updates to land in Slack—quietly and reliably—start here:&lt;/p&gt;

&lt;p&gt;You’ll probably forget it’s running.&lt;br&gt;&lt;br&gt;
But when something changes, you’ll be glad it’s there.&lt;/p&gt;

</description>
      <category>openai</category>
      <category>slack</category>
      <category>monitoring</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
