[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-service-worker-stale-cache-deploy":44,"search-suggestions":58},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"slug":45,"title":46,"summary":47,"html":48,"css":49,"js":50,"config":51,"doc":53},"service-worker-stale-cache-deploy","Service worker deploy — interactive playground",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 Why your users are still seeing the old bug\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Three open tabs, one deploy. Pick a caching strategy and a lifecycle option, hit\n      \u003Cstrong>Deploy new version\u003C\u002Fstrong>, and watch which tabs actually get the fix.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Service worker settings\u003C\u002Fdiv>\n      \u003Cp class=\"hint\">This is the config the fetch handler and lifecycle hooks are built with — exactly like the article's two code blocks.\u003C\u002Fp>\n\n      \u003Cdiv class=\"field\">\n        \u003Cdiv class=\"ctl-label\">Caching strategy for the HTML\u002FJS bundle\u003C\u002Fdiv>\n        \u003Cdiv class=\"seg\" id=\"strategySeg\">\n          \u003Cbutton aria-pressed=\"true\" data-val=\"cache-first\">cache-first\u003C\u002Fbutton>\n          \u003Cbutton aria-pressed=\"false\" data-val=\"network-first\">network-first\u003C\u002Fbutton>\n          \u003Cbutton aria-pressed=\"false\" data-val=\"swr\">stale-while-revalidate\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"btn-row\" style=\"margin-top:14px\">\n        \u003Clabel class=\"toggle\">\n          \u003Cinput type=\"checkbox\" id=\"skipWaiting\" \u002F>\u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>\u003Ccode>skipWaiting()\u003C\u002Fcode> in install\u003C\u002Fspan>\n        \u003C\u002Flabel>\n        \u003Clabel class=\"toggle\">\n          \u003Cinput type=\"checkbox\" id=\"clientsClaim\" \u002F>\u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>\u003Ccode>clients.claim()\u003C\u002Fcode> in activate\u003C\u002Fspan>\n        \u003C\u002Flabel>\n        \u003Clabel class=\"toggle\">\n          \u003Cinput type=\"checkbox\" id=\"versionBump\" \u002F>\u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>versioned cache name (evicts old cache)\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"presets\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn sm\" data-preset=\"broken\">Preset: the common broken setup\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-preset=\"fixed\">Preset: the full fix\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Three users, mid-session\u003C\u002Fdiv>\n      \u003Cp class=\"hint\">Each tab loaded the site before today's deploy and has been sitting open since.\u003C\u002Fp>\n      \u003Cdiv class=\"tabs\" id=\"tabs\">\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\">\n        \u003Cbutton class=\"btn primary\" id=\"deploy\">🚀 Deploy new version\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost\" id=\"reset\">Reset\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> What the cache actually holds\u003C\u002Fdiv>\n      \u003Cdiv class=\"lifecycle\" id=\"lifecycle\">\n        \u003Cspan class=\"step\" data-step=\"idle\">idle\u003C\u002Fspan>\n        \u003Cspan class=\"step\" data-step=\"install\">install\u003C\u002Fspan>\n        \u003Cspan class=\"step\" data-step=\"waiting\">waiting\u003C\u002Fspan>\n        \u003Cspan class=\"step\" data-step=\"activate\">activate\u003C\u002Fspan>\n        \u003Cspan class=\"step\" data-step=\"controlling\">controlling\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"cache-list\" id=\"cacheList\">—\u003C\u002Fdiv>\n      \u003Cdiv class=\"status\" id=\"status\" style=\"margin-top:12px\">\n        \u003Cspan class=\"dot info\" id=\"dot\">\u003C\u002Fspan>\u003Cspan id=\"statusText\">Ready — set your options, then deploy.\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"hint\">Caption: this narrates exactly what each tab would receive on its next request.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>","\u002F*\n * Shared playground design system — daily-post\n * ============================================\n * Link it from any post's playground\u002Findex.html (no per-post boilerplate):\n *\n *   \u003Clink rel=\"stylesheet\" href=\"..\u002F..\u002F..\u002Fplayground\u002Fassets\u002Fbase.css\" \u002F>\n *\n * The relative path resolves the same locally (posts\u002F\u003Cbase>\u002Fplayground\u002F) and on\n * the deployed host (public\u002Fposts\u002F\u003Cbase>\u002Fplayground\u002F), because build-site.js\n * mirrors this file to public\u002Fplayground\u002Fassets\u002Fbase.css. For a CodeSandbox\n * --embed, playground.js inlines this file so the single-file sandbox still styles.\n *\n * Then write ONLY the demo-specific CSS in your inline \u003Cstyle>. Everything below\n * — tokens, layout, panels, buttons, toggles, sliders, code blocks, status\u002Flog,\n * banners, presets — comes for free and looks consistent across every post.\n *\n * Theme-aware: dark by default, light via prefers-color-scheme AND a\n * [data-theme] override on \u003Chtml> (so a toggle can force either). Accessible:\n * real focus rings, adequate contrast, reduced-motion honored.\n *\u002F\n\n\u002F* ---------- design tokens ---------- *\u002F\n:root {\n  --bg: #0d1117;\n  --bg-grid: #ffffff0a;\n  --panel: #161b22;\n  --panel-2: #0b0e14;\n  --card: #0b0e14;\n  --ink: #e8ebf2;\n  --muted: #9aa4b2;\n  --faint: #6b7688;\n  --line: #232a35;\n  --line-soft: #ffffff12;\n  --accent: #7c9cff;\n  --accent-ink: #0b0e14;\n  --good: #3fb950;\n  --warn: #f0883e;\n  --bad: #f85149;\n  --info: #58a6ff;\n  --radius: 14px;\n  --radius-sm: 9px;\n  --shadow: 0 8px 30px #0006;\n  --shadow-sm: 0 4px 14px #0005;\n  --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  --sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  color-scheme: dark;\n}\n\n\u002F* Light theme — system preference, overridable by [data-theme] on \u003Chtml>. *\u002F\n@media (prefers-color-scheme: light) {\n  :root:not([data-theme=\"dark\"]) {\n    --bg: #f4f6fb;\n    --bg-grid: #00000008;\n    --panel: #ffffff;\n    --panel-2: #f0f3f9;\n    --card: #f7f9fc;\n    --ink: #131722;\n    --muted: #5b6472;\n    --faint: #8a93a3;\n    --line: #e2e7f0;\n    --line-soft: #00000010;\n    --accent: #2f6df0;\n    --accent-ink: #ffffff;\n    --good: #1a7f37;\n    --warn: #bc4c00;\n    --bad: #cf222e;\n    --info: #0969da;\n    --shadow: 0 8px 30px #0000001f;\n    --shadow-sm: 0 4px 14px #00000014;\n    color-scheme: light;\n  }\n}\n:root[data-theme=\"light\"] {\n  --bg: #f4f6fb;\n  --bg-grid: #00000008;\n  --panel: #ffffff;\n  --panel-2: #f0f3f9;\n  --card: #f7f9fc;\n  --ink: #131722;\n  --muted: #5b6472;\n  --faint: #8a93a3;\n  --line: #e2e7f0;\n  --line-soft: #00000010;\n  --accent: #2f6df0;\n  --accent-ink: #ffffff;\n  --good: #1a7f37;\n  --warn: #bc4c00;\n  --bad: #cf222e;\n  --info: #0969da;\n  --shadow: 0 8px 30px #0000001f;\n  --shadow-sm: 0 4px 14px #00000014;\n  color-scheme: light;\n}\n\n\u002F* ---------- reset & base ---------- *\u002F\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  font-family: var(--sans);\n  color: var(--ink);\n  background:\n    radial-gradient(circle at 1px 1px, var(--bg-grid) 1px, transparent 0) 0 0 \u002F 24px 24px,\n    var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: flex-start;\n  padding: clamp(14px, 3vw, 26px);\n  line-height: 1.55;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n\u002F* The single content column. *\u002F\n.pg {\n  width: 100%;\n  max-width: 860px;\n}\n\n\u002F* ---------- header ---------- *\u002F\n.pg-title {\n  font-size: clamp(1.15rem, 1rem + 1vw, 1.4rem);\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  margin: 0 0 4px;\n  display: flex;\n  align-items: center;\n  gap: 9px;\n}\n.pg-sub {\n  color: var(--muted);\n  font-size: 0.92rem;\n  line-height: 1.5;\n  margin: 0 0 16px;\n  max-width: 66ch;\n}\n.pg-sub strong, .pg-sub b { color: var(--ink); font-weight: 700; }\n.pg-sub code { font-family: var(--mono); background: var(--line-soft); color: var(--accent);\n  padding: 1px 6px; border-radius: 6px; font-size: 0.85em; }\n\n\u002F* ---------- panels ---------- *\u002F\n.panel {\n  background: var(--panel);\n  border: 1px solid var(--line);\n  border-radius: var(--radius);\n  padding: 16px 18px;\n  margin-bottom: 14px;\n  box-shadow: var(--shadow-sm);\n}\n.panel-label {\n  font-size: 0.7rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--muted);\n  font-weight: 800;\n  margin: 0 0 13px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.panel-label .n {\n  display: inline-grid; place-items: center;\n  width: 18px; height: 18px; border-radius: 50%;\n  background: var(--accent); color: var(--accent-ink);\n  font-size: 0.66rem; font-weight: 800;\n}\n\n\u002F* ---------- layout helpers ---------- *\u002F\n.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }\n.row.between { justify-content: space-between; }\n.col { display: flex; flex-direction: column; gap: 10px; }\n.grow { flex: 1 1 0; min-width: 0; }\n.wrap-controls { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-end; }\n.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.field > .label, .ctl-label {\n  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;\n  color: var(--muted); font-weight: 800;\n}\n\n\u002F* ---------- buttons ---------- *\u002F\n.btn {\n  font: inherit; font-weight: 650; font-size: 0.88rem;\n  cursor: pointer; user-select: none;\n  border: 1px solid var(--line);\n  background: var(--card); color: var(--ink);\n  padding: 9px 15px; border-radius: var(--radius-sm);\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: background .15s ease, border-color .15s ease, transform .05s ease, color .15s ease;\n}\n.btn:hover { background: var(--line-soft); border-color: var(--muted); }\n.btn:active { transform: translateY(1px); }\n.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.btn[disabled] { opacity: 0.45; cursor: not-allowed; }\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }\n.btn.primary:hover { filter: brightness(1.08); background: var(--accent); }\n.btn.good { background: var(--good); border-color: var(--good); color: #04140a; }\n.btn.bad  { background: var(--bad);  border-color: var(--bad);  color: #fff; }\n.btn.ghost { background: transparent; }\n.btn.sm { padding: 6px 11px; font-size: 0.82rem; }\n\n.btn-row { display: flex; flex-wrap: wrap; gap: 9px; }\n\n\u002F* Segmented control — a row of buttons where one is active (aria-pressed). *\u002F\n.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line);\n  border-radius: var(--radius-sm); padding: 3px; gap: 3px; }\n.seg button {\n  font: inherit; font-weight: 650; font-size: 0.84rem; cursor: pointer;\n  border: none; background: transparent; color: var(--muted);\n  padding: 7px 13px; border-radius: 7px;\n  transition: background .15s ease, color .15s ease;\n}\n.seg button:hover { color: var(--ink); }\n.seg button[aria-pressed=\"true\"] { background: var(--accent); color: var(--accent-ink); }\n.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* Preset chips — worked-example shortcuts. *\u002F\n.presets { display: flex; flex-wrap: wrap; gap: 8px; }\n.presets .btn { background: var(--panel-2); }\n\n\u002F* ---------- toggle switch ---------- *\u002F\n.toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer;\n  font-size: 0.88rem; font-weight: 600; user-select: none; }\n.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }\n.switch { position: relative; width: 44px; height: 25px; border-radius: 999px; flex: none;\n  background: var(--line); border: 1px solid var(--line); transition: background .18s ease; }\n.switch::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;\n  border-radius: 50%; background: #cfd6e4; transition: transform .18s ease; box-shadow: 0 1px 3px #0006; }\n.toggle input:checked + .switch { background: var(--good); border-color: var(--good); }\n.toggle input:checked + .switch::after { transform: translateX(19px); background: #fff; }\n.toggle input:focus-visible + .switch { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* ---------- range slider ---------- *\u002F\ninput[type=\"range\"] {\n  -webkit-appearance: none; appearance: none; width: 100%; height: 6px;\n  background: var(--line); border-radius: 999px; outline: none; cursor: pointer; margin: 8px 0;\n}\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none; appearance: none; width: 19px; height: 19px; border-radius: 50%;\n  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]::-moz-range-thumb {\n  width: 19px; height: 19px; border-radius: 50%; background: var(--accent);\n  border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }\n\n\u002F* ---------- text inputs \u002F selects ---------- *\u002F\ninput[type=\"text\"], input[type=\"number\"], input[type=\"search\"], select, textarea {\n  font: inherit; color: var(--ink); background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 11px; width: 100%;\n}\ntextarea { font-family: var(--mono); font-size: 0.85rem; line-height: 1.6; resize: vertical; }\ninput:focus-visible, select:focus-visible, textarea:focus-visible {\n  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);\n}\n\n\u002F* ---------- status \u002F output \u002F log ---------- *\u002F\n.status {\n  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;\n  font-size: 0.9rem; font-weight: 600;\n  padding: 11px 14px; border-radius: var(--radius-sm);\n  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);\n}\n.status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n.status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n.status.bad  { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }\n.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.good { background: var(--good); } .dot.warn { background: var(--warn); }\n.dot.bad { background: var(--bad); } .dot.info { background: var(--info); }\n\n.output { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; }\n.log { font-family: var(--mono); font-size: 0.8rem; line-height: 1.7; background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 11px 14px;\n  max-height: 220px; overflow-y: auto; white-space: pre-wrap; }\n\n\u002F* ---------- code block + syntax tokens ---------- *\u002F\n.code {\n  font-family: var(--mono); font-size: 0.82rem; line-height: 1.7;\n  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; overflow-x: auto; white-space: pre; tab-size: 2;\n}\n.code .tok-sel  { color: var(--accent); }\n.code .tok-prop { color: #86e29b; }\n.code .tok-kw   { color: #ff9bcb; }\n.code .tok-str  { color: #f0c674; }\n.code .tok-num  { color: #f0883e; }\n.code .tok-cmt  { color: var(--faint); font-style: italic; }\n.code .hl { background: color-mix(in srgb, var(--good) 22%, transparent); border-radius: 4px; padding: 0 3px; }\n\n\u002F* ---------- hint \u002F caption ---------- *\u002F\n.hint { font-size: 0.83rem; color: var(--muted); line-height: 1.55; margin: 10px 0 0; }\n.hint code { font-family: var(--mono); color: var(--ink); background: var(--line-soft);\n  padding: 1px 5px; border-radius: 5px; font-size: 0.86em; }\n.hint strong { color: var(--ink); }\n\n\u002F* ---------- banner (feature-support \u002F notices) ---------- *\u002F\n.banner {\n  display: none; margin-bottom: 14px; padding: 11px 14px; border-radius: 11px;\n  font-size: 0.85rem; line-height: 1.5;\n  color: var(--ink); background: color-mix(in srgb, var(--warn) 12%, var(--panel));\n  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));\n}\n.banner.show { display: block; }\n.banner.info { background: color-mix(in srgb, var(--info) 12%, var(--panel));\n  border-color: color-mix(in srgb, var(--info) 45%, var(--line)); }\n.banner code { font-family: var(--mono); background: var(--line-soft); padding: 1px 5px; border-radius: 5px; }\n\n\u002F* ---------- misc atoms ---------- *\u002F\n.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700;\n  padding: 3px 9px; border-radius: 999px; background: var(--line-soft); color: var(--muted); }\n.pill.good { color: var(--good); background: color-mix(in srgb, var(--good) 16%, transparent); }\n.pill.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 16%, transparent); }\n.mono { font-family: var(--mono); }\n.muted { color: var(--muted); }\n.big-num { font-family: var(--mono); font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; }\n\n\u002F* Optional theme toggle button (top-right). Wire it in JS if you want it. *\u002F\n.theme-btn { position: fixed; top: 12px; right: 12px; z-index: 20; }\n\n\u002F* ---------- responsive ---------- *\u002F\n@media (max-width: 560px) {\n  .panel { padding: 14px; }\n  .wrap-controls { gap: 14px; }\n  .btn { padding: 9px 13px; }\n}\n\n\u002F* ---------- reduced motion ---------- *\u002F\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n\n\n.tabs { display: grid; gap: 10px; margin: 14px 0; }\n  .tab-row {\n    display: grid; grid-template-columns: 90px 1fr 110px; gap: 10px; align-items: center;\n    padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);\n    background: var(--panel-2);\n  }\n  .tab-row .page {\n    font-family: var(--font-mono, monospace); font-size: 0.95rem;\n  }\n  .tab-row .worker-badge {\n    justify-self: end; font-size: 0.78rem; padding: 2px 8px; border-radius: 999px;\n    border: 1px solid var(--line);\n  }\n  .worker-badge.old { color: var(--bad, #e5484d); border-color: var(--bad, #e5484d); }\n  .worker-badge.new { color: var(--good, #30a46c); border-color: var(--good, #30a46c); }\n  .lifecycle { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0; }\n  .lifecycle .step {\n    padding: 4px 10px; border-radius: 999px; border: 1px solid var(--line);\n    font-size: 0.8rem; opacity: 0.4;\n  }\n  .lifecycle .step.active { opacity: 1; border-color: var(--accent); color: var(--accent); }\n  .cache-list { font-family: var(--font-mono, monospace); font-size: 0.85rem; line-height: 1.7; }\n  .cache-list .old-file { color: var(--bad, #e5484d); }\n  .cache-list .new-file { color: var(--good, #30a46c); }","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n  const TAB_COUNT = 3;\n  let strategy = \"cache-first\";\n  let skipWaiting = false;\n  let clientsClaim = false;\n  let versionBump = false;\n  let deployed = false;\n\n  function setStatus(text, kind = \"info\") {\n    $(\"statusText\").textContent = text;\n    $(\"dot\").className = \"dot \" + kind;\n  }\n\n  function setLifecycle(step) {\n    document.querySelectorAll(\"#lifecycle .step\").forEach((el) => {\n      el.classList.toggle(\"active\", el.dataset.step === step);\n    });\n  }\n\n  function renderTabs() {\n    const rows = [];\n    for (let i = 1; i \u003C= TAB_COUNT; i++) {\n      let servesNew;\n      if (!deployed) {\n        servesNew = false;\n      } else if (skipWaiting && clientsClaim) {\n        \u002F\u002F new worker takes over every open tab immediately\n        servesNew = true;\n      } else {\n        \u002F\u002F without both opt-ins, an already-open tab keeps its original controller\n        servesNew = false;\n      }\n\n      let badgeClass = servesNew ? \"new\" : \"old\";\n      let badgeText = servesNew ? \"serving NEW worker\" : \"serving OLD worker\";\n\n      rows.push(`\n        \u003Cdiv class=\"tab-row\">\n          \u003Cspan class=\"pill\">Tab ${i}\u003C\u002Fspan>\n          \u003Cspan class=\"page\">yourapp.com\u002F\u003C\u002Fspan>\n          \u003Cspan class=\"worker-badge ${badgeClass}\">${badgeText}\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n      `);\n    }\n    $(\"tabs\").innerHTML = rows.join(\"\");\n  }\n\n  function renderCache() {\n    if (!deployed) {\n      $(\"cacheList\").innerHTML = `\u003Cspan class=\"old-file\">app-cache: index.html (v1), bundle.js (v1)\u003C\u002Fspan>`;\n      return;\n    }\n\n    const oldCacheGone = versionBump;\n    const htmlIsFresh = strategy !== \"cache-first\"; \u002F\u002F network-first and SWR both eventually surface v2\n\n    let lines = [];\n    if (versionBump) {\n      lines.push(`\u003Cspan class=\"muted\">app-cache-v1: \u003C\u002Fspan>\u003Cspan class=\"old-file\">deleted on activate\u003C\u002Fspan>`);\n      lines.push(`\u003Cspan class=\"new-file\">app-cache-v2: index.html (v2), bundle.js (v2)\u003C\u002Fspan>`);\n    } else {\n      lines.push(`\u003Cspan class=\"old-file\">app-cache: index.html (v1), bundle.js (v1)\u003C\u002Fspan> \u003Cspan class=\"muted\">— never evicted, still matched first\u003C\u002Fspan>`);\n    }\n\n    if (strategy === \"cache-first\" && !oldCacheGone) {\n      lines.push(`\u003Cspan class=\"muted\">Next request for index.html → \u003C\u002Fspan>\u003Cspan class=\"old-file\">served from cache, v1, no network check\u003C\u002Fspan>`);\n    } else if (strategy === \"network-first\") {\n      lines.push(`\u003Cspan class=\"muted\">Next request for index.html → \u003C\u002Fspan>\u003Cspan class=\"new-file\">network tried first, v2 wins\u003C\u002Fspan>`);\n    } else if (strategy === \"swr\") {\n      lines.push(`\u003Cspan class=\"muted\">Next request for index.html → \u003C\u002Fspan>\u003Cspan class=\"${oldCacheGone ? 'new-file' : 'old-file'}\">served from cache instantly (${oldCacheGone ? 'v2' : 'v1'}), network refresh queued for the request after\u003C\u002Fspan>`);\n    } else {\n      lines.push(`\u003Cspan class=\"muted\">Next request for index.html → \u003C\u002Fspan>\u003Cspan class=\"new-file\">served from cache, v2 (old cache was evicted)\u003C\u002Fspan>`);\n    }\n\n    $(\"cacheList\").innerHTML = lines.join(\"\u003Cbr>\");\n  }\n\n  function evaluate() {\n    renderTabs();\n    renderCache();\n\n    if (!deployed) {\n      setLifecycle(\"idle\");\n      setStatus(\"Ready — set your options, then deploy.\", \"info\");\n      $(\"hint\").textContent = \"Caption: this narrates exactly what each tab would receive on its next request.\";\n      return;\n    }\n\n    if (!skipWaiting) {\n      setLifecycle(\"waiting\");\n      setStatus(\"New worker installed, but parked in \\\"waiting\\\" — no open tab will get it until every tab closes.\", \"bad\");\n      $(\"hint\").textContent = \"skipWaiting() is off, so the spec's default applies: an open tab keeps its original controller indefinitely.\";\n    } else if (!clientsClaim) {\n      setLifecycle(\"activate\");\n      setStatus(\"New worker activated, but it only controls tabs opened AFTER activation — these three were already open.\", \"bad\");\n      $(\"hint\").textContent = \"clients.claim() is off, so already-open tabs keep talking to whatever controlled them at load time.\";\n    } else {\n      setLifecycle(\"controlling\");\n      const allFresh = strategy !== \"cache-first\" || versionBump;\n      setStatus(\n        allFresh\n          ? \"All three tabs are now controlled by the new worker, and the cache is serving the new files.\"\n          : \"All three tabs are controlled by the new worker — but it's still cache-first on an un-evicted cache, so it hands back v1 anyway.\",\n        allFresh ? \"good\" : \"warn\"\n      );\n      $(\"hint\").textContent = allFresh\n        ? \"Lifecycle handed over control AND the cache strategy surfaced the new files — the fix is actually live.\"\n        : \"Lifecycle handed over control, but cache-first + no version bump means the new worker still finds v1 sitting in its own cache.\";\n    }\n  }\n\n  $(\"strategySeg\").addEventListener(\"click\", (e) => {\n    const btn = e.target.closest(\"button\");\n    if (!btn) return;\n    document.querySelectorAll(\"#strategySeg button\").forEach((b) => b.setAttribute(\"aria-pressed\", \"false\"));\n    btn.setAttribute(\"aria-pressed\", \"true\");\n    strategy = btn.dataset.val;\n    evaluate();\n  });\n\n  $(\"skipWaiting\").addEventListener(\"change\", (e) => { skipWaiting = e.target.checked; evaluate(); });\n  $(\"clientsClaim\").addEventListener(\"change\", (e) => { clientsClaim = e.target.checked; evaluate(); });\n  $(\"versionBump\").addEventListener(\"change\", (e) => { versionBump = e.target.checked; evaluate(); });\n\n  $(\"deploy\").addEventListener(\"click\", () => {\n    deployed = true;\n    evaluate();\n  });\n\n  $(\"reset\").addEventListener(\"click\", () => {\n    deployed = false;\n    evaluate();\n  });\n\n  function applyPreset(name) {\n    if (name === \"broken\") {\n      strategy = \"cache-first\"; skipWaiting = false; clientsClaim = false; versionBump = false;\n    } else {\n      strategy = \"network-first\"; skipWaiting = true; clientsClaim = true; versionBump = true;\n    }\n    document.querySelectorAll(\"#strategySeg button\").forEach((b) =>\n      b.setAttribute(\"aria-pressed\", String(b.dataset.val === strategy))\n    );\n    $(\"skipWaiting\").checked = skipWaiting;\n    $(\"clientsClaim\").checked = clientsClaim;\n    $(\"versionBump\").checked = versionBump;\n    deployed = true;\n    evaluate();\n  }\n\n  document.querySelectorAll(\"[data-preset]\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => applyPreset(btn.dataset.preset));\n  });\n\n  evaluate();",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"I Shipped a Fix. My Service Worker Kept Serving the Bug.","A critical bug fix went out, and support tickets kept coming in from people who swore they'd refreshed. The code was fine. The service worker's cache-first strategy was doing its job perfectly — which was the whole problem.","\u002Fmedia\u002Fcovers\u002Fservice-worker-stale-cache-deploy.png",{"slug":6},[59,63,67,71,75,79,83,87,91,95,99,103],{"slug":60,"name":61,"articles":62},"webdev","Webdev",135,{"slug":64,"name":65,"articles":66},"javascript","Javascript",111,{"slug":68,"name":69,"articles":70},"frontend","Frontend",82,{"slug":72,"name":73,"articles":74},"tutorial","Tutorial",52,{"slug":76,"name":77,"articles":78},"css","Css",42,{"slug":80,"name":81,"articles":82},"performance","Performance",19,{"slug":84,"name":85,"articles":86},"typescript","Typescript",18,{"slug":88,"name":89,"articles":90},"react","React",16,{"slug":92,"name":93,"articles":94},"browser","Browser",13,{"slug":96,"name":97,"articles":98},"node","Node",12,{"slug":100,"name":101,"articles":102},"accessibility","Accessibility",9,{"slug":104,"name":105,"articles":102},"programming","Programming"]