[{"data":1,"prerenderedAt":8661},["ShallowReactive",2],{"post-progressive-web-apps-deep-dive":3},{"id":4,"title":5,"author":6,"body":7,"category":8641,"cover":8642,"date":8643,"description":8644,"extension":8645,"featured":990,"meta":8646,"navigation":990,"path":8647,"published":990,"readingTime":8648,"seo":8649,"sitemap":8650,"stem":8651,"tags":8652,"updated":8643,"__hash__":8660},"posts/posts/progressive-web-apps-deep-dive.md","Progressive Web Apps: A Deep Dive","Kashyap Kumar",{"type":8,"value":9,"toc":8581},"minimark",[10,15,19,27,30,37,40,43,47,50,56,62,65,68,81,84,95,97,101,104,107,110,121,123,127,134,139,756,763,800,804,812,839,845,857,865,873,885,889,892,898,901,903,907,914,918,921,927,930,947,951,954,960,963,966,970,973,1245,1265,1269,1276,1425,1428,1430,1434,1437,1441,1474,1478,1669,1683,1687,1939,1943,1946,2104,2108,2114,2325,2335,2339,2350,2573,2579,2583,2586,3692,3694,3698,3705,4076,4078,4082,4085,4092,4098,4439,4442,4446,4449,4522,4526,4532,4623,4625,4629,4632,4636,4639,4865,4869,4872,5235,5239,5246,5692,5696,5703,5929,5932,5938,5941,5943,5947,5950,5953,5957,6064,6068,6322,6329,6331,6335,6341,6487,6686,6693,6695,6699,6706,6710,7219,7234,7236,7240,7247,7250,7253,7337,7340,7408,7410,7414,7417,7487,7500,7504,7507,7679,7682,7750,7752,7756,7760,7767,7855,7859,7862,7908,7912,7918,7975,7979,7994,7998,8001,8161,8222,8224,8228,8231,8310,8313,8316,8318,8322,8325,8525,8527,8531,8534,8572,8575,8578],[11,12,14],"h2",{"id":13},"what-is-a-progressive-web-app","What is a Progressive Web App?",[16,17,18],"p",{},"You have almost certainly used one without knowing it. Twitter, Spotify, Starbucks, Pinterest, Uber, and Google Maps all ship Progressive Web Apps. On a fast connection they feel like any other website. On a slow connection, or with no internet at all, they still work. On a phone, you can install them to your home screen and they open like a native app, full-screen, with no browser chrome.",[16,20,21,22,26],{},"A ",[23,24,25],"strong",{},"Progressive Web App (PWA)"," is a web application that uses a specific set of browser APIs to deliver an experience that feels native: installable, offline-capable, fast, and able to receive push notifications. The key word is \"progressive\": these features enhance the experience for users whose browsers and devices support them, without breaking the experience for anyone else.",[16,28,29],{},"PWAs are not a single technology. They are a combination of three core ingredients working together:",[16,31,32],{},[33,34],"img",{"alt":35,"src":36},"The three pillars of a Progressive Web App: Service Worker, Web App Manifest, and HTTPS","/blog-post-images/progressive-web-apps-deep-dive/pwa-three-pillars.png",[16,38,39],{},"This post is a complete technical walkthrough of all three, plus the supporting APIs that make PWAs powerful. We will go from the theory to working code.",[41,42],"hr",{},[11,44,46],{"id":45},"why-pwas-exist-the-problem-they-solve","Why PWAs Exist: The Problem They Solve",[16,48,49],{},"Before diving in, it helps to understand what gap PWAs fill.",[16,51,52,55],{},[23,53,54],{},"Native apps"," (iOS and Android) have genuine advantages: they work offline, they can send push notifications, they load from the home screen instantly, and they can access device hardware. But building native requires separate codebases (Swift/Kotlin), separate distribution (App Store/Play Store), separate approval processes, and significant expertise.",[16,57,58,61],{},[23,59,60],{},"Web apps"," have their own advantages: one codebase, instant updates (no app store review), discoverable via search, linkable with a URL, and zero installation friction. But traditionally they required a network connection and could not be installed or send push notifications.",[16,63,64],{},"PWAs close that gap. They let you write once (HTML, CSS, JavaScript) and get most of the benefits of both worlds.",[16,66,67],{},"The business case is clear. When Pinterest built their PWA:",[69,70,71,75,78],"ul",{},[72,73,74],"li",{},"Time spent on site increased 40%",[72,76,77],{},"User-generated ad revenue increased 44%",[72,79,80],{},"Core engagements increased 60%",[16,82,83],{},"When Twitter built Twitter Lite (their PWA):",[69,85,86,89,92],{},[72,87,88],{},"65% increase in pages per session",[72,90,91],{},"75% increase in tweets sent",[72,93,94],{},"20% decrease in bounce rate",[41,96],{},[11,98,100],{"id":99},"pillar-1-https-the-non-negotiable-foundation","Pillar 1: HTTPS (The Non-Negotiable Foundation)",[16,102,103],{},"Before anything else: PWAs require HTTPS. Not optional.",[16,105,106],{},"Service workers (which we will get to) can intercept all network requests your page makes. If a malicious actor could inject a service worker over an unencrypted connection, they could hijack every request your users make. HTTPS prevents this.",[16,108,109],{},"In practice this is rarely a blocker. Let's Encrypt provides free TLS certificates, and most hosting platforms (Vercel, Netlify, Cloudflare Pages) provide HTTPS automatically.",[111,112,113],"blockquote",{},[16,114,115,116,120],{},"During development, ",[117,118,119],"code",{},"localhost"," is treated as a secure origin. You can test all PWA features locally without a certificate.",[41,122],{},[11,124,126],{"id":125},"pillar-2-the-web-app-manifest","Pillar 2: The Web App Manifest",[16,128,129,130,133],{},"The ",[23,131,132],{},"Web App Manifest"," is a JSON file that tells the browser everything it needs to know to install your app: its name, icons, colors, and how it should open.",[135,136,138],"h3",{"id":137},"a-complete-manifest-file","A Complete Manifest File",[140,141,146],"pre",{"className":142,"code":143,"language":144,"meta":145,"style":145},"language-json shiki shiki-themes github-light","{\n  \"name\": \"Clarity App\",\n  \"short_name\": \"Clarity\",\n  \"description\": \"Design, development, and marketing that converts.\",\n  \"start_url\": \"/\",\n  \"scope\": \"/\",\n  \"display\": \"standalone\",\n  \"orientation\": \"portrait-primary\",\n  \"background_color\": \"#ffffff\",\n  \"theme_color\": \"#1e3a5f\",\n  \"lang\": \"en-US\",\n  \"dir\": \"ltr\",\n  \"icons\": [\n    {\n      \"src\": \"/icons/icon-72x72.png\",\n      \"sizes\": \"72x72\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable any\"\n    },\n    {\n      \"src\": \"/icons/icon-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable any\"\n    },\n    {\n      \"src\": \"/icons/icon-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable any\"\n    }\n  ],\n  \"screenshots\": [\n    {\n      \"src\": \"/screenshots/desktop.png\",\n      \"sizes\": \"1280x720\",\n      \"type\": \"image/png\",\n      \"form_factor\": \"wide\"\n    },\n    {\n      \"src\": \"/screenshots/mobile.png\",\n      \"sizes\": \"390x844\",\n      \"type\": \"image/png\",\n      \"form_factor\": \"narrow\"\n    }\n  ],\n  \"shortcuts\": [\n    {\n      \"name\": \"New Project\",\n      \"short_name\": \"New\",\n      \"description\": \"Start a new project\",\n      \"url\": \"/projects/new\",\n      \"icons\": [{ \"src\": \"/icons/new-project.png\", \"sizes\": \"96x96\" }]\n    }\n  ],\n  \"categories\": [\"business\", \"productivity\"],\n  \"prefer_related_applications\": false\n}\n","json","",[117,147,148,157,174,187,200,213,225,238,251,264,277,290,303,312,318,331,344,357,368,374,379,391,403,414,423,428,433,445,457,468,477,483,489,497,502,514,526,537,548,553,558,570,582,593,603,608,613,621,626,639,652,665,678,709,714,719,739,750],{"__ignoreMap":145},[149,150,153],"span",{"class":151,"line":152},"line",1,[149,154,156],{"class":155},"sgsFI","{\n",[149,158,160,164,167,171],{"class":151,"line":159},2,[149,161,163],{"class":162},"sYu0t","  \"name\"",[149,165,166],{"class":155},": ",[149,168,170],{"class":169},"sYBdl","\"Clarity App\"",[149,172,173],{"class":155},",\n",[149,175,177,180,182,185],{"class":151,"line":176},3,[149,178,179],{"class":162},"  \"short_name\"",[149,181,166],{"class":155},[149,183,184],{"class":169},"\"Clarity\"",[149,186,173],{"class":155},[149,188,190,193,195,198],{"class":151,"line":189},4,[149,191,192],{"class":162},"  \"description\"",[149,194,166],{"class":155},[149,196,197],{"class":169},"\"Design, development, and marketing that converts.\"",[149,199,173],{"class":155},[149,201,203,206,208,211],{"class":151,"line":202},5,[149,204,205],{"class":162},"  \"start_url\"",[149,207,166],{"class":155},[149,209,210],{"class":169},"\"/\"",[149,212,173],{"class":155},[149,214,216,219,221,223],{"class":151,"line":215},6,[149,217,218],{"class":162},"  \"scope\"",[149,220,166],{"class":155},[149,222,210],{"class":169},[149,224,173],{"class":155},[149,226,228,231,233,236],{"class":151,"line":227},7,[149,229,230],{"class":162},"  \"display\"",[149,232,166],{"class":155},[149,234,235],{"class":169},"\"standalone\"",[149,237,173],{"class":155},[149,239,241,244,246,249],{"class":151,"line":240},8,[149,242,243],{"class":162},"  \"orientation\"",[149,245,166],{"class":155},[149,247,248],{"class":169},"\"portrait-primary\"",[149,250,173],{"class":155},[149,252,254,257,259,262],{"class":151,"line":253},9,[149,255,256],{"class":162},"  \"background_color\"",[149,258,166],{"class":155},[149,260,261],{"class":169},"\"#ffffff\"",[149,263,173],{"class":155},[149,265,267,270,272,275],{"class":151,"line":266},10,[149,268,269],{"class":162},"  \"theme_color\"",[149,271,166],{"class":155},[149,273,274],{"class":169},"\"#1e3a5f\"",[149,276,173],{"class":155},[149,278,280,283,285,288],{"class":151,"line":279},11,[149,281,282],{"class":162},"  \"lang\"",[149,284,166],{"class":155},[149,286,287],{"class":169},"\"en-US\"",[149,289,173],{"class":155},[149,291,293,296,298,301],{"class":151,"line":292},12,[149,294,295],{"class":162},"  \"dir\"",[149,297,166],{"class":155},[149,299,300],{"class":169},"\"ltr\"",[149,302,173],{"class":155},[149,304,306,309],{"class":151,"line":305},13,[149,307,308],{"class":162},"  \"icons\"",[149,310,311],{"class":155},": [\n",[149,313,315],{"class":151,"line":314},14,[149,316,317],{"class":155},"    {\n",[149,319,321,324,326,329],{"class":151,"line":320},15,[149,322,323],{"class":162},"      \"src\"",[149,325,166],{"class":155},[149,327,328],{"class":169},"\"/icons/icon-72x72.png\"",[149,330,173],{"class":155},[149,332,334,337,339,342],{"class":151,"line":333},16,[149,335,336],{"class":162},"      \"sizes\"",[149,338,166],{"class":155},[149,340,341],{"class":169},"\"72x72\"",[149,343,173],{"class":155},[149,345,347,350,352,355],{"class":151,"line":346},17,[149,348,349],{"class":162},"      \"type\"",[149,351,166],{"class":155},[149,353,354],{"class":169},"\"image/png\"",[149,356,173],{"class":155},[149,358,360,363,365],{"class":151,"line":359},18,[149,361,362],{"class":162},"      \"purpose\"",[149,364,166],{"class":155},[149,366,367],{"class":169},"\"maskable any\"\n",[149,369,371],{"class":151,"line":370},19,[149,372,373],{"class":155},"    },\n",[149,375,377],{"class":151,"line":376},20,[149,378,317],{"class":155},[149,380,382,384,386,389],{"class":151,"line":381},21,[149,383,323],{"class":162},[149,385,166],{"class":155},[149,387,388],{"class":169},"\"/icons/icon-192x192.png\"",[149,390,173],{"class":155},[149,392,394,396,398,401],{"class":151,"line":393},22,[149,395,336],{"class":162},[149,397,166],{"class":155},[149,399,400],{"class":169},"\"192x192\"",[149,402,173],{"class":155},[149,404,406,408,410,412],{"class":151,"line":405},23,[149,407,349],{"class":162},[149,409,166],{"class":155},[149,411,354],{"class":169},[149,413,173],{"class":155},[149,415,417,419,421],{"class":151,"line":416},24,[149,418,362],{"class":162},[149,420,166],{"class":155},[149,422,367],{"class":169},[149,424,426],{"class":151,"line":425},25,[149,427,373],{"class":155},[149,429,431],{"class":151,"line":430},26,[149,432,317],{"class":155},[149,434,436,438,440,443],{"class":151,"line":435},27,[149,437,323],{"class":162},[149,439,166],{"class":155},[149,441,442],{"class":169},"\"/icons/icon-512x512.png\"",[149,444,173],{"class":155},[149,446,448,450,452,455],{"class":151,"line":447},28,[149,449,336],{"class":162},[149,451,166],{"class":155},[149,453,454],{"class":169},"\"512x512\"",[149,456,173],{"class":155},[149,458,460,462,464,466],{"class":151,"line":459},29,[149,461,349],{"class":162},[149,463,166],{"class":155},[149,465,354],{"class":169},[149,467,173],{"class":155},[149,469,471,473,475],{"class":151,"line":470},30,[149,472,362],{"class":162},[149,474,166],{"class":155},[149,476,367],{"class":169},[149,478,480],{"class":151,"line":479},31,[149,481,482],{"class":155},"    }\n",[149,484,486],{"class":151,"line":485},32,[149,487,488],{"class":155},"  ],\n",[149,490,492,495],{"class":151,"line":491},33,[149,493,494],{"class":162},"  \"screenshots\"",[149,496,311],{"class":155},[149,498,500],{"class":151,"line":499},34,[149,501,317],{"class":155},[149,503,505,507,509,512],{"class":151,"line":504},35,[149,506,323],{"class":162},[149,508,166],{"class":155},[149,510,511],{"class":169},"\"/screenshots/desktop.png\"",[149,513,173],{"class":155},[149,515,517,519,521,524],{"class":151,"line":516},36,[149,518,336],{"class":162},[149,520,166],{"class":155},[149,522,523],{"class":169},"\"1280x720\"",[149,525,173],{"class":155},[149,527,529,531,533,535],{"class":151,"line":528},37,[149,530,349],{"class":162},[149,532,166],{"class":155},[149,534,354],{"class":169},[149,536,173],{"class":155},[149,538,540,543,545],{"class":151,"line":539},38,[149,541,542],{"class":162},"      \"form_factor\"",[149,544,166],{"class":155},[149,546,547],{"class":169},"\"wide\"\n",[149,549,551],{"class":151,"line":550},39,[149,552,373],{"class":155},[149,554,556],{"class":151,"line":555},40,[149,557,317],{"class":155},[149,559,561,563,565,568],{"class":151,"line":560},41,[149,562,323],{"class":162},[149,564,166],{"class":155},[149,566,567],{"class":169},"\"/screenshots/mobile.png\"",[149,569,173],{"class":155},[149,571,573,575,577,580],{"class":151,"line":572},42,[149,574,336],{"class":162},[149,576,166],{"class":155},[149,578,579],{"class":169},"\"390x844\"",[149,581,173],{"class":155},[149,583,585,587,589,591],{"class":151,"line":584},43,[149,586,349],{"class":162},[149,588,166],{"class":155},[149,590,354],{"class":169},[149,592,173],{"class":155},[149,594,596,598,600],{"class":151,"line":595},44,[149,597,542],{"class":162},[149,599,166],{"class":155},[149,601,602],{"class":169},"\"narrow\"\n",[149,604,606],{"class":151,"line":605},45,[149,607,482],{"class":155},[149,609,611],{"class":151,"line":610},46,[149,612,488],{"class":155},[149,614,616,619],{"class":151,"line":615},47,[149,617,618],{"class":162},"  \"shortcuts\"",[149,620,311],{"class":155},[149,622,624],{"class":151,"line":623},48,[149,625,317],{"class":155},[149,627,629,632,634,637],{"class":151,"line":628},49,[149,630,631],{"class":162},"      \"name\"",[149,633,166],{"class":155},[149,635,636],{"class":169},"\"New Project\"",[149,638,173],{"class":155},[149,640,642,645,647,650],{"class":151,"line":641},50,[149,643,644],{"class":162},"      \"short_name\"",[149,646,166],{"class":155},[149,648,649],{"class":169},"\"New\"",[149,651,173],{"class":155},[149,653,655,658,660,663],{"class":151,"line":654},51,[149,656,657],{"class":162},"      \"description\"",[149,659,166],{"class":155},[149,661,662],{"class":169},"\"Start a new project\"",[149,664,173],{"class":155},[149,666,668,671,673,676],{"class":151,"line":667},52,[149,669,670],{"class":162},"      \"url\"",[149,672,166],{"class":155},[149,674,675],{"class":169},"\"/projects/new\"",[149,677,173],{"class":155},[149,679,681,684,687,690,692,695,698,701,703,706],{"class":151,"line":680},53,[149,682,683],{"class":162},"      \"icons\"",[149,685,686],{"class":155},": [{ ",[149,688,689],{"class":162},"\"src\"",[149,691,166],{"class":155},[149,693,694],{"class":169},"\"/icons/new-project.png\"",[149,696,697],{"class":155},", ",[149,699,700],{"class":162},"\"sizes\"",[149,702,166],{"class":155},[149,704,705],{"class":169},"\"96x96\"",[149,707,708],{"class":155}," }]\n",[149,710,712],{"class":151,"line":711},54,[149,713,482],{"class":155},[149,715,717],{"class":151,"line":716},55,[149,718,488],{"class":155},[149,720,722,725,728,731,733,736],{"class":151,"line":721},56,[149,723,724],{"class":162},"  \"categories\"",[149,726,727],{"class":155},": [",[149,729,730],{"class":169},"\"business\"",[149,732,697],{"class":155},[149,734,735],{"class":169},"\"productivity\"",[149,737,738],{"class":155},"],\n",[149,740,742,745,747],{"class":151,"line":741},57,[149,743,744],{"class":162},"  \"prefer_related_applications\"",[149,746,166],{"class":155},[149,748,749],{"class":162},"false\n",[149,751,753],{"class":151,"line":752},58,[149,754,755],{"class":155},"}\n",[16,757,758,759,762],{},"Link it from every HTML page in the ",[117,760,761],{},"\u003Chead>",":",[140,764,768],{"className":765,"code":766,"language":767,"meta":145,"style":145},"language-html shiki shiki-themes github-light","\u003Clink rel=\"manifest\" href=\"/manifest.json\" />\n","html",[117,769,770],{"__ignoreMap":145},[149,771,772,775,779,783,786,789,792,794,797],{"class":151,"line":152},[149,773,774],{"class":155},"\u003C",[149,776,778],{"class":777},"shJU0","link",[149,780,782],{"class":781},"s7eDp"," rel",[149,784,785],{"class":155},"=",[149,787,788],{"class":169},"\"manifest\"",[149,790,791],{"class":781}," href",[149,793,785],{"class":155},[149,795,796],{"class":169},"\"/manifest.json\"",[149,798,799],{"class":155}," />\n",[135,801,803],{"id":802},"key-manifest-fields-explained","Key Manifest Fields Explained",[16,805,806,811],{},[23,807,808],{},[117,809,810],{},"display"," controls how the app opens when launched from the home screen:",[140,813,817],{"className":814,"code":815,"language":816,"meta":145,"style":145},"language-txt shiki shiki-themes github-light","display: \"browser\"     - opens in a regular browser tab (no PWA feel)\ndisplay: \"minimal-ui\"  - browser chrome, but minimal (back button, URL)\ndisplay: \"standalone\"  - looks like a native app (no URL bar)\ndisplay: \"fullscreen\"  - covers entire screen (games, media players)\n","txt",[117,818,819,824,829,834],{"__ignoreMap":145},[149,820,821],{"class":151,"line":152},[149,822,823],{},"display: \"browser\"     - opens in a regular browser tab (no PWA feel)\n",[149,825,826],{"class":151,"line":159},[149,827,828],{},"display: \"minimal-ui\"  - browser chrome, but minimal (back button, URL)\n",[149,830,831],{"class":151,"line":176},[149,832,833],{},"display: \"standalone\"  - looks like a native app (no URL bar)\n",[149,835,836],{"class":151,"line":189},[149,837,838],{},"display: \"fullscreen\"  - covers entire screen (games, media players)\n",[16,840,841,842,844],{},"For most apps, ",[117,843,235],{}," is the right choice. It removes the browser chrome so the experience is indistinguishable from a native app.",[16,846,847,852,853,856],{},[23,848,849],{},[117,850,851],{},"start_url"," is the page that opens when the user launches the installed app. Always include a query parameter for analytics so you can measure installs: ",[117,854,855],{},"\"start_url\": \"/?source=pwa\"",".",[16,858,859,864],{},[23,860,861],{},[117,862,863],{},"scope"," defines which URLs are considered \"inside\" the app. If a user navigates outside the scope (e.g., clicks an external link), it opens in a browser tab instead.",[16,866,867,872],{},[23,868,869],{},[117,870,871],{},"theme_color"," sets the color of the browser address bar and the system status bar on Android. It should match your primary brand color.",[16,874,875,880,881,884],{},[23,876,877],{},[117,878,879],{},"icons"," need at minimum 192x192 and 512x512 PNG icons. The ",[117,882,883],{},"purpose: \"maskable\""," variant tells the OS it can crop the icon into any shape (circle, rounded square, etc.) safely, because your content is inside a \"safe zone\" in the center.",[135,886,888],{"id":887},"the-maskable-icon-safe-zone","The Maskable Icon Safe Zone",[16,890,891],{},"Android adaptive icons apply a mask to app icons. If your icon has important content at the edges, it will be clipped. The safe zone is the inner 80% of the icon:",[16,893,894],{},[33,895],{"alt":896,"src":897},"Maskable icon safe zone diagram showing the inner 80% safe area and clipped edges","/blog-post-images/progressive-web-apps-deep-dive/maskable-icon-safe-zone.png",[16,899,900],{},"Keep your logo and important content within the safe zone. Use maskable.app to preview your icon against all Android mask shapes before shipping.",[41,902],{},[11,904,906],{"id":905},"pillar-3-the-service-worker","Pillar 3: The Service Worker",[16,908,909,910,913],{},"This is the heart of everything. A ",[23,911,912],{},"Service Worker"," is a JavaScript file that the browser runs in a separate background thread, completely separate from your web page. It has no access to the DOM. What it does have is the ability to intercept every network request your page makes and decide what to do with it.",[135,915,917],{"id":916},"the-service-worker-mental-model","The Service Worker Mental Model",[16,919,920],{},"Think of a service worker as a programmable network proxy sitting between your web page and the internet:",[16,922,923],{},[33,924],{"alt":925,"src":926},"Diagram comparing request flow without and with a service worker acting as a network proxy","/blog-post-images/progressive-web-apps-deep-dive/service-worker-proxy.png",[16,928,929],{},"The service worker can:",[69,931,932,935,938,941,944],{},[72,933,934],{},"Serve responses from a local cache (offline support)",[72,936,937],{},"Fetch from the network and update the cache in the background",[72,939,940],{},"Intercept and modify requests before they hit the network",[72,942,943],{},"Receive push notifications from a server (even when the page is closed)",[72,945,946],{},"Run background sync tasks when connectivity is restored",[135,948,950],{"id":949},"service-worker-lifecycle","Service Worker Lifecycle",[16,952,953],{},"Understanding the lifecycle is critical. Many PWA bugs come from misunderstanding when a service worker takes effect.",[16,955,956],{},[33,957],{"alt":958,"src":959},"Service worker lifecycle diagram showing the six stages: Registration, Installation, Waiting, Activation, Idle/Fetch, and Terminated","/blog-post-images/progressive-web-apps-deep-dive/service-worker-lifecyle.png",[16,961,962],{},"The \"waiting\" phase trips up many developers. You install a new service worker, refresh the page, and the new version seems to do nothing. This is by design. The old service worker is still controlling the page. Only when you close all tabs does the new one activate.",[16,964,965],{},"During development, use Chrome DevTools > Application > Service Workers > \"Update on reload\" to bypass this.",[135,967,969],{"id":968},"registering-a-service-worker","Registering a Service Worker",[16,971,972],{},"Registration happens from your main JavaScript file, not from the service worker itself:",[140,974,978],{"className":975,"code":976,"language":977,"meta":145,"style":145},"language-javascript shiki shiki-themes github-light","// main.js (runs in the page, not in the service worker)\n\nasync function registerServiceWorker() {\n  if (!('serviceWorker' in navigator)) {\n    console.log('Service workers not supported');\n    return;\n  }\n\n  try {\n    const registration = await navigator.serviceWorker.register('/sw.js', {\n      scope: '/'\n    });\n\n    registration.addEventListener('updatefound', () => {\n      const newWorker = registration.installing;\n      console.log('New service worker installing:', newWorker);\n    });\n\n    console.log('Service worker registered, scope:', registration.scope);\n  } catch (error) {\n    console.error('Service worker registration failed:', error);\n  }\n}\n\n// Register after the page loads\nwindow.addEventListener('load', registerServiceWorker);\n","javascript",[117,979,980,986,992,1007,1030,1046,1054,1059,1063,1071,1099,1107,1112,1116,1137,1150,1165,1169,1173,1187,1198,1213,1217,1221,1225,1230],{"__ignoreMap":145},[149,981,982],{"class":151,"line":152},[149,983,985],{"class":984},"sAwPA","// main.js (runs in the page, not in the service worker)\n",[149,987,988],{"class":151,"line":159},[149,989,991],{"emptyLinePlaceholder":990},true,"\n",[149,993,994,998,1001,1004],{"class":151,"line":176},[149,995,997],{"class":996},"sD7c4","async",[149,999,1000],{"class":996}," function",[149,1002,1003],{"class":781}," registerServiceWorker",[149,1005,1006],{"class":155},"() {\n",[149,1008,1009,1012,1015,1018,1021,1024,1027],{"class":151,"line":189},[149,1010,1011],{"class":996},"  if",[149,1013,1014],{"class":155}," (",[149,1016,1017],{"class":996},"!",[149,1019,1020],{"class":155},"(",[149,1022,1023],{"class":169},"'serviceWorker'",[149,1025,1026],{"class":996}," in",[149,1028,1029],{"class":155}," navigator)) {\n",[149,1031,1032,1035,1038,1040,1043],{"class":151,"line":202},[149,1033,1034],{"class":155},"    console.",[149,1036,1037],{"class":781},"log",[149,1039,1020],{"class":155},[149,1041,1042],{"class":169},"'Service workers not supported'",[149,1044,1045],{"class":155},");\n",[149,1047,1048,1051],{"class":151,"line":215},[149,1049,1050],{"class":996},"    return",[149,1052,1053],{"class":155},";\n",[149,1055,1056],{"class":151,"line":227},[149,1057,1058],{"class":155},"  }\n",[149,1060,1061],{"class":151,"line":240},[149,1062,991],{"emptyLinePlaceholder":990},[149,1064,1065,1068],{"class":151,"line":253},[149,1066,1067],{"class":996},"  try",[149,1069,1070],{"class":155}," {\n",[149,1072,1073,1076,1079,1082,1085,1088,1091,1093,1096],{"class":151,"line":266},[149,1074,1075],{"class":996},"    const",[149,1077,1078],{"class":162}," registration",[149,1080,1081],{"class":996}," =",[149,1083,1084],{"class":996}," await",[149,1086,1087],{"class":155}," navigator.serviceWorker.",[149,1089,1090],{"class":781},"register",[149,1092,1020],{"class":155},[149,1094,1095],{"class":169},"'/sw.js'",[149,1097,1098],{"class":155},", {\n",[149,1100,1101,1104],{"class":151,"line":279},[149,1102,1103],{"class":155},"      scope: ",[149,1105,1106],{"class":169},"'/'\n",[149,1108,1109],{"class":151,"line":292},[149,1110,1111],{"class":155},"    });\n",[149,1113,1114],{"class":151,"line":305},[149,1115,991],{"emptyLinePlaceholder":990},[149,1117,1118,1121,1124,1126,1129,1132,1135],{"class":151,"line":314},[149,1119,1120],{"class":155},"    registration.",[149,1122,1123],{"class":781},"addEventListener",[149,1125,1020],{"class":155},[149,1127,1128],{"class":169},"'updatefound'",[149,1130,1131],{"class":155},", () ",[149,1133,1134],{"class":996},"=>",[149,1136,1070],{"class":155},[149,1138,1139,1142,1145,1147],{"class":151,"line":320},[149,1140,1141],{"class":996},"      const",[149,1143,1144],{"class":162}," newWorker",[149,1146,1081],{"class":996},[149,1148,1149],{"class":155}," registration.installing;\n",[149,1151,1152,1155,1157,1159,1162],{"class":151,"line":333},[149,1153,1154],{"class":155},"      console.",[149,1156,1037],{"class":781},[149,1158,1020],{"class":155},[149,1160,1161],{"class":169},"'New service worker installing:'",[149,1163,1164],{"class":155},", newWorker);\n",[149,1166,1167],{"class":151,"line":346},[149,1168,1111],{"class":155},[149,1170,1171],{"class":151,"line":359},[149,1172,991],{"emptyLinePlaceholder":990},[149,1174,1175,1177,1179,1181,1184],{"class":151,"line":370},[149,1176,1034],{"class":155},[149,1178,1037],{"class":781},[149,1180,1020],{"class":155},[149,1182,1183],{"class":169},"'Service worker registered, scope:'",[149,1185,1186],{"class":155},", registration.scope);\n",[149,1188,1189,1192,1195],{"class":151,"line":376},[149,1190,1191],{"class":155},"  } ",[149,1193,1194],{"class":996},"catch",[149,1196,1197],{"class":155}," (error) {\n",[149,1199,1200,1202,1205,1207,1210],{"class":151,"line":381},[149,1201,1034],{"class":155},[149,1203,1204],{"class":781},"error",[149,1206,1020],{"class":155},[149,1208,1209],{"class":169},"'Service worker registration failed:'",[149,1211,1212],{"class":155},", error);\n",[149,1214,1215],{"class":151,"line":393},[149,1216,1058],{"class":155},[149,1218,1219],{"class":151,"line":405},[149,1220,755],{"class":155},[149,1222,1223],{"class":151,"line":416},[149,1224,991],{"emptyLinePlaceholder":990},[149,1226,1227],{"class":151,"line":425},[149,1228,1229],{"class":984},"// Register after the page loads\n",[149,1231,1232,1235,1237,1239,1242],{"class":151,"line":430},[149,1233,1234],{"class":155},"window.",[149,1236,1123],{"class":781},[149,1238,1020],{"class":155},[149,1240,1241],{"class":169},"'load'",[149,1243,1244],{"class":155},", registerServiceWorker);\n",[16,1246,129,1247,1249,1250,1253,1254,1257,1258,1261,1262,856],{},[117,1248,863],{}," defaults to the directory containing the service worker file. A service worker at ",[117,1251,1252],{},"/sw.js"," controls all pages at ",[117,1255,1256],{},"/",". A service worker at ",[117,1259,1260],{},"/blog/sw.js"," only controls pages under ",[117,1263,1264],{},"/blog/",[135,1266,1268],{"id":1267},"the-fetch-event-intercepting-requests","The Fetch Event: Intercepting Requests",[16,1270,1271,1272,1275],{},"The most important event in a service worker is ",[117,1273,1274],{},"fetch",". Every network request from your page flows through it:",[140,1277,1279],{"className":975,"code":1278,"language":977,"meta":145,"style":145},"// sw.js\n\nself.addEventListener('fetch', (event) => {\n  // event.request is the original Request object\n  // event.respondWith() lets you override the response\n\n  event.respondWith(\n    // Try the cache first, fall back to network\n    caches.match(event.request).then((cachedResponse) => {\n      if (cachedResponse) {\n        return cachedResponse; // serve from cache\n      }\n      return fetch(event.request); // fall back to network\n    })\n  );\n});\n",[117,1280,1281,1286,1290,1316,1321,1326,1330,1341,1346,1372,1380,1391,1396,1410,1415,1420],{"__ignoreMap":145},[149,1282,1283],{"class":151,"line":152},[149,1284,1285],{"class":984},"// sw.js\n",[149,1287,1288],{"class":151,"line":159},[149,1289,991],{"emptyLinePlaceholder":990},[149,1291,1292,1295,1297,1299,1302,1305,1309,1312,1314],{"class":151,"line":176},[149,1293,1294],{"class":155},"self.",[149,1296,1123],{"class":781},[149,1298,1020],{"class":155},[149,1300,1301],{"class":169},"'fetch'",[149,1303,1304],{"class":155},", (",[149,1306,1308],{"class":1307},"sqxcx","event",[149,1310,1311],{"class":155},") ",[149,1313,1134],{"class":996},[149,1315,1070],{"class":155},[149,1317,1318],{"class":151,"line":189},[149,1319,1320],{"class":984},"  // event.request is the original Request object\n",[149,1322,1323],{"class":151,"line":202},[149,1324,1325],{"class":984},"  // event.respondWith() lets you override the response\n",[149,1327,1328],{"class":151,"line":215},[149,1329,991],{"emptyLinePlaceholder":990},[149,1331,1332,1335,1338],{"class":151,"line":227},[149,1333,1334],{"class":155},"  event.",[149,1336,1337],{"class":781},"respondWith",[149,1339,1340],{"class":155},"(\n",[149,1342,1343],{"class":151,"line":240},[149,1344,1345],{"class":984},"    // Try the cache first, fall back to network\n",[149,1347,1348,1351,1354,1357,1360,1363,1366,1368,1370],{"class":151,"line":253},[149,1349,1350],{"class":155},"    caches.",[149,1352,1353],{"class":781},"match",[149,1355,1356],{"class":155},"(event.request).",[149,1358,1359],{"class":781},"then",[149,1361,1362],{"class":155},"((",[149,1364,1365],{"class":1307},"cachedResponse",[149,1367,1311],{"class":155},[149,1369,1134],{"class":996},[149,1371,1070],{"class":155},[149,1373,1374,1377],{"class":151,"line":266},[149,1375,1376],{"class":996},"      if",[149,1378,1379],{"class":155}," (cachedResponse) {\n",[149,1381,1382,1385,1388],{"class":151,"line":279},[149,1383,1384],{"class":996},"        return",[149,1386,1387],{"class":155}," cachedResponse; ",[149,1389,1390],{"class":984},"// serve from cache\n",[149,1392,1393],{"class":151,"line":292},[149,1394,1395],{"class":155},"      }\n",[149,1397,1398,1401,1404,1407],{"class":151,"line":305},[149,1399,1400],{"class":996},"      return",[149,1402,1403],{"class":781}," fetch",[149,1405,1406],{"class":155},"(event.request); ",[149,1408,1409],{"class":984},"// fall back to network\n",[149,1411,1412],{"class":151,"line":314},[149,1413,1414],{"class":155},"    })\n",[149,1416,1417],{"class":151,"line":320},[149,1418,1419],{"class":155},"  );\n",[149,1421,1422],{"class":151,"line":333},[149,1423,1424],{"class":155},"});\n",[16,1426,1427],{},"This is the simplest possible strategy: cache first, network fallback. But different resources need different strategies.",[41,1429],{},[11,1431,1433],{"id":1432},"caching-strategies","Caching Strategies",[16,1435,1436],{},"Choosing the right caching strategy for each type of resource is the most important design decision in a PWA. There is no single right answer. You mix and match strategies based on how often the content changes and how critical freshness is.",[135,1438,1440],{"id":1439},"the-five-core-strategies","The Five Core Strategies",[1442,1443,1444,1450,1456,1462,1468],"ol",{},[72,1445,1446,1449],{},[23,1447,1448],{},"Cache First (Cache Falling Back to Network):"," Check cache first. If found, serve it. If not, fetch from network, cache the response, then serve it. Best for static assets (JS, CSS, fonts, images with a content hash in the filename).",[72,1451,1452,1455],{},[23,1453,1454],{},"Network First (Network Falling Back to Cache):"," Try the network first. If successful, cache the response and serve it. If the network fails, serve the stale cached version. Best for API responses and HTML pages that must be fresh.",[72,1457,1458,1461],{},[23,1459,1460],{},"Stale While Revalidate:"," Serve from cache immediately (even if stale), and simultaneously fetch an updated version from the network in the background to refresh the cache for next time. Best for non-critical content where speed matters more than freshness (avatars, blog posts).",[72,1463,1464,1467],{},[23,1465,1466],{},"Network Only:"," Always fetch from the network. Never cache. Best for analytics pings, POST requests, and payments.",[72,1469,1470,1473],{},[23,1471,1472],{},"Cache Only:"," Serve exclusively from cache. Never touch the network. Best for assets pre-cached at install time as part of the app shell.",[135,1475,1477],{"id":1476},"strategy-1-cache-first","Strategy 1: Cache First",[140,1479,1481],{"className":975,"code":1480,"language":977,"meta":145,"style":145},"// sw.js\nconst STATIC_CACHE = 'static-v1';\n\nself.addEventListener('fetch', (event) => {\n  event.respondWith(\n    caches.open(STATIC_CACHE).then(async (cache) => {\n      const cached = await cache.match(event.request);\n      if (cached) return cached;\n\n      const response = await fetch(event.request);\n      // Only cache successful responses\n      if (response.ok) {\n        cache.put(event.request, response.clone());\n      }\n      return response;\n    })\n  );\n});\n",[117,1482,1483,1487,1502,1506,1526,1534,1566,1585,1598,1602,1617,1622,1629,1646,1650,1657,1661,1665],{"__ignoreMap":145},[149,1484,1485],{"class":151,"line":152},[149,1486,1285],{"class":984},[149,1488,1489,1492,1495,1497,1500],{"class":151,"line":159},[149,1490,1491],{"class":996},"const",[149,1493,1494],{"class":162}," STATIC_CACHE",[149,1496,1081],{"class":996},[149,1498,1499],{"class":169}," 'static-v1'",[149,1501,1053],{"class":155},[149,1503,1504],{"class":151,"line":176},[149,1505,991],{"emptyLinePlaceholder":990},[149,1507,1508,1510,1512,1514,1516,1518,1520,1522,1524],{"class":151,"line":189},[149,1509,1294],{"class":155},[149,1511,1123],{"class":781},[149,1513,1020],{"class":155},[149,1515,1301],{"class":169},[149,1517,1304],{"class":155},[149,1519,1308],{"class":1307},[149,1521,1311],{"class":155},[149,1523,1134],{"class":996},[149,1525,1070],{"class":155},[149,1527,1528,1530,1532],{"class":151,"line":202},[149,1529,1334],{"class":155},[149,1531,1337],{"class":781},[149,1533,1340],{"class":155},[149,1535,1536,1538,1541,1543,1546,1549,1551,1553,1555,1557,1560,1562,1564],{"class":151,"line":215},[149,1537,1350],{"class":155},[149,1539,1540],{"class":781},"open",[149,1542,1020],{"class":155},[149,1544,1545],{"class":162},"STATIC_CACHE",[149,1547,1548],{"class":155},").",[149,1550,1359],{"class":781},[149,1552,1020],{"class":155},[149,1554,997],{"class":996},[149,1556,1014],{"class":155},[149,1558,1559],{"class":1307},"cache",[149,1561,1311],{"class":155},[149,1563,1134],{"class":996},[149,1565,1070],{"class":155},[149,1567,1568,1570,1573,1575,1577,1580,1582],{"class":151,"line":227},[149,1569,1141],{"class":996},[149,1571,1572],{"class":162}," cached",[149,1574,1081],{"class":996},[149,1576,1084],{"class":996},[149,1578,1579],{"class":155}," cache.",[149,1581,1353],{"class":781},[149,1583,1584],{"class":155},"(event.request);\n",[149,1586,1587,1589,1592,1595],{"class":151,"line":240},[149,1588,1376],{"class":996},[149,1590,1591],{"class":155}," (cached) ",[149,1593,1594],{"class":996},"return",[149,1596,1597],{"class":155}," cached;\n",[149,1599,1600],{"class":151,"line":253},[149,1601,991],{"emptyLinePlaceholder":990},[149,1603,1604,1606,1609,1611,1613,1615],{"class":151,"line":266},[149,1605,1141],{"class":996},[149,1607,1608],{"class":162}," response",[149,1610,1081],{"class":996},[149,1612,1084],{"class":996},[149,1614,1403],{"class":781},[149,1616,1584],{"class":155},[149,1618,1619],{"class":151,"line":279},[149,1620,1621],{"class":984},"      // Only cache successful responses\n",[149,1623,1624,1626],{"class":151,"line":292},[149,1625,1376],{"class":996},[149,1627,1628],{"class":155}," (response.ok) {\n",[149,1630,1631,1634,1637,1640,1643],{"class":151,"line":305},[149,1632,1633],{"class":155},"        cache.",[149,1635,1636],{"class":781},"put",[149,1638,1639],{"class":155},"(event.request, response.",[149,1641,1642],{"class":781},"clone",[149,1644,1645],{"class":155},"());\n",[149,1647,1648],{"class":151,"line":314},[149,1649,1395],{"class":155},[149,1651,1652,1654],{"class":151,"line":320},[149,1653,1400],{"class":996},[149,1655,1656],{"class":155}," response;\n",[149,1658,1659],{"class":151,"line":333},[149,1660,1414],{"class":155},[149,1662,1663],{"class":151,"line":346},[149,1664,1419],{"class":155},[149,1666,1667],{"class":151,"line":359},[149,1668,1424],{"class":155},[16,1670,1671,1672,1675,1676,1679,1680,1682],{},"Notice ",[117,1673,1674],{},"response.clone()",". A Response body can only be consumed once (it is a stream). If you ",[117,1677,1678],{},"cache.put()"," and then ",[117,1681,1594],{}," the same response, the body will already be consumed and the page gets an empty response. Always clone before caching.",[135,1684,1686],{"id":1685},"strategy-2-network-first","Strategy 2: Network First",[140,1688,1690],{"className":975,"code":1689,"language":977,"meta":145,"style":145},"// sw.js\nconst DYNAMIC_CACHE = 'dynamic-v1';\n\nasync function networkFirst(request) {\n  try {\n    const response = await fetch(request);\n    if (response.ok) {\n      const cache = await caches.open(DYNAMIC_CACHE);\n      cache.put(request, response.clone());\n    }\n    return response;\n  } catch (error) {\n    // Network failed: try cache\n    const cached = await caches.match(request);\n    if (cached) return cached;\n\n    // Nothing in cache either: return offline fallback\n    return caches.match('/offline.html');\n  }\n}\n\nself.addEventListener('fetch', (event) => {\n  if (event.request.mode === 'navigate') {\n    event.respondWith(networkFirst(event.request));\n  }\n});\n",[117,1691,1692,1696,1710,1714,1731,1737,1752,1759,1782,1796,1800,1806,1814,1819,1835,1845,1849,1854,1869,1873,1877,1881,1901,1916,1931,1935],{"__ignoreMap":145},[149,1693,1694],{"class":151,"line":152},[149,1695,1285],{"class":984},[149,1697,1698,1700,1703,1705,1708],{"class":151,"line":159},[149,1699,1491],{"class":996},[149,1701,1702],{"class":162}," DYNAMIC_CACHE",[149,1704,1081],{"class":996},[149,1706,1707],{"class":169}," 'dynamic-v1'",[149,1709,1053],{"class":155},[149,1711,1712],{"class":151,"line":176},[149,1713,991],{"emptyLinePlaceholder":990},[149,1715,1716,1718,1720,1723,1725,1728],{"class":151,"line":189},[149,1717,997],{"class":996},[149,1719,1000],{"class":996},[149,1721,1722],{"class":781}," networkFirst",[149,1724,1020],{"class":155},[149,1726,1727],{"class":1307},"request",[149,1729,1730],{"class":155},") {\n",[149,1732,1733,1735],{"class":151,"line":202},[149,1734,1067],{"class":996},[149,1736,1070],{"class":155},[149,1738,1739,1741,1743,1745,1747,1749],{"class":151,"line":215},[149,1740,1075],{"class":996},[149,1742,1608],{"class":162},[149,1744,1081],{"class":996},[149,1746,1084],{"class":996},[149,1748,1403],{"class":781},[149,1750,1751],{"class":155},"(request);\n",[149,1753,1754,1757],{"class":151,"line":227},[149,1755,1756],{"class":996},"    if",[149,1758,1628],{"class":155},[149,1760,1761,1763,1766,1768,1770,1773,1775,1777,1780],{"class":151,"line":240},[149,1762,1141],{"class":996},[149,1764,1765],{"class":162}," cache",[149,1767,1081],{"class":996},[149,1769,1084],{"class":996},[149,1771,1772],{"class":155}," caches.",[149,1774,1540],{"class":781},[149,1776,1020],{"class":155},[149,1778,1779],{"class":162},"DYNAMIC_CACHE",[149,1781,1045],{"class":155},[149,1783,1784,1787,1789,1792,1794],{"class":151,"line":253},[149,1785,1786],{"class":155},"      cache.",[149,1788,1636],{"class":781},[149,1790,1791],{"class":155},"(request, response.",[149,1793,1642],{"class":781},[149,1795,1645],{"class":155},[149,1797,1798],{"class":151,"line":266},[149,1799,482],{"class":155},[149,1801,1802,1804],{"class":151,"line":279},[149,1803,1050],{"class":996},[149,1805,1656],{"class":155},[149,1807,1808,1810,1812],{"class":151,"line":292},[149,1809,1191],{"class":155},[149,1811,1194],{"class":996},[149,1813,1197],{"class":155},[149,1815,1816],{"class":151,"line":305},[149,1817,1818],{"class":984},"    // Network failed: try cache\n",[149,1820,1821,1823,1825,1827,1829,1831,1833],{"class":151,"line":314},[149,1822,1075],{"class":996},[149,1824,1572],{"class":162},[149,1826,1081],{"class":996},[149,1828,1084],{"class":996},[149,1830,1772],{"class":155},[149,1832,1353],{"class":781},[149,1834,1751],{"class":155},[149,1836,1837,1839,1841,1843],{"class":151,"line":320},[149,1838,1756],{"class":996},[149,1840,1591],{"class":155},[149,1842,1594],{"class":996},[149,1844,1597],{"class":155},[149,1846,1847],{"class":151,"line":333},[149,1848,991],{"emptyLinePlaceholder":990},[149,1850,1851],{"class":151,"line":346},[149,1852,1853],{"class":984},"    // Nothing in cache either: return offline fallback\n",[149,1855,1856,1858,1860,1862,1864,1867],{"class":151,"line":359},[149,1857,1050],{"class":996},[149,1859,1772],{"class":155},[149,1861,1353],{"class":781},[149,1863,1020],{"class":155},[149,1865,1866],{"class":169},"'/offline.html'",[149,1868,1045],{"class":155},[149,1870,1871],{"class":151,"line":370},[149,1872,1058],{"class":155},[149,1874,1875],{"class":151,"line":376},[149,1876,755],{"class":155},[149,1878,1879],{"class":151,"line":381},[149,1880,991],{"emptyLinePlaceholder":990},[149,1882,1883,1885,1887,1889,1891,1893,1895,1897,1899],{"class":151,"line":393},[149,1884,1294],{"class":155},[149,1886,1123],{"class":781},[149,1888,1020],{"class":155},[149,1890,1301],{"class":169},[149,1892,1304],{"class":155},[149,1894,1308],{"class":1307},[149,1896,1311],{"class":155},[149,1898,1134],{"class":996},[149,1900,1070],{"class":155},[149,1902,1903,1905,1908,1911,1914],{"class":151,"line":405},[149,1904,1011],{"class":996},[149,1906,1907],{"class":155}," (event.request.mode ",[149,1909,1910],{"class":996},"===",[149,1912,1913],{"class":169}," 'navigate'",[149,1915,1730],{"class":155},[149,1917,1918,1921,1923,1925,1928],{"class":151,"line":416},[149,1919,1920],{"class":155},"    event.",[149,1922,1337],{"class":781},[149,1924,1020],{"class":155},[149,1926,1927],{"class":781},"networkFirst",[149,1929,1930],{"class":155},"(event.request));\n",[149,1932,1933],{"class":151,"line":425},[149,1934,1058],{"class":155},[149,1936,1937],{"class":151,"line":430},[149,1938,1424],{"class":155},[135,1940,1942],{"id":1941},"strategy-3-stale-while-revalidate","Strategy 3: Stale While Revalidate",[16,1944,1945],{},"This strategy gives you the best of both worlds for most content: instant response from cache AND fresh data for next time.",[140,1947,1949],{"className":975,"code":1948,"language":977,"meta":145,"style":145},"// sw.js\nasync function staleWhileRevalidate(request) {\n  const cache = await caches.open('swr-cache-v1');\n  const cached = await cache.match(request);\n\n  // Kick off a network fetch in the background regardless\n  const networkFetch = fetch(request).then((response) => {\n    if (response.ok) {\n      cache.put(request, response.clone());\n    }\n    return response;\n  });\n\n  // Return cached immediately if available, otherwise wait for network\n  return cached ?? networkFetch;\n}\n",[117,1950,1951,1955,1970,1992,2008,2012,2017,2044,2050,2062,2066,2072,2077,2081,2086,2100],{"__ignoreMap":145},[149,1952,1953],{"class":151,"line":152},[149,1954,1285],{"class":984},[149,1956,1957,1959,1961,1964,1966,1968],{"class":151,"line":159},[149,1958,997],{"class":996},[149,1960,1000],{"class":996},[149,1962,1963],{"class":781}," staleWhileRevalidate",[149,1965,1020],{"class":155},[149,1967,1727],{"class":1307},[149,1969,1730],{"class":155},[149,1971,1972,1975,1977,1979,1981,1983,1985,1987,1990],{"class":151,"line":176},[149,1973,1974],{"class":996},"  const",[149,1976,1765],{"class":162},[149,1978,1081],{"class":996},[149,1980,1084],{"class":996},[149,1982,1772],{"class":155},[149,1984,1540],{"class":781},[149,1986,1020],{"class":155},[149,1988,1989],{"class":169},"'swr-cache-v1'",[149,1991,1045],{"class":155},[149,1993,1994,1996,1998,2000,2002,2004,2006],{"class":151,"line":189},[149,1995,1974],{"class":996},[149,1997,1572],{"class":162},[149,1999,1081],{"class":996},[149,2001,1084],{"class":996},[149,2003,1579],{"class":155},[149,2005,1353],{"class":781},[149,2007,1751],{"class":155},[149,2009,2010],{"class":151,"line":202},[149,2011,991],{"emptyLinePlaceholder":990},[149,2013,2014],{"class":151,"line":215},[149,2015,2016],{"class":984},"  // Kick off a network fetch in the background regardless\n",[149,2018,2019,2021,2024,2026,2028,2031,2033,2035,2038,2040,2042],{"class":151,"line":227},[149,2020,1974],{"class":996},[149,2022,2023],{"class":162}," networkFetch",[149,2025,1081],{"class":996},[149,2027,1403],{"class":781},[149,2029,2030],{"class":155},"(request).",[149,2032,1359],{"class":781},[149,2034,1362],{"class":155},[149,2036,2037],{"class":1307},"response",[149,2039,1311],{"class":155},[149,2041,1134],{"class":996},[149,2043,1070],{"class":155},[149,2045,2046,2048],{"class":151,"line":240},[149,2047,1756],{"class":996},[149,2049,1628],{"class":155},[149,2051,2052,2054,2056,2058,2060],{"class":151,"line":253},[149,2053,1786],{"class":155},[149,2055,1636],{"class":781},[149,2057,1791],{"class":155},[149,2059,1642],{"class":781},[149,2061,1645],{"class":155},[149,2063,2064],{"class":151,"line":266},[149,2065,482],{"class":155},[149,2067,2068,2070],{"class":151,"line":279},[149,2069,1050],{"class":996},[149,2071,1656],{"class":155},[149,2073,2074],{"class":151,"line":292},[149,2075,2076],{"class":155},"  });\n",[149,2078,2079],{"class":151,"line":305},[149,2080,991],{"emptyLinePlaceholder":990},[149,2082,2083],{"class":151,"line":314},[149,2084,2085],{"class":984},"  // Return cached immediately if available, otherwise wait for network\n",[149,2087,2088,2091,2094,2097],{"class":151,"line":320},[149,2089,2090],{"class":996},"  return",[149,2092,2093],{"class":155}," cached ",[149,2095,2096],{"class":996},"??",[149,2098,2099],{"class":155}," networkFetch;\n",[149,2101,2102],{"class":151,"line":333},[149,2103,755],{"class":155},[135,2105,2107],{"id":2106},"pre-caching-the-app-shell","Pre-caching: The App Shell",[16,2109,129,2110,2113],{},[23,2111,2112],{},"App Shell"," pattern is fundamental to PWAs. You identify the minimal set of assets needed to show a meaningful UI (your HTML skeleton, main CSS, core JS bundle, logo, fonts) and cache them at install time. The app shell should never change for a given version.",[140,2115,2117],{"className":975,"code":2116,"language":977,"meta":145,"style":145},"// sw.js\nconst APP_SHELL_CACHE = 'app-shell-v1';\n\n// These files are pre-cached when the service worker installs\nconst APP_SHELL_FILES = [\n  '/',\n  '/offline.html',\n  '/styles.a3f9b2.css',\n  '/app.8d7c3e.js',\n  '/icons/icon-192x192.png',\n  '/fonts/fira-sans.woff2',\n];\n\nself.addEventListener('install', (event) => {\n  event.waitUntil(\n    caches.open(APP_SHELL_CACHE).then((cache) => {\n      console.log('Pre-caching app shell');\n      return cache.addAll(APP_SHELL_FILES);\n    })\n  );\n\n  // Skip waiting so this SW activates immediately\n  self.skipWaiting();\n});\n",[117,2118,2119,2123,2137,2141,2146,2158,2165,2172,2179,2186,2193,2200,2205,2209,2230,2239,2264,2277,2293,2297,2301,2305,2310,2321],{"__ignoreMap":145},[149,2120,2121],{"class":151,"line":152},[149,2122,1285],{"class":984},[149,2124,2125,2127,2130,2132,2135],{"class":151,"line":159},[149,2126,1491],{"class":996},[149,2128,2129],{"class":162}," APP_SHELL_CACHE",[149,2131,1081],{"class":996},[149,2133,2134],{"class":169}," 'app-shell-v1'",[149,2136,1053],{"class":155},[149,2138,2139],{"class":151,"line":176},[149,2140,991],{"emptyLinePlaceholder":990},[149,2142,2143],{"class":151,"line":189},[149,2144,2145],{"class":984},"// These files are pre-cached when the service worker installs\n",[149,2147,2148,2150,2153,2155],{"class":151,"line":202},[149,2149,1491],{"class":996},[149,2151,2152],{"class":162}," APP_SHELL_FILES",[149,2154,1081],{"class":996},[149,2156,2157],{"class":155}," [\n",[149,2159,2160,2163],{"class":151,"line":215},[149,2161,2162],{"class":169},"  '/'",[149,2164,173],{"class":155},[149,2166,2167,2170],{"class":151,"line":227},[149,2168,2169],{"class":169},"  '/offline.html'",[149,2171,173],{"class":155},[149,2173,2174,2177],{"class":151,"line":240},[149,2175,2176],{"class":169},"  '/styles.a3f9b2.css'",[149,2178,173],{"class":155},[149,2180,2181,2184],{"class":151,"line":253},[149,2182,2183],{"class":169},"  '/app.8d7c3e.js'",[149,2185,173],{"class":155},[149,2187,2188,2191],{"class":151,"line":266},[149,2189,2190],{"class":169},"  '/icons/icon-192x192.png'",[149,2192,173],{"class":155},[149,2194,2195,2198],{"class":151,"line":279},[149,2196,2197],{"class":169},"  '/fonts/fira-sans.woff2'",[149,2199,173],{"class":155},[149,2201,2202],{"class":151,"line":292},[149,2203,2204],{"class":155},"];\n",[149,2206,2207],{"class":151,"line":305},[149,2208,991],{"emptyLinePlaceholder":990},[149,2210,2211,2213,2215,2217,2220,2222,2224,2226,2228],{"class":151,"line":314},[149,2212,1294],{"class":155},[149,2214,1123],{"class":781},[149,2216,1020],{"class":155},[149,2218,2219],{"class":169},"'install'",[149,2221,1304],{"class":155},[149,2223,1308],{"class":1307},[149,2225,1311],{"class":155},[149,2227,1134],{"class":996},[149,2229,1070],{"class":155},[149,2231,2232,2234,2237],{"class":151,"line":320},[149,2233,1334],{"class":155},[149,2235,2236],{"class":781},"waitUntil",[149,2238,1340],{"class":155},[149,2240,2241,2243,2245,2247,2250,2252,2254,2256,2258,2260,2262],{"class":151,"line":333},[149,2242,1350],{"class":155},[149,2244,1540],{"class":781},[149,2246,1020],{"class":155},[149,2248,2249],{"class":162},"APP_SHELL_CACHE",[149,2251,1548],{"class":155},[149,2253,1359],{"class":781},[149,2255,1362],{"class":155},[149,2257,1559],{"class":1307},[149,2259,1311],{"class":155},[149,2261,1134],{"class":996},[149,2263,1070],{"class":155},[149,2265,2266,2268,2270,2272,2275],{"class":151,"line":346},[149,2267,1154],{"class":155},[149,2269,1037],{"class":781},[149,2271,1020],{"class":155},[149,2273,2274],{"class":169},"'Pre-caching app shell'",[149,2276,1045],{"class":155},[149,2278,2279,2281,2283,2286,2288,2291],{"class":151,"line":359},[149,2280,1400],{"class":996},[149,2282,1579],{"class":155},[149,2284,2285],{"class":781},"addAll",[149,2287,1020],{"class":155},[149,2289,2290],{"class":162},"APP_SHELL_FILES",[149,2292,1045],{"class":155},[149,2294,2295],{"class":151,"line":370},[149,2296,1414],{"class":155},[149,2298,2299],{"class":151,"line":376},[149,2300,1419],{"class":155},[149,2302,2303],{"class":151,"line":381},[149,2304,991],{"emptyLinePlaceholder":990},[149,2306,2307],{"class":151,"line":393},[149,2308,2309],{"class":984},"  // Skip waiting so this SW activates immediately\n",[149,2311,2312,2315,2318],{"class":151,"line":405},[149,2313,2314],{"class":155},"  self.",[149,2316,2317],{"class":781},"skipWaiting",[149,2319,2320],{"class":155},"();\n",[149,2322,2323],{"class":151,"line":416},[149,2324,1424],{"class":155},[16,2326,2327,2330,2331,2334],{},[117,2328,2329],{},"event.waitUntil()"," keeps the service worker in the installing state until the promise resolves. If any file in ",[117,2332,2333],{},"addAll()"," fails to download, the entire installation fails and the service worker does not activate. Be conservative with which files you include here.",[135,2336,2338],{"id":2337},"cleaning-up-old-caches","Cleaning Up Old Caches",[16,2340,2341,2342,2345,2346,2349],{},"When you deploy a new version (new cache name ",[117,2343,2344],{},"app-shell-v2","), old caches from previous versions sit on the user's device eating storage. The ",[117,2347,2348],{},"activate"," event is the right place to clean them up:",[140,2351,2353],{"className":975,"code":2352,"language":977,"meta":145,"style":145},"// sw.js\nconst CURRENT_CACHES = ['app-shell-v2', 'dynamic-v1'];\n\nself.addEventListener('activate', (event) => {\n  event.waitUntil(\n    caches.keys().then((cacheNames) => {\n      return Promise.all(\n        cacheNames\n          .filter((name) => !CURRENT_CACHES.includes(name))\n          .map((name) => {\n            console.log('Deleting old cache:', name);\n            return caches.delete(name);\n          })\n      );\n    })\n  );\n\n  // Claim all clients immediately (don't wait for tab reopen)\n  self.clients.claim();\n});\n",[117,2354,2355,2359,2381,2385,2406,2414,2437,2451,2456,2487,2504,2519,2532,2537,2542,2546,2550,2554,2559,2569],{"__ignoreMap":145},[149,2356,2357],{"class":151,"line":152},[149,2358,1285],{"class":984},[149,2360,2361,2363,2366,2368,2371,2374,2376,2379],{"class":151,"line":159},[149,2362,1491],{"class":996},[149,2364,2365],{"class":162}," CURRENT_CACHES",[149,2367,1081],{"class":996},[149,2369,2370],{"class":155}," [",[149,2372,2373],{"class":169},"'app-shell-v2'",[149,2375,697],{"class":155},[149,2377,2378],{"class":169},"'dynamic-v1'",[149,2380,2204],{"class":155},[149,2382,2383],{"class":151,"line":176},[149,2384,991],{"emptyLinePlaceholder":990},[149,2386,2387,2389,2391,2393,2396,2398,2400,2402,2404],{"class":151,"line":189},[149,2388,1294],{"class":155},[149,2390,1123],{"class":781},[149,2392,1020],{"class":155},[149,2394,2395],{"class":169},"'activate'",[149,2397,1304],{"class":155},[149,2399,1308],{"class":1307},[149,2401,1311],{"class":155},[149,2403,1134],{"class":996},[149,2405,1070],{"class":155},[149,2407,2408,2410,2412],{"class":151,"line":202},[149,2409,1334],{"class":155},[149,2411,2236],{"class":781},[149,2413,1340],{"class":155},[149,2415,2416,2418,2421,2424,2426,2428,2431,2433,2435],{"class":151,"line":215},[149,2417,1350],{"class":155},[149,2419,2420],{"class":781},"keys",[149,2422,2423],{"class":155},"().",[149,2425,1359],{"class":781},[149,2427,1362],{"class":155},[149,2429,2430],{"class":1307},"cacheNames",[149,2432,1311],{"class":155},[149,2434,1134],{"class":996},[149,2436,1070],{"class":155},[149,2438,2439,2441,2444,2446,2449],{"class":151,"line":227},[149,2440,1400],{"class":996},[149,2442,2443],{"class":162}," Promise",[149,2445,856],{"class":155},[149,2447,2448],{"class":781},"all",[149,2450,1340],{"class":155},[149,2452,2453],{"class":151,"line":240},[149,2454,2455],{"class":155},"        cacheNames\n",[149,2457,2458,2461,2464,2466,2469,2471,2473,2476,2479,2481,2484],{"class":151,"line":253},[149,2459,2460],{"class":155},"          .",[149,2462,2463],{"class":781},"filter",[149,2465,1362],{"class":155},[149,2467,2468],{"class":1307},"name",[149,2470,1311],{"class":155},[149,2472,1134],{"class":996},[149,2474,2475],{"class":996}," !",[149,2477,2478],{"class":162},"CURRENT_CACHES",[149,2480,856],{"class":155},[149,2482,2483],{"class":781},"includes",[149,2485,2486],{"class":155},"(name))\n",[149,2488,2489,2491,2494,2496,2498,2500,2502],{"class":151,"line":266},[149,2490,2460],{"class":155},[149,2492,2493],{"class":781},"map",[149,2495,1362],{"class":155},[149,2497,2468],{"class":1307},[149,2499,1311],{"class":155},[149,2501,1134],{"class":996},[149,2503,1070],{"class":155},[149,2505,2506,2509,2511,2513,2516],{"class":151,"line":279},[149,2507,2508],{"class":155},"            console.",[149,2510,1037],{"class":781},[149,2512,1020],{"class":155},[149,2514,2515],{"class":169},"'Deleting old cache:'",[149,2517,2518],{"class":155},", name);\n",[149,2520,2521,2524,2526,2529],{"class":151,"line":292},[149,2522,2523],{"class":996},"            return",[149,2525,1772],{"class":155},[149,2527,2528],{"class":781},"delete",[149,2530,2531],{"class":155},"(name);\n",[149,2533,2534],{"class":151,"line":305},[149,2535,2536],{"class":155},"          })\n",[149,2538,2539],{"class":151,"line":314},[149,2540,2541],{"class":155},"      );\n",[149,2543,2544],{"class":151,"line":320},[149,2545,1414],{"class":155},[149,2547,2548],{"class":151,"line":333},[149,2549,1419],{"class":155},[149,2551,2552],{"class":151,"line":346},[149,2553,991],{"emptyLinePlaceholder":990},[149,2555,2556],{"class":151,"line":359},[149,2557,2558],{"class":984},"  // Claim all clients immediately (don't wait for tab reopen)\n",[149,2560,2561,2564,2567],{"class":151,"line":370},[149,2562,2563],{"class":155},"  self.clients.",[149,2565,2566],{"class":781},"claim",[149,2568,2320],{"class":155},[149,2570,2571],{"class":151,"line":376},[149,2572,1424],{"class":155},[16,2574,2575,2578],{},[117,2576,2577],{},"self.clients.claim()"," makes the activated service worker take control of all open tabs immediately, without waiting for a page reload.",[135,2580,2582],{"id":2581},"a-complete-production-ready-service-worker","A Complete, Production-Ready Service Worker",[16,2584,2585],{},"Here is a service worker that combines everything above, routing different request types to the appropriate strategy:",[140,2587,2589],{"className":975,"code":2588,"language":977,"meta":145,"style":145},"// sw.js\n\nconst APP_SHELL_CACHE = 'app-shell-v2';\nconst DYNAMIC_CACHE = 'dynamic-v1';\nconst CURRENT_CACHES = [APP_SHELL_CACHE, DYNAMIC_CACHE];\n\nconst APP_SHELL_FILES = [\n  '/',\n  '/offline.html',\n  '/styles.abc123.css',\n  '/app.def456.js',\n];\n\n// ---------- INSTALL ----------\nself.addEventListener('install', (event) => {\n  event.waitUntil(\n    caches.open(APP_SHELL_CACHE).then((cache) => cache.addAll(APP_SHELL_FILES))\n  );\n  self.skipWaiting();\n});\n\n// ---------- ACTIVATE ----------\nself.addEventListener('activate', (event) => {\n  event.waitUntil(\n    caches.keys().then((names) =>\n      Promise.all(\n        names\n          .filter((name) => !CURRENT_CACHES.includes(name))\n          .map((name) => caches.delete(name))\n      )\n    )\n  );\n  self.clients.claim();\n});\n\n// ---------- FETCH ----------\nself.addEventListener('fetch', (event) => {\n  const { request } = event;\n  const url = new URL(request.url);\n\n  // 1. Skip non-GET requests and cross-origin requests\n  if (request.method !== 'GET' || url.origin !== location.origin) return;\n\n  // 2. API calls: network first\n  if (url.pathname.startsWith('/api/')) {\n    event.respondWith(networkFirst(request));\n    return;\n  }\n\n  // 3. Static assets with content hash in filename: cache first\n  if (/\\.[a-f0-9]{6,}\\.(css|js|woff2|png|jpg)$/.test(url.pathname)) {\n    event.respondWith(cacheFirst(request));\n    return;\n  }\n\n  // 4. HTML navigation: stale while revalidate\n  if (request.mode === 'navigate') {\n    event.respondWith(staleWhileRevalidate(request));\n    return;\n  }\n\n  // 5. Everything else: stale while revalidate\n  event.respondWith(staleWhileRevalidate(request));\n});\n\n// ---------- STRATEGIES ----------\n\nasync function cacheFirst(request) {\n  const cached = await caches.match(request);\n  if (cached) return cached;\n  const response = await fetch(request);\n  if (response.ok) {\n    const cache = await caches.open(DYNAMIC_CACHE);\n    cache.put(request, response.clone());\n  }\n  return response;\n}\n\nasync function networkFirst(request) {\n  const cache = await caches.open(DYNAMIC_CACHE);\n  try {\n    const response = await fetch(request);\n    if (response.ok) cache.put(request, response.clone());\n    return response;\n  } catch {\n    const cached = await cache.match(request);\n    return cached ?? new Response(JSON.stringify({ error: 'Offline' }), {\n      headers: { 'Content-Type': 'application/json' },\n    });\n  }\n}\n\nasync function staleWhileRevalidate(request) {\n  const cache = await caches.open(DYNAMIC_CACHE);\n  const cached = await cache.match(request);\n  const networkFetch = fetch(request).then((response) => {\n    if (response.ok) cache.put(request, response.clone());\n    return response;\n  }).catch(() => caches.match('/offline.html'));\n\n  return cached ?? networkFetch;\n}\n",[117,2590,2591,2595,2599,2612,2624,2642,2646,2656,2662,2668,2675,2682,2686,2690,2695,2715,2723,2756,2760,2768,2772,2776,2781,2801,2809,2829,2840,2845,2869,2889,2894,2899,2903,2911,2915,2919,2924,2944,2961,2979,2983,2988,3016,3020,3025,3043,3056,3062,3066,3070,3075,3132,3145,3151,3155,3159,3164,3177,3190,3197,3202,3207,3213,3226,3231,3236,3242,3247,3263,3280,3291,3306,3313,3334,3348,3353,3360,3365,3370,3385,3406,3413,3428,3444,3451,3460,3477,3510,3527,3532,3537,3542,3547,3562,3583,3600,3625,3640,3647,3671,3676,3687],{"__ignoreMap":145},[149,2592,2593],{"class":151,"line":152},[149,2594,1285],{"class":984},[149,2596,2597],{"class":151,"line":159},[149,2598,991],{"emptyLinePlaceholder":990},[149,2600,2601,2603,2605,2607,2610],{"class":151,"line":176},[149,2602,1491],{"class":996},[149,2604,2129],{"class":162},[149,2606,1081],{"class":996},[149,2608,2609],{"class":169}," 'app-shell-v2'",[149,2611,1053],{"class":155},[149,2613,2614,2616,2618,2620,2622],{"class":151,"line":189},[149,2615,1491],{"class":996},[149,2617,1702],{"class":162},[149,2619,1081],{"class":996},[149,2621,1707],{"class":169},[149,2623,1053],{"class":155},[149,2625,2626,2628,2630,2632,2634,2636,2638,2640],{"class":151,"line":202},[149,2627,1491],{"class":996},[149,2629,2365],{"class":162},[149,2631,1081],{"class":996},[149,2633,2370],{"class":155},[149,2635,2249],{"class":162},[149,2637,697],{"class":155},[149,2639,1779],{"class":162},[149,2641,2204],{"class":155},[149,2643,2644],{"class":151,"line":215},[149,2645,991],{"emptyLinePlaceholder":990},[149,2647,2648,2650,2652,2654],{"class":151,"line":227},[149,2649,1491],{"class":996},[149,2651,2152],{"class":162},[149,2653,1081],{"class":996},[149,2655,2157],{"class":155},[149,2657,2658,2660],{"class":151,"line":240},[149,2659,2162],{"class":169},[149,2661,173],{"class":155},[149,2663,2664,2666],{"class":151,"line":253},[149,2665,2169],{"class":169},[149,2667,173],{"class":155},[149,2669,2670,2673],{"class":151,"line":266},[149,2671,2672],{"class":169},"  '/styles.abc123.css'",[149,2674,173],{"class":155},[149,2676,2677,2680],{"class":151,"line":279},[149,2678,2679],{"class":169},"  '/app.def456.js'",[149,2681,173],{"class":155},[149,2683,2684],{"class":151,"line":292},[149,2685,2204],{"class":155},[149,2687,2688],{"class":151,"line":305},[149,2689,991],{"emptyLinePlaceholder":990},[149,2691,2692],{"class":151,"line":314},[149,2693,2694],{"class":984},"// ---------- INSTALL ----------\n",[149,2696,2697,2699,2701,2703,2705,2707,2709,2711,2713],{"class":151,"line":320},[149,2698,1294],{"class":155},[149,2700,1123],{"class":781},[149,2702,1020],{"class":155},[149,2704,2219],{"class":169},[149,2706,1304],{"class":155},[149,2708,1308],{"class":1307},[149,2710,1311],{"class":155},[149,2712,1134],{"class":996},[149,2714,1070],{"class":155},[149,2716,2717,2719,2721],{"class":151,"line":333},[149,2718,1334],{"class":155},[149,2720,2236],{"class":781},[149,2722,1340],{"class":155},[149,2724,2725,2727,2729,2731,2733,2735,2737,2739,2741,2743,2745,2747,2749,2751,2753],{"class":151,"line":346},[149,2726,1350],{"class":155},[149,2728,1540],{"class":781},[149,2730,1020],{"class":155},[149,2732,2249],{"class":162},[149,2734,1548],{"class":155},[149,2736,1359],{"class":781},[149,2738,1362],{"class":155},[149,2740,1559],{"class":1307},[149,2742,1311],{"class":155},[149,2744,1134],{"class":996},[149,2746,1579],{"class":155},[149,2748,2285],{"class":781},[149,2750,1020],{"class":155},[149,2752,2290],{"class":162},[149,2754,2755],{"class":155},"))\n",[149,2757,2758],{"class":151,"line":359},[149,2759,1419],{"class":155},[149,2761,2762,2764,2766],{"class":151,"line":370},[149,2763,2314],{"class":155},[149,2765,2317],{"class":781},[149,2767,2320],{"class":155},[149,2769,2770],{"class":151,"line":376},[149,2771,1424],{"class":155},[149,2773,2774],{"class":151,"line":381},[149,2775,991],{"emptyLinePlaceholder":990},[149,2777,2778],{"class":151,"line":393},[149,2779,2780],{"class":984},"// ---------- ACTIVATE ----------\n",[149,2782,2783,2785,2787,2789,2791,2793,2795,2797,2799],{"class":151,"line":405},[149,2784,1294],{"class":155},[149,2786,1123],{"class":781},[149,2788,1020],{"class":155},[149,2790,2395],{"class":169},[149,2792,1304],{"class":155},[149,2794,1308],{"class":1307},[149,2796,1311],{"class":155},[149,2798,1134],{"class":996},[149,2800,1070],{"class":155},[149,2802,2803,2805,2807],{"class":151,"line":416},[149,2804,1334],{"class":155},[149,2806,2236],{"class":781},[149,2808,1340],{"class":155},[149,2810,2811,2813,2815,2817,2819,2821,2824,2826],{"class":151,"line":425},[149,2812,1350],{"class":155},[149,2814,2420],{"class":781},[149,2816,2423],{"class":155},[149,2818,1359],{"class":781},[149,2820,1362],{"class":155},[149,2822,2823],{"class":1307},"names",[149,2825,1311],{"class":155},[149,2827,2828],{"class":996},"=>\n",[149,2830,2831,2834,2836,2838],{"class":151,"line":430},[149,2832,2833],{"class":162},"      Promise",[149,2835,856],{"class":155},[149,2837,2448],{"class":781},[149,2839,1340],{"class":155},[149,2841,2842],{"class":151,"line":435},[149,2843,2844],{"class":155},"        names\n",[149,2846,2847,2849,2851,2853,2855,2857,2859,2861,2863,2865,2867],{"class":151,"line":447},[149,2848,2460],{"class":155},[149,2850,2463],{"class":781},[149,2852,1362],{"class":155},[149,2854,2468],{"class":1307},[149,2856,1311],{"class":155},[149,2858,1134],{"class":996},[149,2860,2475],{"class":996},[149,2862,2478],{"class":162},[149,2864,856],{"class":155},[149,2866,2483],{"class":781},[149,2868,2486],{"class":155},[149,2870,2871,2873,2875,2877,2879,2881,2883,2885,2887],{"class":151,"line":459},[149,2872,2460],{"class":155},[149,2874,2493],{"class":781},[149,2876,1362],{"class":155},[149,2878,2468],{"class":1307},[149,2880,1311],{"class":155},[149,2882,1134],{"class":996},[149,2884,1772],{"class":155},[149,2886,2528],{"class":781},[149,2888,2486],{"class":155},[149,2890,2891],{"class":151,"line":470},[149,2892,2893],{"class":155},"      )\n",[149,2895,2896],{"class":151,"line":479},[149,2897,2898],{"class":155},"    )\n",[149,2900,2901],{"class":151,"line":485},[149,2902,1419],{"class":155},[149,2904,2905,2907,2909],{"class":151,"line":491},[149,2906,2563],{"class":155},[149,2908,2566],{"class":781},[149,2910,2320],{"class":155},[149,2912,2913],{"class":151,"line":499},[149,2914,1424],{"class":155},[149,2916,2917],{"class":151,"line":504},[149,2918,991],{"emptyLinePlaceholder":990},[149,2920,2921],{"class":151,"line":516},[149,2922,2923],{"class":984},"// ---------- FETCH ----------\n",[149,2925,2926,2928,2930,2932,2934,2936,2938,2940,2942],{"class":151,"line":528},[149,2927,1294],{"class":155},[149,2929,1123],{"class":781},[149,2931,1020],{"class":155},[149,2933,1301],{"class":169},[149,2935,1304],{"class":155},[149,2937,1308],{"class":1307},[149,2939,1311],{"class":155},[149,2941,1134],{"class":996},[149,2943,1070],{"class":155},[149,2945,2946,2948,2951,2953,2956,2958],{"class":151,"line":539},[149,2947,1974],{"class":996},[149,2949,2950],{"class":155}," { ",[149,2952,1727],{"class":162},[149,2954,2955],{"class":155}," } ",[149,2957,785],{"class":996},[149,2959,2960],{"class":155}," event;\n",[149,2962,2963,2965,2968,2970,2973,2976],{"class":151,"line":550},[149,2964,1974],{"class":996},[149,2966,2967],{"class":162}," url",[149,2969,1081],{"class":996},[149,2971,2972],{"class":996}," new",[149,2974,2975],{"class":781}," URL",[149,2977,2978],{"class":155},"(request.url);\n",[149,2980,2981],{"class":151,"line":555},[149,2982,991],{"emptyLinePlaceholder":990},[149,2984,2985],{"class":151,"line":560},[149,2986,2987],{"class":984},"  // 1. Skip non-GET requests and cross-origin requests\n",[149,2989,2990,2992,2995,2998,3001,3004,3007,3009,3012,3014],{"class":151,"line":572},[149,2991,1011],{"class":996},[149,2993,2994],{"class":155}," (request.method ",[149,2996,2997],{"class":996},"!==",[149,2999,3000],{"class":169}," 'GET'",[149,3002,3003],{"class":996}," ||",[149,3005,3006],{"class":155}," url.origin ",[149,3008,2997],{"class":996},[149,3010,3011],{"class":155}," location.origin) ",[149,3013,1594],{"class":996},[149,3015,1053],{"class":155},[149,3017,3018],{"class":151,"line":584},[149,3019,991],{"emptyLinePlaceholder":990},[149,3021,3022],{"class":151,"line":595},[149,3023,3024],{"class":984},"  // 2. API calls: network first\n",[149,3026,3027,3029,3032,3035,3037,3040],{"class":151,"line":605},[149,3028,1011],{"class":996},[149,3030,3031],{"class":155}," (url.pathname.",[149,3033,3034],{"class":781},"startsWith",[149,3036,1020],{"class":155},[149,3038,3039],{"class":169},"'/api/'",[149,3041,3042],{"class":155},")) {\n",[149,3044,3045,3047,3049,3051,3053],{"class":151,"line":610},[149,3046,1920],{"class":155},[149,3048,1337],{"class":781},[149,3050,1020],{"class":155},[149,3052,1927],{"class":781},[149,3054,3055],{"class":155},"(request));\n",[149,3057,3058,3060],{"class":151,"line":615},[149,3059,1050],{"class":996},[149,3061,1053],{"class":155},[149,3063,3064],{"class":151,"line":623},[149,3065,1058],{"class":155},[149,3067,3068],{"class":151,"line":628},[149,3069,991],{"emptyLinePlaceholder":990},[149,3071,3072],{"class":151,"line":641},[149,3073,3074],{"class":984},"  // 3. Static assets with content hash in filename: cache first\n",[149,3076,3077,3079,3081,3083,3087,3090,3093,3095,3098,3101,3104,3106,3109,3111,3114,3116,3119,3122,3124,3126,3129],{"class":151,"line":654},[149,3078,1011],{"class":996},[149,3080,1014],{"class":155},[149,3082,1256],{"class":169},[149,3084,3086],{"class":3085},"s691h","\\.",[149,3088,3089],{"class":162},"[a-f0-9]",[149,3091,3092],{"class":996},"{6,}",[149,3094,3086],{"class":3085},[149,3096,3097],{"class":169},"(css",[149,3099,3100],{"class":996},"|",[149,3102,3103],{"class":169},"js",[149,3105,3100],{"class":996},[149,3107,3108],{"class":169},"woff2",[149,3110,3100],{"class":996},[149,3112,3113],{"class":169},"png",[149,3115,3100],{"class":996},[149,3117,3118],{"class":169},"jpg)",[149,3120,3121],{"class":996},"$",[149,3123,1256],{"class":169},[149,3125,856],{"class":155},[149,3127,3128],{"class":781},"test",[149,3130,3131],{"class":155},"(url.pathname)) {\n",[149,3133,3134,3136,3138,3140,3143],{"class":151,"line":667},[149,3135,1920],{"class":155},[149,3137,1337],{"class":781},[149,3139,1020],{"class":155},[149,3141,3142],{"class":781},"cacheFirst",[149,3144,3055],{"class":155},[149,3146,3147,3149],{"class":151,"line":680},[149,3148,1050],{"class":996},[149,3150,1053],{"class":155},[149,3152,3153],{"class":151,"line":711},[149,3154,1058],{"class":155},[149,3156,3157],{"class":151,"line":716},[149,3158,991],{"emptyLinePlaceholder":990},[149,3160,3161],{"class":151,"line":721},[149,3162,3163],{"class":984},"  // 4. HTML navigation: stale while revalidate\n",[149,3165,3166,3168,3171,3173,3175],{"class":151,"line":741},[149,3167,1011],{"class":996},[149,3169,3170],{"class":155}," (request.mode ",[149,3172,1910],{"class":996},[149,3174,1913],{"class":169},[149,3176,1730],{"class":155},[149,3178,3179,3181,3183,3185,3188],{"class":151,"line":752},[149,3180,1920],{"class":155},[149,3182,1337],{"class":781},[149,3184,1020],{"class":155},[149,3186,3187],{"class":781},"staleWhileRevalidate",[149,3189,3055],{"class":155},[149,3191,3193,3195],{"class":151,"line":3192},59,[149,3194,1050],{"class":996},[149,3196,1053],{"class":155},[149,3198,3200],{"class":151,"line":3199},60,[149,3201,1058],{"class":155},[149,3203,3205],{"class":151,"line":3204},61,[149,3206,991],{"emptyLinePlaceholder":990},[149,3208,3210],{"class":151,"line":3209},62,[149,3211,3212],{"class":984},"  // 5. Everything else: stale while revalidate\n",[149,3214,3216,3218,3220,3222,3224],{"class":151,"line":3215},63,[149,3217,1334],{"class":155},[149,3219,1337],{"class":781},[149,3221,1020],{"class":155},[149,3223,3187],{"class":781},[149,3225,3055],{"class":155},[149,3227,3229],{"class":151,"line":3228},64,[149,3230,1424],{"class":155},[149,3232,3234],{"class":151,"line":3233},65,[149,3235,991],{"emptyLinePlaceholder":990},[149,3237,3239],{"class":151,"line":3238},66,[149,3240,3241],{"class":984},"// ---------- STRATEGIES ----------\n",[149,3243,3245],{"class":151,"line":3244},67,[149,3246,991],{"emptyLinePlaceholder":990},[149,3248,3250,3252,3254,3257,3259,3261],{"class":151,"line":3249},68,[149,3251,997],{"class":996},[149,3253,1000],{"class":996},[149,3255,3256],{"class":781}," cacheFirst",[149,3258,1020],{"class":155},[149,3260,1727],{"class":1307},[149,3262,1730],{"class":155},[149,3264,3266,3268,3270,3272,3274,3276,3278],{"class":151,"line":3265},69,[149,3267,1974],{"class":996},[149,3269,1572],{"class":162},[149,3271,1081],{"class":996},[149,3273,1084],{"class":996},[149,3275,1772],{"class":155},[149,3277,1353],{"class":781},[149,3279,1751],{"class":155},[149,3281,3283,3285,3287,3289],{"class":151,"line":3282},70,[149,3284,1011],{"class":996},[149,3286,1591],{"class":155},[149,3288,1594],{"class":996},[149,3290,1597],{"class":155},[149,3292,3294,3296,3298,3300,3302,3304],{"class":151,"line":3293},71,[149,3295,1974],{"class":996},[149,3297,1608],{"class":162},[149,3299,1081],{"class":996},[149,3301,1084],{"class":996},[149,3303,1403],{"class":781},[149,3305,1751],{"class":155},[149,3307,3309,3311],{"class":151,"line":3308},72,[149,3310,1011],{"class":996},[149,3312,1628],{"class":155},[149,3314,3316,3318,3320,3322,3324,3326,3328,3330,3332],{"class":151,"line":3315},73,[149,3317,1075],{"class":996},[149,3319,1765],{"class":162},[149,3321,1081],{"class":996},[149,3323,1084],{"class":996},[149,3325,1772],{"class":155},[149,3327,1540],{"class":781},[149,3329,1020],{"class":155},[149,3331,1779],{"class":162},[149,3333,1045],{"class":155},[149,3335,3337,3340,3342,3344,3346],{"class":151,"line":3336},74,[149,3338,3339],{"class":155},"    cache.",[149,3341,1636],{"class":781},[149,3343,1791],{"class":155},[149,3345,1642],{"class":781},[149,3347,1645],{"class":155},[149,3349,3351],{"class":151,"line":3350},75,[149,3352,1058],{"class":155},[149,3354,3356,3358],{"class":151,"line":3355},76,[149,3357,2090],{"class":996},[149,3359,1656],{"class":155},[149,3361,3363],{"class":151,"line":3362},77,[149,3364,755],{"class":155},[149,3366,3368],{"class":151,"line":3367},78,[149,3369,991],{"emptyLinePlaceholder":990},[149,3371,3373,3375,3377,3379,3381,3383],{"class":151,"line":3372},79,[149,3374,997],{"class":996},[149,3376,1000],{"class":996},[149,3378,1722],{"class":781},[149,3380,1020],{"class":155},[149,3382,1727],{"class":1307},[149,3384,1730],{"class":155},[149,3386,3388,3390,3392,3394,3396,3398,3400,3402,3404],{"class":151,"line":3387},80,[149,3389,1974],{"class":996},[149,3391,1765],{"class":162},[149,3393,1081],{"class":996},[149,3395,1084],{"class":996},[149,3397,1772],{"class":155},[149,3399,1540],{"class":781},[149,3401,1020],{"class":155},[149,3403,1779],{"class":162},[149,3405,1045],{"class":155},[149,3407,3409,3411],{"class":151,"line":3408},81,[149,3410,1067],{"class":996},[149,3412,1070],{"class":155},[149,3414,3416,3418,3420,3422,3424,3426],{"class":151,"line":3415},82,[149,3417,1075],{"class":996},[149,3419,1608],{"class":162},[149,3421,1081],{"class":996},[149,3423,1084],{"class":996},[149,3425,1403],{"class":781},[149,3427,1751],{"class":155},[149,3429,3431,3433,3436,3438,3440,3442],{"class":151,"line":3430},83,[149,3432,1756],{"class":996},[149,3434,3435],{"class":155}," (response.ok) cache.",[149,3437,1636],{"class":781},[149,3439,1791],{"class":155},[149,3441,1642],{"class":781},[149,3443,1645],{"class":155},[149,3445,3447,3449],{"class":151,"line":3446},84,[149,3448,1050],{"class":996},[149,3450,1656],{"class":155},[149,3452,3454,3456,3458],{"class":151,"line":3453},85,[149,3455,1191],{"class":155},[149,3457,1194],{"class":996},[149,3459,1070],{"class":155},[149,3461,3463,3465,3467,3469,3471,3473,3475],{"class":151,"line":3462},86,[149,3464,1075],{"class":996},[149,3466,1572],{"class":162},[149,3468,1081],{"class":996},[149,3470,1084],{"class":996},[149,3472,1579],{"class":155},[149,3474,1353],{"class":781},[149,3476,1751],{"class":155},[149,3478,3480,3482,3484,3486,3488,3491,3493,3496,3498,3501,3504,3507],{"class":151,"line":3479},87,[149,3481,1050],{"class":996},[149,3483,2093],{"class":155},[149,3485,2096],{"class":996},[149,3487,2972],{"class":996},[149,3489,3490],{"class":781}," Response",[149,3492,1020],{"class":155},[149,3494,3495],{"class":162},"JSON",[149,3497,856],{"class":155},[149,3499,3500],{"class":781},"stringify",[149,3502,3503],{"class":155},"({ error: ",[149,3505,3506],{"class":169},"'Offline'",[149,3508,3509],{"class":155}," }), {\n",[149,3511,3513,3516,3519,3521,3524],{"class":151,"line":3512},88,[149,3514,3515],{"class":155},"      headers: { ",[149,3517,3518],{"class":169},"'Content-Type'",[149,3520,166],{"class":155},[149,3522,3523],{"class":169},"'application/json'",[149,3525,3526],{"class":155}," },\n",[149,3528,3530],{"class":151,"line":3529},89,[149,3531,1111],{"class":155},[149,3533,3535],{"class":151,"line":3534},90,[149,3536,1058],{"class":155},[149,3538,3540],{"class":151,"line":3539},91,[149,3541,755],{"class":155},[149,3543,3545],{"class":151,"line":3544},92,[149,3546,991],{"emptyLinePlaceholder":990},[149,3548,3550,3552,3554,3556,3558,3560],{"class":151,"line":3549},93,[149,3551,997],{"class":996},[149,3553,1000],{"class":996},[149,3555,1963],{"class":781},[149,3557,1020],{"class":155},[149,3559,1727],{"class":1307},[149,3561,1730],{"class":155},[149,3563,3565,3567,3569,3571,3573,3575,3577,3579,3581],{"class":151,"line":3564},94,[149,3566,1974],{"class":996},[149,3568,1765],{"class":162},[149,3570,1081],{"class":996},[149,3572,1084],{"class":996},[149,3574,1772],{"class":155},[149,3576,1540],{"class":781},[149,3578,1020],{"class":155},[149,3580,1779],{"class":162},[149,3582,1045],{"class":155},[149,3584,3586,3588,3590,3592,3594,3596,3598],{"class":151,"line":3585},95,[149,3587,1974],{"class":996},[149,3589,1572],{"class":162},[149,3591,1081],{"class":996},[149,3593,1084],{"class":996},[149,3595,1579],{"class":155},[149,3597,1353],{"class":781},[149,3599,1751],{"class":155},[149,3601,3603,3605,3607,3609,3611,3613,3615,3617,3619,3621,3623],{"class":151,"line":3602},96,[149,3604,1974],{"class":996},[149,3606,2023],{"class":162},[149,3608,1081],{"class":996},[149,3610,1403],{"class":781},[149,3612,2030],{"class":155},[149,3614,1359],{"class":781},[149,3616,1362],{"class":155},[149,3618,2037],{"class":1307},[149,3620,1311],{"class":155},[149,3622,1134],{"class":996},[149,3624,1070],{"class":155},[149,3626,3628,3630,3632,3634,3636,3638],{"class":151,"line":3627},97,[149,3629,1756],{"class":996},[149,3631,3435],{"class":155},[149,3633,1636],{"class":781},[149,3635,1791],{"class":155},[149,3637,1642],{"class":781},[149,3639,1645],{"class":155},[149,3641,3643,3645],{"class":151,"line":3642},98,[149,3644,1050],{"class":996},[149,3646,1656],{"class":155},[149,3648,3650,3653,3655,3658,3660,3662,3664,3666,3668],{"class":151,"line":3649},99,[149,3651,3652],{"class":155},"  }).",[149,3654,1194],{"class":781},[149,3656,3657],{"class":155},"(() ",[149,3659,1134],{"class":996},[149,3661,1772],{"class":155},[149,3663,1353],{"class":781},[149,3665,1020],{"class":155},[149,3667,1866],{"class":169},[149,3669,3670],{"class":155},"));\n",[149,3672,3674],{"class":151,"line":3673},100,[149,3675,991],{"emptyLinePlaceholder":990},[149,3677,3679,3681,3683,3685],{"class":151,"line":3678},101,[149,3680,2090],{"class":996},[149,3682,2093],{"class":155},[149,3684,2096],{"class":996},[149,3686,2099],{"class":155},[149,3688,3690],{"class":151,"line":3689},102,[149,3691,755],{"class":155},[41,3693],{},[11,3695,3697],{"id":3696},"the-offline-page","The Offline Page",[16,3699,3700,3701,3704],{},"Every PWA should have a fallback ",[117,3702,3703],{},"/offline.html"," page to show when a navigation fails and there is no cached version. Keep it simple and informative:",[140,3706,3708],{"className":765,"code":3707,"language":767,"meta":145,"style":145},"\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n  \u003Cmeta charset=\"UTF-8\" />\n  \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  \u003Ctitle>You are offline\u003C/title>\n  \u003Cstyle>\n    body {\n      font-family: sans-serif;\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      min-height: 100vh;\n      margin: 0;\n      background: #f9fafb;\n      color: #111;\n    }\n    h1 { font-size: 1.5rem; }\n    p  { color: #6b7280; }\n  \u003C/style>\n\u003C/head>\n\u003Cbody>\n  \u003Ch1>You are offline\u003C/h1>\n  \u003Cp>Check your internet connection and try again.\u003C/p>\n  \u003Cbutton onclick=\"location.reload()\">Try again\u003C/button>\n\u003C/body>\n\u003C/html>\n",[117,3709,3710,3724,3740,3749,3767,3791,3805,3814,3821,3833,3845,3857,3869,3880,3895,3907,3919,3931,3935,3956,3974,3983,3992,4001,4014,4027,4060,4068],{"__ignoreMap":145},[149,3711,3712,3715,3718,3721],{"class":151,"line":152},[149,3713,3714],{"class":155},"\u003C!",[149,3716,3717],{"class":777},"DOCTYPE",[149,3719,3720],{"class":781}," html",[149,3722,3723],{"class":155},">\n",[149,3725,3726,3728,3730,3733,3735,3738],{"class":151,"line":159},[149,3727,774],{"class":155},[149,3729,767],{"class":777},[149,3731,3732],{"class":781}," lang",[149,3734,785],{"class":155},[149,3736,3737],{"class":169},"\"en\"",[149,3739,3723],{"class":155},[149,3741,3742,3744,3747],{"class":151,"line":176},[149,3743,774],{"class":155},[149,3745,3746],{"class":777},"head",[149,3748,3723],{"class":155},[149,3750,3751,3754,3757,3760,3762,3765],{"class":151,"line":189},[149,3752,3753],{"class":155},"  \u003C",[149,3755,3756],{"class":777},"meta",[149,3758,3759],{"class":781}," charset",[149,3761,785],{"class":155},[149,3763,3764],{"class":169},"\"UTF-8\"",[149,3766,799],{"class":155},[149,3768,3769,3771,3773,3776,3778,3781,3784,3786,3789],{"class":151,"line":202},[149,3770,3753],{"class":155},[149,3772,3756],{"class":777},[149,3774,3775],{"class":781}," name",[149,3777,785],{"class":155},[149,3779,3780],{"class":169},"\"viewport\"",[149,3782,3783],{"class":781}," content",[149,3785,785],{"class":155},[149,3787,3788],{"class":169},"\"width=device-width, initial-scale=1.0\"",[149,3790,799],{"class":155},[149,3792,3793,3795,3798,3801,3803],{"class":151,"line":215},[149,3794,3753],{"class":155},[149,3796,3797],{"class":777},"title",[149,3799,3800],{"class":155},">You are offline\u003C/",[149,3802,3797],{"class":777},[149,3804,3723],{"class":155},[149,3806,3807,3809,3812],{"class":151,"line":227},[149,3808,3753],{"class":155},[149,3810,3811],{"class":777},"style",[149,3813,3723],{"class":155},[149,3815,3816,3819],{"class":151,"line":240},[149,3817,3818],{"class":777},"    body",[149,3820,1070],{"class":155},[149,3822,3823,3826,3828,3831],{"class":151,"line":253},[149,3824,3825],{"class":162},"      font-family",[149,3827,166],{"class":155},[149,3829,3830],{"class":162},"sans-serif",[149,3832,1053],{"class":155},[149,3834,3835,3838,3840,3843],{"class":151,"line":266},[149,3836,3837],{"class":162},"      display",[149,3839,166],{"class":155},[149,3841,3842],{"class":162},"flex",[149,3844,1053],{"class":155},[149,3846,3847,3850,3852,3855],{"class":151,"line":279},[149,3848,3849],{"class":162},"      flex-direction",[149,3851,166],{"class":155},[149,3853,3854],{"class":162},"column",[149,3856,1053],{"class":155},[149,3858,3859,3862,3864,3867],{"class":151,"line":292},[149,3860,3861],{"class":162},"      align-items",[149,3863,166],{"class":155},[149,3865,3866],{"class":162},"center",[149,3868,1053],{"class":155},[149,3870,3871,3874,3876,3878],{"class":151,"line":305},[149,3872,3873],{"class":162},"      justify-content",[149,3875,166],{"class":155},[149,3877,3866],{"class":162},[149,3879,1053],{"class":155},[149,3881,3882,3885,3887,3890,3893],{"class":151,"line":314},[149,3883,3884],{"class":162},"      min-height",[149,3886,166],{"class":155},[149,3888,3889],{"class":162},"100",[149,3891,3892],{"class":996},"vh",[149,3894,1053],{"class":155},[149,3896,3897,3900,3902,3905],{"class":151,"line":320},[149,3898,3899],{"class":162},"      margin",[149,3901,166],{"class":155},[149,3903,3904],{"class":162},"0",[149,3906,1053],{"class":155},[149,3908,3909,3912,3914,3917],{"class":151,"line":333},[149,3910,3911],{"class":162},"      background",[149,3913,166],{"class":155},[149,3915,3916],{"class":162},"#f9fafb",[149,3918,1053],{"class":155},[149,3920,3921,3924,3926,3929],{"class":151,"line":346},[149,3922,3923],{"class":162},"      color",[149,3925,166],{"class":155},[149,3927,3928],{"class":162},"#111",[149,3930,1053],{"class":155},[149,3932,3933],{"class":151,"line":359},[149,3934,482],{"class":155},[149,3936,3937,3940,3942,3945,3947,3950,3953],{"class":151,"line":370},[149,3938,3939],{"class":777},"    h1",[149,3941,2950],{"class":155},[149,3943,3944],{"class":162},"font-size",[149,3946,166],{"class":155},[149,3948,3949],{"class":162},"1.5",[149,3951,3952],{"class":996},"rem",[149,3954,3955],{"class":155},"; }\n",[149,3957,3958,3961,3964,3967,3969,3972],{"class":151,"line":376},[149,3959,3960],{"class":777},"    p",[149,3962,3963],{"class":155},"  { ",[149,3965,3966],{"class":162},"color",[149,3968,166],{"class":155},[149,3970,3971],{"class":162},"#6b7280",[149,3973,3955],{"class":155},[149,3975,3976,3979,3981],{"class":151,"line":381},[149,3977,3978],{"class":155},"  \u003C/",[149,3980,3811],{"class":777},[149,3982,3723],{"class":155},[149,3984,3985,3988,3990],{"class":151,"line":393},[149,3986,3987],{"class":155},"\u003C/",[149,3989,3746],{"class":777},[149,3991,3723],{"class":155},[149,3993,3994,3996,3999],{"class":151,"line":405},[149,3995,774],{"class":155},[149,3997,3998],{"class":777},"body",[149,4000,3723],{"class":155},[149,4002,4003,4005,4008,4010,4012],{"class":151,"line":416},[149,4004,3753],{"class":155},[149,4006,4007],{"class":777},"h1",[149,4009,3800],{"class":155},[149,4011,4007],{"class":777},[149,4013,3723],{"class":155},[149,4015,4016,4018,4020,4023,4025],{"class":151,"line":425},[149,4017,3753],{"class":155},[149,4019,16],{"class":777},[149,4021,4022],{"class":155},">Check your internet connection and try again.\u003C/",[149,4024,16],{"class":777},[149,4026,3723],{"class":155},[149,4028,4029,4031,4034,4037,4039,4042,4045,4047,4050,4053,4056,4058],{"class":151,"line":430},[149,4030,3753],{"class":155},[149,4032,4033],{"class":777},"button",[149,4035,4036],{"class":781}," onclick",[149,4038,785],{"class":155},[149,4040,4041],{"class":169},"\"",[149,4043,4044],{"class":155},"location",[149,4046,856],{"class":169},[149,4048,4049],{"class":781},"reload",[149,4051,4052],{"class":169},"()\"",[149,4054,4055],{"class":155},">Try again\u003C/",[149,4057,4033],{"class":777},[149,4059,3723],{"class":155},[149,4061,4062,4064,4066],{"class":151,"line":435},[149,4063,3987],{"class":155},[149,4065,3998],{"class":777},[149,4067,3723],{"class":155},[149,4069,4070,4072,4074],{"class":151,"line":447},[149,4071,3987],{"class":155},[149,4073,767],{"class":777},[149,4075,3723],{"class":155},[41,4077],{},[11,4079,4081],{"id":4080},"pwa-installation-the-add-to-home-screen-flow","PWA Installation: The \"Add to Home Screen\" Flow",[16,4083,4084],{},"When the browser determines your app meets the PWA installability criteria (HTTPS + service worker + valid manifest with icons), it may show an install prompt automatically. But you have more control than that.",[135,4086,129,4088,4091],{"id":4087},"the-beforeinstallprompt-event",[117,4089,4090],{},"beforeinstallprompt"," Event",[16,4093,4094,4095,4097],{},"The browser fires ",[117,4096,4090],{}," when it is ready to show the install prompt. You can capture it and show your own UI at a better moment:",[140,4099,4101],{"className":975,"code":4100,"language":977,"meta":145,"style":145},"// main.js\n\nlet deferredPrompt = null;\nconst installButton = document.querySelector('#install-btn');\n\nwindow.addEventListener('beforeinstallprompt', (event) => {\n  // Prevent the browser's default mini-infobar\n  event.preventDefault();\n\n  // Save the event for later use\n  deferredPrompt = event;\n\n  // Show your custom install button\n  installButton.style.display = 'block';\n});\n\ninstallButton.addEventListener('click', async () => {\n  if (!deferredPrompt) return;\n\n  // Show the browser's install prompt\n  deferredPrompt.prompt();\n\n  // Wait for the user's choice\n  const { outcome } = await deferredPrompt.userChoice;\n\n  console.log(`User ${outcome === 'accepted' ? 'installed' : 'dismissed'} the app`);\n\n  // The prompt can only be used once\n  deferredPrompt = null;\n  installButton.style.display = 'none';\n});\n\n// Track successful installations\nwindow.addEventListener('appinstalled', () => {\n  console.log('App installed to home screen');\n  // Send an analytics event\n});\n",[117,4102,4103,4108,4112,4127,4149,4153,4174,4179,4188,4192,4197,4206,4210,4215,4227,4231,4235,4258,4273,4277,4282,4292,4296,4301,4319,4323,4357,4361,4366,4376,4387,4391,4395,4400,4417,4430,4435],{"__ignoreMap":145},[149,4104,4105],{"class":151,"line":152},[149,4106,4107],{"class":984},"// main.js\n",[149,4109,4110],{"class":151,"line":159},[149,4111,991],{"emptyLinePlaceholder":990},[149,4113,4114,4117,4120,4122,4125],{"class":151,"line":176},[149,4115,4116],{"class":996},"let",[149,4118,4119],{"class":155}," deferredPrompt ",[149,4121,785],{"class":996},[149,4123,4124],{"class":162}," null",[149,4126,1053],{"class":155},[149,4128,4129,4131,4134,4136,4139,4142,4144,4147],{"class":151,"line":189},[149,4130,1491],{"class":996},[149,4132,4133],{"class":162}," installButton",[149,4135,1081],{"class":996},[149,4137,4138],{"class":155}," document.",[149,4140,4141],{"class":781},"querySelector",[149,4143,1020],{"class":155},[149,4145,4146],{"class":169},"'#install-btn'",[149,4148,1045],{"class":155},[149,4150,4151],{"class":151,"line":202},[149,4152,991],{"emptyLinePlaceholder":990},[149,4154,4155,4157,4159,4161,4164,4166,4168,4170,4172],{"class":151,"line":215},[149,4156,1234],{"class":155},[149,4158,1123],{"class":781},[149,4160,1020],{"class":155},[149,4162,4163],{"class":169},"'beforeinstallprompt'",[149,4165,1304],{"class":155},[149,4167,1308],{"class":1307},[149,4169,1311],{"class":155},[149,4171,1134],{"class":996},[149,4173,1070],{"class":155},[149,4175,4176],{"class":151,"line":227},[149,4177,4178],{"class":984},"  // Prevent the browser's default mini-infobar\n",[149,4180,4181,4183,4186],{"class":151,"line":240},[149,4182,1334],{"class":155},[149,4184,4185],{"class":781},"preventDefault",[149,4187,2320],{"class":155},[149,4189,4190],{"class":151,"line":253},[149,4191,991],{"emptyLinePlaceholder":990},[149,4193,4194],{"class":151,"line":266},[149,4195,4196],{"class":984},"  // Save the event for later use\n",[149,4198,4199,4202,4204],{"class":151,"line":279},[149,4200,4201],{"class":155},"  deferredPrompt ",[149,4203,785],{"class":996},[149,4205,2960],{"class":155},[149,4207,4208],{"class":151,"line":292},[149,4209,991],{"emptyLinePlaceholder":990},[149,4211,4212],{"class":151,"line":305},[149,4213,4214],{"class":984},"  // Show your custom install button\n",[149,4216,4217,4220,4222,4225],{"class":151,"line":314},[149,4218,4219],{"class":155},"  installButton.style.display ",[149,4221,785],{"class":996},[149,4223,4224],{"class":169}," 'block'",[149,4226,1053],{"class":155},[149,4228,4229],{"class":151,"line":320},[149,4230,1424],{"class":155},[149,4232,4233],{"class":151,"line":333},[149,4234,991],{"emptyLinePlaceholder":990},[149,4236,4237,4240,4242,4244,4247,4249,4251,4254,4256],{"class":151,"line":346},[149,4238,4239],{"class":155},"installButton.",[149,4241,1123],{"class":781},[149,4243,1020],{"class":155},[149,4245,4246],{"class":169},"'click'",[149,4248,697],{"class":155},[149,4250,997],{"class":996},[149,4252,4253],{"class":155}," () ",[149,4255,1134],{"class":996},[149,4257,1070],{"class":155},[149,4259,4260,4262,4264,4266,4269,4271],{"class":151,"line":359},[149,4261,1011],{"class":996},[149,4263,1014],{"class":155},[149,4265,1017],{"class":996},[149,4267,4268],{"class":155},"deferredPrompt) ",[149,4270,1594],{"class":996},[149,4272,1053],{"class":155},[149,4274,4275],{"class":151,"line":370},[149,4276,991],{"emptyLinePlaceholder":990},[149,4278,4279],{"class":151,"line":376},[149,4280,4281],{"class":984},"  // Show the browser's install prompt\n",[149,4283,4284,4287,4290],{"class":151,"line":381},[149,4285,4286],{"class":155},"  deferredPrompt.",[149,4288,4289],{"class":781},"prompt",[149,4291,2320],{"class":155},[149,4293,4294],{"class":151,"line":393},[149,4295,991],{"emptyLinePlaceholder":990},[149,4297,4298],{"class":151,"line":405},[149,4299,4300],{"class":984},"  // Wait for the user's choice\n",[149,4302,4303,4305,4307,4310,4312,4314,4316],{"class":151,"line":416},[149,4304,1974],{"class":996},[149,4306,2950],{"class":155},[149,4308,4309],{"class":162},"outcome",[149,4311,2955],{"class":155},[149,4313,785],{"class":996},[149,4315,1084],{"class":996},[149,4317,4318],{"class":155}," deferredPrompt.userChoice;\n",[149,4320,4321],{"class":151,"line":425},[149,4322,991],{"emptyLinePlaceholder":990},[149,4324,4325,4328,4330,4332,4335,4337,4340,4343,4346,4349,4352,4355],{"class":151,"line":430},[149,4326,4327],{"class":155},"  console.",[149,4329,1037],{"class":781},[149,4331,1020],{"class":155},[149,4333,4334],{"class":169},"`User ${",[149,4336,4309],{"class":155},[149,4338,4339],{"class":996}," ===",[149,4341,4342],{"class":169}," 'accepted'",[149,4344,4345],{"class":996}," ?",[149,4347,4348],{"class":169}," 'installed'",[149,4350,4351],{"class":996}," :",[149,4353,4354],{"class":169}," 'dismissed'} the app`",[149,4356,1045],{"class":155},[149,4358,4359],{"class":151,"line":435},[149,4360,991],{"emptyLinePlaceholder":990},[149,4362,4363],{"class":151,"line":447},[149,4364,4365],{"class":984},"  // The prompt can only be used once\n",[149,4367,4368,4370,4372,4374],{"class":151,"line":459},[149,4369,4201],{"class":155},[149,4371,785],{"class":996},[149,4373,4124],{"class":162},[149,4375,1053],{"class":155},[149,4377,4378,4380,4382,4385],{"class":151,"line":470},[149,4379,4219],{"class":155},[149,4381,785],{"class":996},[149,4383,4384],{"class":169}," 'none'",[149,4386,1053],{"class":155},[149,4388,4389],{"class":151,"line":479},[149,4390,1424],{"class":155},[149,4392,4393],{"class":151,"line":485},[149,4394,991],{"emptyLinePlaceholder":990},[149,4396,4397],{"class":151,"line":491},[149,4398,4399],{"class":984},"// Track successful installations\n",[149,4401,4402,4404,4406,4408,4411,4413,4415],{"class":151,"line":499},[149,4403,1234],{"class":155},[149,4405,1123],{"class":781},[149,4407,1020],{"class":155},[149,4409,4410],{"class":169},"'appinstalled'",[149,4412,1131],{"class":155},[149,4414,1134],{"class":996},[149,4416,1070],{"class":155},[149,4418,4419,4421,4423,4425,4428],{"class":151,"line":504},[149,4420,4327],{"class":155},[149,4422,1037],{"class":781},[149,4424,1020],{"class":155},[149,4426,4427],{"class":169},"'App installed to home screen'",[149,4429,1045],{"class":155},[149,4431,4432],{"class":151,"line":516},[149,4433,4434],{"class":984},"  // Send an analytics event\n",[149,4436,4437],{"class":151,"line":528},[149,4438,1424],{"class":155},[16,4440,4441],{},"This pattern lets you present the install offer at a moment of high engagement, like after a user completes a key action, rather than blasting a prompt the moment they land on the page.",[135,4443,4445],{"id":4444},"detecting-if-the-app-is-already-installed","Detecting if the App is Already Installed",[16,4447,4448],{},"You can detect whether the user is running your app in standalone mode (installed) vs. in the browser:",[140,4450,4452],{"className":975,"code":4451,"language":977,"meta":145,"style":145},"const isStandalone = window.matchMedia('(display-mode: standalone)').matches\n  || window.navigator.standalone === true; // iOS Safari\n\nif (isStandalone) {\n  // Running as installed PWA\n  // Hide the \"install\" UI, show \"open in browser\" if needed\n}\n",[117,4453,4454,4477,4496,4500,4508,4513,4518],{"__ignoreMap":145},[149,4455,4456,4458,4461,4463,4466,4469,4471,4474],{"class":151,"line":152},[149,4457,1491],{"class":996},[149,4459,4460],{"class":162}," isStandalone",[149,4462,1081],{"class":996},[149,4464,4465],{"class":155}," window.",[149,4467,4468],{"class":781},"matchMedia",[149,4470,1020],{"class":155},[149,4472,4473],{"class":169},"'(display-mode: standalone)'",[149,4475,4476],{"class":155},").matches\n",[149,4478,4479,4482,4485,4487,4490,4493],{"class":151,"line":159},[149,4480,4481],{"class":996},"  ||",[149,4483,4484],{"class":155}," window.navigator.standalone ",[149,4486,1910],{"class":996},[149,4488,4489],{"class":162}," true",[149,4491,4492],{"class":155},"; ",[149,4494,4495],{"class":984},"// iOS Safari\n",[149,4497,4498],{"class":151,"line":176},[149,4499,991],{"emptyLinePlaceholder":990},[149,4501,4502,4505],{"class":151,"line":189},[149,4503,4504],{"class":996},"if",[149,4506,4507],{"class":155}," (isStandalone) {\n",[149,4509,4510],{"class":151,"line":202},[149,4511,4512],{"class":984},"  // Running as installed PWA\n",[149,4514,4515],{"class":151,"line":215},[149,4516,4517],{"class":984},"  // Hide the \"install\" UI, show \"open in browser\" if needed\n",[149,4519,4520],{"class":151,"line":227},[149,4521,755],{"class":155},[135,4523,4525],{"id":4524},"ios-safari-considerations","iOS Safari Considerations",[16,4527,4528,4529,4531],{},"iOS Safari does not fire ",[117,4530,4090],{},". Installation is entirely manual (\"Share > Add to Home Screen\"). You need to show your own instruction UI to guide iOS users. Detect iOS and show a modal:",[140,4533,4535],{"className":975,"code":4534,"language":977,"meta":145,"style":145},"const isIos = /iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase());\nconst isInStandaloneMode = window.navigator.standalone === true;\n\nif (isIos && !isInStandaloneMode) {\n  // Show \"tap Share, then Add to Home Screen\" instruction UI\n  showIosInstallInstructions();\n}\n",[117,4536,4537,4571,4588,4592,4607,4612,4619],{"__ignoreMap":145},[149,4538,4539,4541,4544,4546,4549,4551,4554,4556,4559,4561,4563,4566,4569],{"class":151,"line":152},[149,4540,1491],{"class":996},[149,4542,4543],{"class":162}," isIos",[149,4545,1081],{"class":996},[149,4547,4548],{"class":169}," /iphone",[149,4550,3100],{"class":996},[149,4552,4553],{"class":169},"ipad",[149,4555,3100],{"class":996},[149,4557,4558],{"class":169},"ipod/",[149,4560,856],{"class":155},[149,4562,3128],{"class":781},[149,4564,4565],{"class":155},"(navigator.userAgent.",[149,4567,4568],{"class":781},"toLowerCase",[149,4570,1645],{"class":155},[149,4572,4573,4575,4578,4580,4582,4584,4586],{"class":151,"line":159},[149,4574,1491],{"class":996},[149,4576,4577],{"class":162}," isInStandaloneMode",[149,4579,1081],{"class":996},[149,4581,4484],{"class":155},[149,4583,1910],{"class":996},[149,4585,4489],{"class":162},[149,4587,1053],{"class":155},[149,4589,4590],{"class":151,"line":176},[149,4591,991],{"emptyLinePlaceholder":990},[149,4593,4594,4596,4599,4602,4604],{"class":151,"line":189},[149,4595,4504],{"class":996},[149,4597,4598],{"class":155}," (isIos ",[149,4600,4601],{"class":996},"&&",[149,4603,2475],{"class":996},[149,4605,4606],{"class":155},"isInStandaloneMode) {\n",[149,4608,4609],{"class":151,"line":202},[149,4610,4611],{"class":984},"  // Show \"tap Share, then Add to Home Screen\" instruction UI\n",[149,4613,4614,4617],{"class":151,"line":215},[149,4615,4616],{"class":781},"  showIosInstallInstructions",[149,4618,2320],{"class":155},[149,4620,4621],{"class":151,"line":227},[149,4622,755],{"class":155},[41,4624],{},[11,4626,4628],{"id":4627},"push-notifications","Push Notifications",[16,4630,4631],{},"Push notifications let your server send messages to users even when the browser is closed. They require two things: the Push API (for receiving messages) and the Notifications API (for displaying them).",[135,4633,4635],{"id":4634},"the-permission-flow","The Permission Flow",[16,4637,4638],{},"You must ask the user for permission before sending notifications. Ask at a moment when the value is obvious, never on first page load:",[140,4640,4642],{"className":975,"code":4641,"language":977,"meta":145,"style":145},"// main.js\n\nasync function requestNotificationPermission() {\n  if (!('Notification' in window)) {\n    console.log('Notifications not supported');\n    return;\n  }\n\n  if (Notification.permission === 'granted') return 'granted';\n  if (Notification.permission === 'denied') return 'denied';\n\n  const permission = await Notification.requestPermission();\n  return permission;\n}\n\n// Only ask after a user action, like subscribing to a newsletter\ndocument.querySelector('#subscribe-btn').addEventListener('click', async () => {\n  const permission = await requestNotificationPermission();\n  if (permission === 'granted') {\n    await subscribeUserToPush();\n  }\n});\n",[117,4643,4644,4648,4652,4663,4681,4694,4700,4704,4708,4728,4747,4751,4770,4777,4781,4785,4790,4820,4834,4847,4857,4861],{"__ignoreMap":145},[149,4645,4646],{"class":151,"line":152},[149,4647,4107],{"class":984},[149,4649,4650],{"class":151,"line":159},[149,4651,991],{"emptyLinePlaceholder":990},[149,4653,4654,4656,4658,4661],{"class":151,"line":176},[149,4655,997],{"class":996},[149,4657,1000],{"class":996},[149,4659,4660],{"class":781}," requestNotificationPermission",[149,4662,1006],{"class":155},[149,4664,4665,4667,4669,4671,4673,4676,4678],{"class":151,"line":189},[149,4666,1011],{"class":996},[149,4668,1014],{"class":155},[149,4670,1017],{"class":996},[149,4672,1020],{"class":155},[149,4674,4675],{"class":169},"'Notification'",[149,4677,1026],{"class":996},[149,4679,4680],{"class":155}," window)) {\n",[149,4682,4683,4685,4687,4689,4692],{"class":151,"line":202},[149,4684,1034],{"class":155},[149,4686,1037],{"class":781},[149,4688,1020],{"class":155},[149,4690,4691],{"class":169},"'Notifications not supported'",[149,4693,1045],{"class":155},[149,4695,4696,4698],{"class":151,"line":215},[149,4697,1050],{"class":996},[149,4699,1053],{"class":155},[149,4701,4702],{"class":151,"line":227},[149,4703,1058],{"class":155},[149,4705,4706],{"class":151,"line":240},[149,4707,991],{"emptyLinePlaceholder":990},[149,4709,4710,4712,4715,4717,4720,4722,4724,4726],{"class":151,"line":253},[149,4711,1011],{"class":996},[149,4713,4714],{"class":155}," (Notification.permission ",[149,4716,1910],{"class":996},[149,4718,4719],{"class":169}," 'granted'",[149,4721,1311],{"class":155},[149,4723,1594],{"class":996},[149,4725,4719],{"class":169},[149,4727,1053],{"class":155},[149,4729,4730,4732,4734,4736,4739,4741,4743,4745],{"class":151,"line":266},[149,4731,1011],{"class":996},[149,4733,4714],{"class":155},[149,4735,1910],{"class":996},[149,4737,4738],{"class":169}," 'denied'",[149,4740,1311],{"class":155},[149,4742,1594],{"class":996},[149,4744,4738],{"class":169},[149,4746,1053],{"class":155},[149,4748,4749],{"class":151,"line":279},[149,4750,991],{"emptyLinePlaceholder":990},[149,4752,4753,4755,4758,4760,4762,4765,4768],{"class":151,"line":292},[149,4754,1974],{"class":996},[149,4756,4757],{"class":162}," permission",[149,4759,1081],{"class":996},[149,4761,1084],{"class":996},[149,4763,4764],{"class":155}," Notification.",[149,4766,4767],{"class":781},"requestPermission",[149,4769,2320],{"class":155},[149,4771,4772,4774],{"class":151,"line":305},[149,4773,2090],{"class":996},[149,4775,4776],{"class":155}," permission;\n",[149,4778,4779],{"class":151,"line":314},[149,4780,755],{"class":155},[149,4782,4783],{"class":151,"line":320},[149,4784,991],{"emptyLinePlaceholder":990},[149,4786,4787],{"class":151,"line":333},[149,4788,4789],{"class":984},"// Only ask after a user action, like subscribing to a newsletter\n",[149,4791,4792,4795,4797,4799,4802,4804,4806,4808,4810,4812,4814,4816,4818],{"class":151,"line":346},[149,4793,4794],{"class":155},"document.",[149,4796,4141],{"class":781},[149,4798,1020],{"class":155},[149,4800,4801],{"class":169},"'#subscribe-btn'",[149,4803,1548],{"class":155},[149,4805,1123],{"class":781},[149,4807,1020],{"class":155},[149,4809,4246],{"class":169},[149,4811,697],{"class":155},[149,4813,997],{"class":996},[149,4815,4253],{"class":155},[149,4817,1134],{"class":996},[149,4819,1070],{"class":155},[149,4821,4822,4824,4826,4828,4830,4832],{"class":151,"line":359},[149,4823,1974],{"class":996},[149,4825,4757],{"class":162},[149,4827,1081],{"class":996},[149,4829,1084],{"class":996},[149,4831,4660],{"class":781},[149,4833,2320],{"class":155},[149,4835,4836,4838,4841,4843,4845],{"class":151,"line":370},[149,4837,1011],{"class":996},[149,4839,4840],{"class":155}," (permission ",[149,4842,1910],{"class":996},[149,4844,4719],{"class":169},[149,4846,1730],{"class":155},[149,4848,4849,4852,4855],{"class":151,"line":376},[149,4850,4851],{"class":996},"    await",[149,4853,4854],{"class":781}," subscribeUserToPush",[149,4856,2320],{"class":155},[149,4858,4859],{"class":151,"line":381},[149,4860,1058],{"class":155},[149,4862,4863],{"class":151,"line":393},[149,4864,1424],{"class":155},[135,4866,4868],{"id":4867},"subscribing-to-push","Subscribing to Push",[16,4870,4871],{},"The subscription process uses the Push API and requires a VAPID (Voluntary Application Server Identification) key pair. VAPID keys identify your server to the push service:",[140,4873,4875],{"className":975,"code":4874,"language":977,"meta":145,"style":145},"// main.js\n\nasync function subscribeUserToPush() {\n  const registration = await navigator.serviceWorker.ready;\n\n  const subscription = await registration.pushManager.subscribe({\n    userVisibleOnly: true, // required to be true\n    applicationServerKey: urlBase64ToUint8Array(YOUR_PUBLIC_VAPID_KEY),\n  });\n\n  // Send the subscription object to your backend\n  await fetch('/api/push-subscriptions', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(subscription),\n  });\n\n  console.log('User subscribed to push notifications');\n}\n\n// Helper to convert VAPID key\nfunction urlBase64ToUint8Array(base64String) {\n  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);\n  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');\n  const rawData = atob(base64);\n  return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));\n}\n",[117,4876,4877,4881,4885,4895,4908,4912,4932,4945,4961,4965,4969,4974,4988,4998,5011,5025,5029,5033,5046,5050,5054,5059,5074,5121,5173,5188,5231],{"__ignoreMap":145},[149,4878,4879],{"class":151,"line":152},[149,4880,4107],{"class":984},[149,4882,4883],{"class":151,"line":159},[149,4884,991],{"emptyLinePlaceholder":990},[149,4886,4887,4889,4891,4893],{"class":151,"line":176},[149,4888,997],{"class":996},[149,4890,1000],{"class":996},[149,4892,4854],{"class":781},[149,4894,1006],{"class":155},[149,4896,4897,4899,4901,4903,4905],{"class":151,"line":189},[149,4898,1974],{"class":996},[149,4900,1078],{"class":162},[149,4902,1081],{"class":996},[149,4904,1084],{"class":996},[149,4906,4907],{"class":155}," navigator.serviceWorker.ready;\n",[149,4909,4910],{"class":151,"line":202},[149,4911,991],{"emptyLinePlaceholder":990},[149,4913,4914,4916,4919,4921,4923,4926,4929],{"class":151,"line":215},[149,4915,1974],{"class":996},[149,4917,4918],{"class":162}," subscription",[149,4920,1081],{"class":996},[149,4922,1084],{"class":996},[149,4924,4925],{"class":155}," registration.pushManager.",[149,4927,4928],{"class":781},"subscribe",[149,4930,4931],{"class":155},"({\n",[149,4933,4934,4937,4940,4942],{"class":151,"line":227},[149,4935,4936],{"class":155},"    userVisibleOnly: ",[149,4938,4939],{"class":162},"true",[149,4941,697],{"class":155},[149,4943,4944],{"class":984},"// required to be true\n",[149,4946,4947,4950,4953,4955,4958],{"class":151,"line":240},[149,4948,4949],{"class":155},"    applicationServerKey: ",[149,4951,4952],{"class":781},"urlBase64ToUint8Array",[149,4954,1020],{"class":155},[149,4956,4957],{"class":162},"YOUR_PUBLIC_VAPID_KEY",[149,4959,4960],{"class":155},"),\n",[149,4962,4963],{"class":151,"line":253},[149,4964,2076],{"class":155},[149,4966,4967],{"class":151,"line":266},[149,4968,991],{"emptyLinePlaceholder":990},[149,4970,4971],{"class":151,"line":279},[149,4972,4973],{"class":984},"  // Send the subscription object to your backend\n",[149,4975,4976,4979,4981,4983,4986],{"class":151,"line":292},[149,4977,4978],{"class":996},"  await",[149,4980,1403],{"class":781},[149,4982,1020],{"class":155},[149,4984,4985],{"class":169},"'/api/push-subscriptions'",[149,4987,1098],{"class":155},[149,4989,4990,4993,4996],{"class":151,"line":305},[149,4991,4992],{"class":155},"    method: ",[149,4994,4995],{"class":169},"'POST'",[149,4997,173],{"class":155},[149,4999,5000,5003,5005,5007,5009],{"class":151,"line":314},[149,5001,5002],{"class":155},"    headers: { ",[149,5004,3518],{"class":169},[149,5006,166],{"class":155},[149,5008,3523],{"class":169},[149,5010,3526],{"class":155},[149,5012,5013,5016,5018,5020,5022],{"class":151,"line":320},[149,5014,5015],{"class":155},"    body: ",[149,5017,3495],{"class":162},[149,5019,856],{"class":155},[149,5021,3500],{"class":781},[149,5023,5024],{"class":155},"(subscription),\n",[149,5026,5027],{"class":151,"line":333},[149,5028,2076],{"class":155},[149,5030,5031],{"class":151,"line":346},[149,5032,991],{"emptyLinePlaceholder":990},[149,5034,5035,5037,5039,5041,5044],{"class":151,"line":359},[149,5036,4327],{"class":155},[149,5038,1037],{"class":781},[149,5040,1020],{"class":155},[149,5042,5043],{"class":169},"'User subscribed to push notifications'",[149,5045,1045],{"class":155},[149,5047,5048],{"class":151,"line":370},[149,5049,755],{"class":155},[149,5051,5052],{"class":151,"line":376},[149,5053,991],{"emptyLinePlaceholder":990},[149,5055,5056],{"class":151,"line":381},[149,5057,5058],{"class":984},"// Helper to convert VAPID key\n",[149,5060,5061,5064,5067,5069,5072],{"class":151,"line":393},[149,5062,5063],{"class":996},"function",[149,5065,5066],{"class":781}," urlBase64ToUint8Array",[149,5068,1020],{"class":155},[149,5070,5071],{"class":1307},"base64String",[149,5073,1730],{"class":155},[149,5075,5076,5078,5081,5083,5086,5088,5091,5093,5096,5099,5102,5105,5108,5111,5114,5117,5119],{"class":151,"line":405},[149,5077,1974],{"class":996},[149,5079,5080],{"class":162}," padding",[149,5082,1081],{"class":996},[149,5084,5085],{"class":169}," '='",[149,5087,856],{"class":155},[149,5089,5090],{"class":781},"repeat",[149,5092,1362],{"class":155},[149,5094,5095],{"class":162},"4",[149,5097,5098],{"class":996}," -",[149,5100,5101],{"class":155}," (base64String.",[149,5103,5104],{"class":162},"length",[149,5106,5107],{"class":996}," %",[149,5109,5110],{"class":162}," 4",[149,5112,5113],{"class":155},")) ",[149,5115,5116],{"class":996},"%",[149,5118,5110],{"class":162},[149,5120,1045],{"class":155},[149,5122,5123,5125,5128,5130,5133,5136,5139,5142,5144,5147,5150,5152,5155,5157,5159,5161,5164,5166,5168,5171],{"class":151,"line":416},[149,5124,1974],{"class":996},[149,5126,5127],{"class":162}," base64",[149,5129,1081],{"class":996},[149,5131,5132],{"class":155}," (base64String ",[149,5134,5135],{"class":996},"+",[149,5137,5138],{"class":155}," padding).",[149,5140,5141],{"class":781},"replace",[149,5143,1020],{"class":155},[149,5145,5146],{"class":169},"/-/",[149,5148,5149],{"class":996},"g",[149,5151,697],{"class":155},[149,5153,5154],{"class":169},"'+'",[149,5156,1548],{"class":155},[149,5158,5141],{"class":781},[149,5160,1020],{"class":155},[149,5162,5163],{"class":169},"/_/",[149,5165,5149],{"class":996},[149,5167,697],{"class":155},[149,5169,5170],{"class":169},"'/'",[149,5172,1045],{"class":155},[149,5174,5175,5177,5180,5182,5185],{"class":151,"line":425},[149,5176,1974],{"class":996},[149,5178,5179],{"class":162}," rawData",[149,5181,1081],{"class":996},[149,5183,5184],{"class":781}," atob",[149,5186,5187],{"class":155},"(base64);\n",[149,5189,5190,5192,5195,5198,5201,5204,5207,5209,5211,5214,5216,5218,5221,5224,5226,5228],{"class":151,"line":430},[149,5191,2090],{"class":996},[149,5193,5194],{"class":155}," Uint8Array.",[149,5196,5197],{"class":781},"from",[149,5199,5200],{"class":155},"([",[149,5202,5203],{"class":996},"...",[149,5205,5206],{"class":155},"rawData].",[149,5208,2493],{"class":781},[149,5210,1362],{"class":155},[149,5212,5213],{"class":1307},"char",[149,5215,1311],{"class":155},[149,5217,1134],{"class":996},[149,5219,5220],{"class":155}," char.",[149,5222,5223],{"class":781},"charCodeAt",[149,5225,1020],{"class":155},[149,5227,3904],{"class":162},[149,5229,5230],{"class":155},")));\n",[149,5232,5233],{"class":151,"line":435},[149,5234,755],{"class":155},[135,5236,5238],{"id":5237},"handling-push-in-the-service-worker","Handling Push in the Service Worker",[16,5240,5241,5242,5245],{},"When your server sends a push message, the service worker receives it via the ",[117,5243,5244],{},"push"," event:",[140,5247,5249],{"className":975,"code":5248,"language":977,"meta":145,"style":145},"// sw.js\n\nself.addEventListener('push', (event) => {\n  if (!event.data) return;\n\n  const data = event.data.json();\n\n  const options = {\n    body: data.body,\n    icon: '/icons/icon-192x192.png',\n    badge: '/icons/badge-72x72.png',\n    image: data.image,\n    data: { url: data.url },\n    actions: [\n      { action: 'open', title: 'Open' },\n      { action: 'dismiss', title: 'Dismiss' },\n    ],\n    tag: data.tag,          // replaces existing notification with same tag\n    renotify: true,         // vibrate even if same tag\n    requireInteraction: false,\n  };\n\n  event.waitUntil(\n    self.registration.showNotification(data.title, options)\n  );\n});\n\nself.addEventListener('notificationclick', (event) => {\n  event.notification.close();\n\n  if (event.action === 'dismiss') return;\n\n  const url = event.notification.data.url ?? '/';\n  event.waitUntil(\n    clients.matchAll({ type: 'window' }).then((windowClients) => {\n      // If a window is already open, focus it\n      for (const client of windowClients) {\n        if (client.url === url && 'focus' in client) {\n          return client.focus();\n        }\n      }\n      // Otherwise open a new window\n      if (clients.openWindow) return clients.openWindow(url);\n    })\n  );\n});\n",[117,5250,5251,5255,5259,5280,5295,5299,5315,5319,5330,5335,5345,5355,5360,5365,5370,5386,5400,5405,5413,5426,5436,5441,5445,5453,5464,5468,5472,5476,5497,5507,5511,5529,5533,5551,5559,5589,5594,5612,5635,5648,5653,5657,5662,5680,5684,5688],{"__ignoreMap":145},[149,5252,5253],{"class":151,"line":152},[149,5254,1285],{"class":984},[149,5256,5257],{"class":151,"line":159},[149,5258,991],{"emptyLinePlaceholder":990},[149,5260,5261,5263,5265,5267,5270,5272,5274,5276,5278],{"class":151,"line":176},[149,5262,1294],{"class":155},[149,5264,1123],{"class":781},[149,5266,1020],{"class":155},[149,5268,5269],{"class":169},"'push'",[149,5271,1304],{"class":155},[149,5273,1308],{"class":1307},[149,5275,1311],{"class":155},[149,5277,1134],{"class":996},[149,5279,1070],{"class":155},[149,5281,5282,5284,5286,5288,5291,5293],{"class":151,"line":189},[149,5283,1011],{"class":996},[149,5285,1014],{"class":155},[149,5287,1017],{"class":996},[149,5289,5290],{"class":155},"event.data) ",[149,5292,1594],{"class":996},[149,5294,1053],{"class":155},[149,5296,5297],{"class":151,"line":202},[149,5298,991],{"emptyLinePlaceholder":990},[149,5300,5301,5303,5306,5308,5311,5313],{"class":151,"line":215},[149,5302,1974],{"class":996},[149,5304,5305],{"class":162}," data",[149,5307,1081],{"class":996},[149,5309,5310],{"class":155}," event.data.",[149,5312,144],{"class":781},[149,5314,2320],{"class":155},[149,5316,5317],{"class":151,"line":227},[149,5318,991],{"emptyLinePlaceholder":990},[149,5320,5321,5323,5326,5328],{"class":151,"line":240},[149,5322,1974],{"class":996},[149,5324,5325],{"class":162}," options",[149,5327,1081],{"class":996},[149,5329,1070],{"class":155},[149,5331,5332],{"class":151,"line":253},[149,5333,5334],{"class":155},"    body: data.body,\n",[149,5336,5337,5340,5343],{"class":151,"line":266},[149,5338,5339],{"class":155},"    icon: ",[149,5341,5342],{"class":169},"'/icons/icon-192x192.png'",[149,5344,173],{"class":155},[149,5346,5347,5350,5353],{"class":151,"line":279},[149,5348,5349],{"class":155},"    badge: ",[149,5351,5352],{"class":169},"'/icons/badge-72x72.png'",[149,5354,173],{"class":155},[149,5356,5357],{"class":151,"line":292},[149,5358,5359],{"class":155},"    image: data.image,\n",[149,5361,5362],{"class":151,"line":305},[149,5363,5364],{"class":155},"    data: { url: data.url },\n",[149,5366,5367],{"class":151,"line":314},[149,5368,5369],{"class":155},"    actions: [\n",[149,5371,5372,5375,5378,5381,5384],{"class":151,"line":320},[149,5373,5374],{"class":155},"      { action: ",[149,5376,5377],{"class":169},"'open'",[149,5379,5380],{"class":155},", title: ",[149,5382,5383],{"class":169},"'Open'",[149,5385,3526],{"class":155},[149,5387,5388,5390,5393,5395,5398],{"class":151,"line":333},[149,5389,5374],{"class":155},[149,5391,5392],{"class":169},"'dismiss'",[149,5394,5380],{"class":155},[149,5396,5397],{"class":169},"'Dismiss'",[149,5399,3526],{"class":155},[149,5401,5402],{"class":151,"line":346},[149,5403,5404],{"class":155},"    ],\n",[149,5406,5407,5410],{"class":151,"line":359},[149,5408,5409],{"class":155},"    tag: data.tag,          ",[149,5411,5412],{"class":984},"// replaces existing notification with same tag\n",[149,5414,5415,5418,5420,5423],{"class":151,"line":370},[149,5416,5417],{"class":155},"    renotify: ",[149,5419,4939],{"class":162},[149,5421,5422],{"class":155},",         ",[149,5424,5425],{"class":984},"// vibrate even if same tag\n",[149,5427,5428,5431,5434],{"class":151,"line":376},[149,5429,5430],{"class":155},"    requireInteraction: ",[149,5432,5433],{"class":162},"false",[149,5435,173],{"class":155},[149,5437,5438],{"class":151,"line":381},[149,5439,5440],{"class":155},"  };\n",[149,5442,5443],{"class":151,"line":393},[149,5444,991],{"emptyLinePlaceholder":990},[149,5446,5447,5449,5451],{"class":151,"line":405},[149,5448,1334],{"class":155},[149,5450,2236],{"class":781},[149,5452,1340],{"class":155},[149,5454,5455,5458,5461],{"class":151,"line":416},[149,5456,5457],{"class":155},"    self.registration.",[149,5459,5460],{"class":781},"showNotification",[149,5462,5463],{"class":155},"(data.title, options)\n",[149,5465,5466],{"class":151,"line":425},[149,5467,1419],{"class":155},[149,5469,5470],{"class":151,"line":430},[149,5471,1424],{"class":155},[149,5473,5474],{"class":151,"line":435},[149,5475,991],{"emptyLinePlaceholder":990},[149,5477,5478,5480,5482,5484,5487,5489,5491,5493,5495],{"class":151,"line":447},[149,5479,1294],{"class":155},[149,5481,1123],{"class":781},[149,5483,1020],{"class":155},[149,5485,5486],{"class":169},"'notificationclick'",[149,5488,1304],{"class":155},[149,5490,1308],{"class":1307},[149,5492,1311],{"class":155},[149,5494,1134],{"class":996},[149,5496,1070],{"class":155},[149,5498,5499,5502,5505],{"class":151,"line":459},[149,5500,5501],{"class":155},"  event.notification.",[149,5503,5504],{"class":781},"close",[149,5506,2320],{"class":155},[149,5508,5509],{"class":151,"line":470},[149,5510,991],{"emptyLinePlaceholder":990},[149,5512,5513,5515,5518,5520,5523,5525,5527],{"class":151,"line":479},[149,5514,1011],{"class":996},[149,5516,5517],{"class":155}," (event.action ",[149,5519,1910],{"class":996},[149,5521,5522],{"class":169}," 'dismiss'",[149,5524,1311],{"class":155},[149,5526,1594],{"class":996},[149,5528,1053],{"class":155},[149,5530,5531],{"class":151,"line":485},[149,5532,991],{"emptyLinePlaceholder":990},[149,5534,5535,5537,5539,5541,5544,5546,5549],{"class":151,"line":491},[149,5536,1974],{"class":996},[149,5538,2967],{"class":162},[149,5540,1081],{"class":996},[149,5542,5543],{"class":155}," event.notification.data.url ",[149,5545,2096],{"class":996},[149,5547,5548],{"class":169}," '/'",[149,5550,1053],{"class":155},[149,5552,5553,5555,5557],{"class":151,"line":499},[149,5554,1334],{"class":155},[149,5556,2236],{"class":781},[149,5558,1340],{"class":155},[149,5560,5561,5564,5567,5570,5573,5576,5578,5580,5583,5585,5587],{"class":151,"line":504},[149,5562,5563],{"class":155},"    clients.",[149,5565,5566],{"class":781},"matchAll",[149,5568,5569],{"class":155},"({ type: ",[149,5571,5572],{"class":169},"'window'",[149,5574,5575],{"class":155}," }).",[149,5577,1359],{"class":781},[149,5579,1362],{"class":155},[149,5581,5582],{"class":1307},"windowClients",[149,5584,1311],{"class":155},[149,5586,1134],{"class":996},[149,5588,1070],{"class":155},[149,5590,5591],{"class":151,"line":516},[149,5592,5593],{"class":984},"      // If a window is already open, focus it\n",[149,5595,5596,5599,5601,5603,5606,5609],{"class":151,"line":528},[149,5597,5598],{"class":996},"      for",[149,5600,1014],{"class":155},[149,5602,1491],{"class":996},[149,5604,5605],{"class":162}," client",[149,5607,5608],{"class":996}," of",[149,5610,5611],{"class":155}," windowClients) {\n",[149,5613,5614,5617,5620,5622,5625,5627,5630,5632],{"class":151,"line":539},[149,5615,5616],{"class":996},"        if",[149,5618,5619],{"class":155}," (client.url ",[149,5621,1910],{"class":996},[149,5623,5624],{"class":155}," url ",[149,5626,4601],{"class":996},[149,5628,5629],{"class":169}," 'focus'",[149,5631,1026],{"class":996},[149,5633,5634],{"class":155}," client) {\n",[149,5636,5637,5640,5643,5646],{"class":151,"line":550},[149,5638,5639],{"class":996},"          return",[149,5641,5642],{"class":155}," client.",[149,5644,5645],{"class":781},"focus",[149,5647,2320],{"class":155},[149,5649,5650],{"class":151,"line":555},[149,5651,5652],{"class":155},"        }\n",[149,5654,5655],{"class":151,"line":560},[149,5656,1395],{"class":155},[149,5658,5659],{"class":151,"line":572},[149,5660,5661],{"class":984},"      // Otherwise open a new window\n",[149,5663,5664,5666,5669,5671,5674,5677],{"class":151,"line":584},[149,5665,1376],{"class":996},[149,5667,5668],{"class":155}," (clients.openWindow) ",[149,5670,1594],{"class":996},[149,5672,5673],{"class":155}," clients.",[149,5675,5676],{"class":781},"openWindow",[149,5678,5679],{"class":155},"(url);\n",[149,5681,5682],{"class":151,"line":595},[149,5683,1414],{"class":155},[149,5685,5686],{"class":151,"line":605},[149,5687,1419],{"class":155},[149,5689,5690],{"class":151,"line":610},[149,5691,1424],{"class":155},[135,5693,5695],{"id":5694},"sending-push-from-your-server","Sending Push from Your Server",[16,5697,5698,5699,5702],{},"On the server side, you use a library like ",[117,5700,5701],{},"web-push"," (Node.js) to send the push message:",[140,5704,5706],{"className":975,"code":5705,"language":977,"meta":145,"style":145},"// server.js (Node.js)\nimport webpush from 'web-push';\n\nwebpush.setVapidDetails(\n  'mailto:hello@yourapp.com',\n  process.env.VAPID_PUBLIC_KEY,\n  process.env.VAPID_PRIVATE_KEY\n);\n\nasync function sendPushNotification(subscription, payload) {\n  try {\n    await webpush.sendNotification(\n      subscription,\n      JSON.stringify({\n        title: 'New message',\n        body: 'You have a new message from Alice',\n        url: '/messages/123',\n        tag: 'message-123',\n      })\n    );\n  } catch (error) {\n    if (error.statusCode === 410) {\n      // Subscription expired: remove from database\n      await removeSubscription(subscription.endpoint);\n    }\n  }\n}\n",[117,5707,5708,5713,5728,5732,5742,5749,5759,5766,5770,5774,5795,5801,5813,5818,5829,5839,5849,5859,5869,5874,5879,5887,5901,5906,5917,5921,5925],{"__ignoreMap":145},[149,5709,5710],{"class":151,"line":152},[149,5711,5712],{"class":984},"// server.js (Node.js)\n",[149,5714,5715,5718,5721,5723,5726],{"class":151,"line":159},[149,5716,5717],{"class":996},"import",[149,5719,5720],{"class":155}," webpush ",[149,5722,5197],{"class":996},[149,5724,5725],{"class":169}," 'web-push'",[149,5727,1053],{"class":155},[149,5729,5730],{"class":151,"line":176},[149,5731,991],{"emptyLinePlaceholder":990},[149,5733,5734,5737,5740],{"class":151,"line":189},[149,5735,5736],{"class":155},"webpush.",[149,5738,5739],{"class":781},"setVapidDetails",[149,5741,1340],{"class":155},[149,5743,5744,5747],{"class":151,"line":202},[149,5745,5746],{"class":169},"  'mailto:hello@yourapp.com'",[149,5748,173],{"class":155},[149,5750,5751,5754,5757],{"class":151,"line":215},[149,5752,5753],{"class":155},"  process.env.",[149,5755,5756],{"class":162},"VAPID_PUBLIC_KEY",[149,5758,173],{"class":155},[149,5760,5761,5763],{"class":151,"line":227},[149,5762,5753],{"class":155},[149,5764,5765],{"class":162},"VAPID_PRIVATE_KEY\n",[149,5767,5768],{"class":151,"line":240},[149,5769,1045],{"class":155},[149,5771,5772],{"class":151,"line":253},[149,5773,991],{"emptyLinePlaceholder":990},[149,5775,5776,5778,5780,5783,5785,5788,5790,5793],{"class":151,"line":266},[149,5777,997],{"class":996},[149,5779,1000],{"class":996},[149,5781,5782],{"class":781}," sendPushNotification",[149,5784,1020],{"class":155},[149,5786,5787],{"class":1307},"subscription",[149,5789,697],{"class":155},[149,5791,5792],{"class":1307},"payload",[149,5794,1730],{"class":155},[149,5796,5797,5799],{"class":151,"line":279},[149,5798,1067],{"class":996},[149,5800,1070],{"class":155},[149,5802,5803,5805,5808,5811],{"class":151,"line":292},[149,5804,4851],{"class":996},[149,5806,5807],{"class":155}," webpush.",[149,5809,5810],{"class":781},"sendNotification",[149,5812,1340],{"class":155},[149,5814,5815],{"class":151,"line":305},[149,5816,5817],{"class":155},"      subscription,\n",[149,5819,5820,5823,5825,5827],{"class":151,"line":314},[149,5821,5822],{"class":162},"      JSON",[149,5824,856],{"class":155},[149,5826,3500],{"class":781},[149,5828,4931],{"class":155},[149,5830,5831,5834,5837],{"class":151,"line":320},[149,5832,5833],{"class":155},"        title: ",[149,5835,5836],{"class":169},"'New message'",[149,5838,173],{"class":155},[149,5840,5841,5844,5847],{"class":151,"line":333},[149,5842,5843],{"class":155},"        body: ",[149,5845,5846],{"class":169},"'You have a new message from Alice'",[149,5848,173],{"class":155},[149,5850,5851,5854,5857],{"class":151,"line":346},[149,5852,5853],{"class":155},"        url: ",[149,5855,5856],{"class":169},"'/messages/123'",[149,5858,173],{"class":155},[149,5860,5861,5864,5867],{"class":151,"line":359},[149,5862,5863],{"class":155},"        tag: ",[149,5865,5866],{"class":169},"'message-123'",[149,5868,173],{"class":155},[149,5870,5871],{"class":151,"line":370},[149,5872,5873],{"class":155},"      })\n",[149,5875,5876],{"class":151,"line":376},[149,5877,5878],{"class":155},"    );\n",[149,5880,5881,5883,5885],{"class":151,"line":381},[149,5882,1191],{"class":155},[149,5884,1194],{"class":996},[149,5886,1197],{"class":155},[149,5888,5889,5891,5894,5896,5899],{"class":151,"line":393},[149,5890,1756],{"class":996},[149,5892,5893],{"class":155}," (error.statusCode ",[149,5895,1910],{"class":996},[149,5897,5898],{"class":162}," 410",[149,5900,1730],{"class":155},[149,5902,5903],{"class":151,"line":405},[149,5904,5905],{"class":984},"      // Subscription expired: remove from database\n",[149,5907,5908,5911,5914],{"class":151,"line":416},[149,5909,5910],{"class":996},"      await",[149,5912,5913],{"class":781}," removeSubscription",[149,5915,5916],{"class":155},"(subscription.endpoint);\n",[149,5918,5919],{"class":151,"line":425},[149,5920,482],{"class":155},[149,5922,5923],{"class":151,"line":430},[149,5924,1058],{"class":155},[149,5926,5927],{"class":151,"line":435},[149,5928,755],{"class":155},[16,5930,5931],{},"The push message flows like this:",[16,5933,5934],{},[33,5935],{"alt":5936,"src":5937},"Push notification delivery flow diagram","/blog-post-images/progressive-web-apps-deep-dive/push-notification-delivery-pipeline.png",[16,5939,5940],{},"Your server never communicates directly with the device. It sends to the push service, which queues and delivers the message. This is why push works even when the user is not actively browsing.",[41,5942],{},[11,5944,5946],{"id":5945},"background-sync","Background Sync",[16,5948,5949],{},"Background Sync solves a specific and frustrating problem: the user submits a form or sends a message, but their connection drops at that exact moment. Without background sync, the request fails silently and the user loses their work.",[16,5951,5952],{},"With background sync, the service worker queues the request and retries it automatically when connectivity is restored, even if the user has closed your app.",[135,5954,5956],{"id":5955},"registering-a-sync","Registering a Sync",[140,5958,5960],{"className":975,"code":5959,"language":977,"meta":145,"style":145},"// main.js\n\nasync function sendMessageWithBackgroundSync(message) {\n  // Save the message to IndexedDB first\n  await saveToIndexedDB('outbox', message);\n\n  // Register a sync event\n  const registration = await navigator.serviceWorker.ready;\n  await registration.sync.register('sync-outbox');\n\n  console.log('Message queued for background sync');\n}\n",[117,5961,5962,5966,5970,5986,5991,6006,6010,6015,6027,6043,6047,6060],{"__ignoreMap":145},[149,5963,5964],{"class":151,"line":152},[149,5965,4107],{"class":984},[149,5967,5968],{"class":151,"line":159},[149,5969,991],{"emptyLinePlaceholder":990},[149,5971,5972,5974,5976,5979,5981,5984],{"class":151,"line":176},[149,5973,997],{"class":996},[149,5975,1000],{"class":996},[149,5977,5978],{"class":781}," sendMessageWithBackgroundSync",[149,5980,1020],{"class":155},[149,5982,5983],{"class":1307},"message",[149,5985,1730],{"class":155},[149,5987,5988],{"class":151,"line":189},[149,5989,5990],{"class":984},"  // Save the message to IndexedDB first\n",[149,5992,5993,5995,5998,6000,6003],{"class":151,"line":202},[149,5994,4978],{"class":996},[149,5996,5997],{"class":781}," saveToIndexedDB",[149,5999,1020],{"class":155},[149,6001,6002],{"class":169},"'outbox'",[149,6004,6005],{"class":155},", message);\n",[149,6007,6008],{"class":151,"line":215},[149,6009,991],{"emptyLinePlaceholder":990},[149,6011,6012],{"class":151,"line":227},[149,6013,6014],{"class":984},"  // Register a sync event\n",[149,6016,6017,6019,6021,6023,6025],{"class":151,"line":240},[149,6018,1974],{"class":996},[149,6020,1078],{"class":162},[149,6022,1081],{"class":996},[149,6024,1084],{"class":996},[149,6026,4907],{"class":155},[149,6028,6029,6031,6034,6036,6038,6041],{"class":151,"line":253},[149,6030,4978],{"class":996},[149,6032,6033],{"class":155}," registration.sync.",[149,6035,1090],{"class":781},[149,6037,1020],{"class":155},[149,6039,6040],{"class":169},"'sync-outbox'",[149,6042,1045],{"class":155},[149,6044,6045],{"class":151,"line":266},[149,6046,991],{"emptyLinePlaceholder":990},[149,6048,6049,6051,6053,6055,6058],{"class":151,"line":279},[149,6050,4327],{"class":155},[149,6052,1037],{"class":781},[149,6054,1020],{"class":155},[149,6056,6057],{"class":169},"'Message queued for background sync'",[149,6059,1045],{"class":155},[149,6061,6062],{"class":151,"line":292},[149,6063,755],{"class":155},[135,6065,6067],{"id":6066},"handling-sync-in-the-service-worker","Handling Sync in the Service Worker",[140,6069,6071],{"className":975,"code":6070,"language":977,"meta":145,"style":145},"// sw.js\n\nself.addEventListener('sync', (event) => {\n  if (event.tag === 'sync-outbox') {\n    event.waitUntil(flushOutbox());\n  }\n});\n\nasync function flushOutbox() {\n  const messages = await getAllFromIndexedDB('outbox');\n\n  for (const message of messages) {\n    try {\n      const response = await fetch('/api/messages', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(message),\n      });\n\n      if (response.ok) {\n        await removeFromIndexedDB('outbox', message.id);\n      }\n    } catch (error) {\n      // Network still down: throw to signal the sync should retry\n      throw error;\n    }\n  }\n}\n",[117,6072,6073,6077,6081,6102,6116,6129,6133,6137,6141,6152,6172,6176,6193,6200,6219,6228,6241,6254,6259,6263,6269,6284,6288,6297,6302,6310,6314,6318],{"__ignoreMap":145},[149,6074,6075],{"class":151,"line":152},[149,6076,1285],{"class":984},[149,6078,6079],{"class":151,"line":159},[149,6080,991],{"emptyLinePlaceholder":990},[149,6082,6083,6085,6087,6089,6092,6094,6096,6098,6100],{"class":151,"line":176},[149,6084,1294],{"class":155},[149,6086,1123],{"class":781},[149,6088,1020],{"class":155},[149,6090,6091],{"class":169},"'sync'",[149,6093,1304],{"class":155},[149,6095,1308],{"class":1307},[149,6097,1311],{"class":155},[149,6099,1134],{"class":996},[149,6101,1070],{"class":155},[149,6103,6104,6106,6109,6111,6114],{"class":151,"line":189},[149,6105,1011],{"class":996},[149,6107,6108],{"class":155}," (event.tag ",[149,6110,1910],{"class":996},[149,6112,6113],{"class":169}," 'sync-outbox'",[149,6115,1730],{"class":155},[149,6117,6118,6120,6122,6124,6127],{"class":151,"line":202},[149,6119,1920],{"class":155},[149,6121,2236],{"class":781},[149,6123,1020],{"class":155},[149,6125,6126],{"class":781},"flushOutbox",[149,6128,1645],{"class":155},[149,6130,6131],{"class":151,"line":215},[149,6132,1058],{"class":155},[149,6134,6135],{"class":151,"line":227},[149,6136,1424],{"class":155},[149,6138,6139],{"class":151,"line":240},[149,6140,991],{"emptyLinePlaceholder":990},[149,6142,6143,6145,6147,6150],{"class":151,"line":253},[149,6144,997],{"class":996},[149,6146,1000],{"class":996},[149,6148,6149],{"class":781}," flushOutbox",[149,6151,1006],{"class":155},[149,6153,6154,6156,6159,6161,6163,6166,6168,6170],{"class":151,"line":266},[149,6155,1974],{"class":996},[149,6157,6158],{"class":162}," messages",[149,6160,1081],{"class":996},[149,6162,1084],{"class":996},[149,6164,6165],{"class":781}," getAllFromIndexedDB",[149,6167,1020],{"class":155},[149,6169,6002],{"class":169},[149,6171,1045],{"class":155},[149,6173,6174],{"class":151,"line":279},[149,6175,991],{"emptyLinePlaceholder":990},[149,6177,6178,6181,6183,6185,6188,6190],{"class":151,"line":292},[149,6179,6180],{"class":996},"  for",[149,6182,1014],{"class":155},[149,6184,1491],{"class":996},[149,6186,6187],{"class":162}," message",[149,6189,5608],{"class":996},[149,6191,6192],{"class":155}," messages) {\n",[149,6194,6195,6198],{"class":151,"line":305},[149,6196,6197],{"class":996},"    try",[149,6199,1070],{"class":155},[149,6201,6202,6204,6206,6208,6210,6212,6214,6217],{"class":151,"line":314},[149,6203,1141],{"class":996},[149,6205,1608],{"class":162},[149,6207,1081],{"class":996},[149,6209,1084],{"class":996},[149,6211,1403],{"class":781},[149,6213,1020],{"class":155},[149,6215,6216],{"class":169},"'/api/messages'",[149,6218,1098],{"class":155},[149,6220,6221,6224,6226],{"class":151,"line":320},[149,6222,6223],{"class":155},"        method: ",[149,6225,4995],{"class":169},[149,6227,173],{"class":155},[149,6229,6230,6233,6235,6237,6239],{"class":151,"line":333},[149,6231,6232],{"class":155},"        headers: { ",[149,6234,3518],{"class":169},[149,6236,166],{"class":155},[149,6238,3523],{"class":169},[149,6240,3526],{"class":155},[149,6242,6243,6245,6247,6249,6251],{"class":151,"line":346},[149,6244,5843],{"class":155},[149,6246,3495],{"class":162},[149,6248,856],{"class":155},[149,6250,3500],{"class":781},[149,6252,6253],{"class":155},"(message),\n",[149,6255,6256],{"class":151,"line":359},[149,6257,6258],{"class":155},"      });\n",[149,6260,6261],{"class":151,"line":370},[149,6262,991],{"emptyLinePlaceholder":990},[149,6264,6265,6267],{"class":151,"line":376},[149,6266,1376],{"class":996},[149,6268,1628],{"class":155},[149,6270,6271,6274,6277,6279,6281],{"class":151,"line":381},[149,6272,6273],{"class":996},"        await",[149,6275,6276],{"class":781}," removeFromIndexedDB",[149,6278,1020],{"class":155},[149,6280,6002],{"class":169},[149,6282,6283],{"class":155},", message.id);\n",[149,6285,6286],{"class":151,"line":393},[149,6287,1395],{"class":155},[149,6289,6290,6293,6295],{"class":151,"line":405},[149,6291,6292],{"class":155},"    } ",[149,6294,1194],{"class":996},[149,6296,1197],{"class":155},[149,6298,6299],{"class":151,"line":416},[149,6300,6301],{"class":984},"      // Network still down: throw to signal the sync should retry\n",[149,6303,6304,6307],{"class":151,"line":425},[149,6305,6306],{"class":996},"      throw",[149,6308,6309],{"class":155}," error;\n",[149,6311,6312],{"class":151,"line":430},[149,6313,482],{"class":155},[149,6315,6316],{"class":151,"line":435},[149,6317,1058],{"class":155},[149,6319,6320],{"class":151,"line":447},[149,6321,755],{"class":155},[16,6323,6324,6325,6328],{},"If ",[117,6326,6327],{},"flushOutbox()"," throws, the browser schedules another retry. The retry interval increases exponentially, and the browser will eventually give up after several hours. This is the right behavior: you don't want to retry a failed message forever.",[41,6330],{},[11,6332,6334],{"id":6333},"periodic-background-sync","Periodic Background Sync",[16,6336,6337,6338,6340],{},"A newer API, ",[23,6339,6334],{},", lets your service worker run on a schedule to refresh content, even while the app is closed. Think: a news app that pre-fetches articles every morning so they are ready when you open the app.",[140,6342,6344],{"className":975,"code":6343,"language":977,"meta":145,"style":145},"// main.js\n\nasync function registerPeriodicSync() {\n  const registration = await navigator.serviceWorker.ready;\n\n  const status = await navigator.permissions.query({\n    name: 'periodic-background-sync',\n  });\n\n  if (status.state === 'granted') {\n    await registration.periodicSync.register('refresh-content', {\n      minInterval: 24 * 60 * 60 * 1000, // once per day at minimum\n    });\n  }\n}\n",[117,6345,6346,6350,6354,6365,6377,6381,6400,6410,6414,6418,6431,6447,6475,6479,6483],{"__ignoreMap":145},[149,6347,6348],{"class":151,"line":152},[149,6349,4107],{"class":984},[149,6351,6352],{"class":151,"line":159},[149,6353,991],{"emptyLinePlaceholder":990},[149,6355,6356,6358,6360,6363],{"class":151,"line":176},[149,6357,997],{"class":996},[149,6359,1000],{"class":996},[149,6361,6362],{"class":781}," registerPeriodicSync",[149,6364,1006],{"class":155},[149,6366,6367,6369,6371,6373,6375],{"class":151,"line":189},[149,6368,1974],{"class":996},[149,6370,1078],{"class":162},[149,6372,1081],{"class":996},[149,6374,1084],{"class":996},[149,6376,4907],{"class":155},[149,6378,6379],{"class":151,"line":202},[149,6380,991],{"emptyLinePlaceholder":990},[149,6382,6383,6385,6388,6390,6392,6395,6398],{"class":151,"line":215},[149,6384,1974],{"class":996},[149,6386,6387],{"class":162}," status",[149,6389,1081],{"class":996},[149,6391,1084],{"class":996},[149,6393,6394],{"class":155}," navigator.permissions.",[149,6396,6397],{"class":781},"query",[149,6399,4931],{"class":155},[149,6401,6402,6405,6408],{"class":151,"line":227},[149,6403,6404],{"class":155},"    name: ",[149,6406,6407],{"class":169},"'periodic-background-sync'",[149,6409,173],{"class":155},[149,6411,6412],{"class":151,"line":240},[149,6413,2076],{"class":155},[149,6415,6416],{"class":151,"line":253},[149,6417,991],{"emptyLinePlaceholder":990},[149,6419,6420,6422,6425,6427,6429],{"class":151,"line":266},[149,6421,1011],{"class":996},[149,6423,6424],{"class":155}," (status.state ",[149,6426,1910],{"class":996},[149,6428,4719],{"class":169},[149,6430,1730],{"class":155},[149,6432,6433,6435,6438,6440,6442,6445],{"class":151,"line":279},[149,6434,4851],{"class":996},[149,6436,6437],{"class":155}," registration.periodicSync.",[149,6439,1090],{"class":781},[149,6441,1020],{"class":155},[149,6443,6444],{"class":169},"'refresh-content'",[149,6446,1098],{"class":155},[149,6448,6449,6452,6455,6458,6461,6463,6465,6467,6470,6472],{"class":151,"line":292},[149,6450,6451],{"class":155},"      minInterval: ",[149,6453,6454],{"class":162},"24",[149,6456,6457],{"class":996}," *",[149,6459,6460],{"class":162}," 60",[149,6462,6457],{"class":996},[149,6464,6460],{"class":162},[149,6466,6457],{"class":996},[149,6468,6469],{"class":162}," 1000",[149,6471,697],{"class":155},[149,6473,6474],{"class":984},"// once per day at minimum\n",[149,6476,6477],{"class":151,"line":305},[149,6478,1111],{"class":155},[149,6480,6481],{"class":151,"line":314},[149,6482,1058],{"class":155},[149,6484,6485],{"class":151,"line":320},[149,6486,755],{"class":155},[140,6488,6490],{"className":975,"code":6489,"language":977,"meta":145,"style":145},"// sw.js\n\nself.addEventListener('periodicsync', (event) => {\n  if (event.tag === 'refresh-content') {\n    event.waitUntil(refreshCachedContent());\n  }\n});\n\nasync function refreshCachedContent() {\n  const cache = await caches.open('dynamic-v1');\n  const urls = ['/api/posts', '/api/featured'];\n\n  await Promise.all(\n    urls.map(async (url) => {\n      const response = await fetch(url);\n      if (response.ok) cache.put(url, response);\n    })\n  );\n}\n",[117,6491,6492,6496,6500,6521,6534,6547,6551,6555,6559,6570,6590,6611,6615,6627,6649,6663,6674,6678,6682],{"__ignoreMap":145},[149,6493,6494],{"class":151,"line":152},[149,6495,1285],{"class":984},[149,6497,6498],{"class":151,"line":159},[149,6499,991],{"emptyLinePlaceholder":990},[149,6501,6502,6504,6506,6508,6511,6513,6515,6517,6519],{"class":151,"line":176},[149,6503,1294],{"class":155},[149,6505,1123],{"class":781},[149,6507,1020],{"class":155},[149,6509,6510],{"class":169},"'periodicsync'",[149,6512,1304],{"class":155},[149,6514,1308],{"class":1307},[149,6516,1311],{"class":155},[149,6518,1134],{"class":996},[149,6520,1070],{"class":155},[149,6522,6523,6525,6527,6529,6532],{"class":151,"line":189},[149,6524,1011],{"class":996},[149,6526,6108],{"class":155},[149,6528,1910],{"class":996},[149,6530,6531],{"class":169}," 'refresh-content'",[149,6533,1730],{"class":155},[149,6535,6536,6538,6540,6542,6545],{"class":151,"line":202},[149,6537,1920],{"class":155},[149,6539,2236],{"class":781},[149,6541,1020],{"class":155},[149,6543,6544],{"class":781},"refreshCachedContent",[149,6546,1645],{"class":155},[149,6548,6549],{"class":151,"line":215},[149,6550,1058],{"class":155},[149,6552,6553],{"class":151,"line":227},[149,6554,1424],{"class":155},[149,6556,6557],{"class":151,"line":240},[149,6558,991],{"emptyLinePlaceholder":990},[149,6560,6561,6563,6565,6568],{"class":151,"line":253},[149,6562,997],{"class":996},[149,6564,1000],{"class":996},[149,6566,6567],{"class":781}," refreshCachedContent",[149,6569,1006],{"class":155},[149,6571,6572,6574,6576,6578,6580,6582,6584,6586,6588],{"class":151,"line":266},[149,6573,1974],{"class":996},[149,6575,1765],{"class":162},[149,6577,1081],{"class":996},[149,6579,1084],{"class":996},[149,6581,1772],{"class":155},[149,6583,1540],{"class":781},[149,6585,1020],{"class":155},[149,6587,2378],{"class":169},[149,6589,1045],{"class":155},[149,6591,6592,6594,6597,6599,6601,6604,6606,6609],{"class":151,"line":279},[149,6593,1974],{"class":996},[149,6595,6596],{"class":162}," urls",[149,6598,1081],{"class":996},[149,6600,2370],{"class":155},[149,6602,6603],{"class":169},"'/api/posts'",[149,6605,697],{"class":155},[149,6607,6608],{"class":169},"'/api/featured'",[149,6610,2204],{"class":155},[149,6612,6613],{"class":151,"line":292},[149,6614,991],{"emptyLinePlaceholder":990},[149,6616,6617,6619,6621,6623,6625],{"class":151,"line":305},[149,6618,4978],{"class":996},[149,6620,2443],{"class":162},[149,6622,856],{"class":155},[149,6624,2448],{"class":781},[149,6626,1340],{"class":155},[149,6628,6629,6632,6634,6636,6638,6640,6643,6645,6647],{"class":151,"line":314},[149,6630,6631],{"class":155},"    urls.",[149,6633,2493],{"class":781},[149,6635,1020],{"class":155},[149,6637,997],{"class":996},[149,6639,1014],{"class":155},[149,6641,6642],{"class":1307},"url",[149,6644,1311],{"class":155},[149,6646,1134],{"class":996},[149,6648,1070],{"class":155},[149,6650,6651,6653,6655,6657,6659,6661],{"class":151,"line":320},[149,6652,1141],{"class":996},[149,6654,1608],{"class":162},[149,6656,1081],{"class":996},[149,6658,1084],{"class":996},[149,6660,1403],{"class":781},[149,6662,5679],{"class":155},[149,6664,6665,6667,6669,6671],{"class":151,"line":333},[149,6666,1376],{"class":996},[149,6668,3435],{"class":155},[149,6670,1636],{"class":781},[149,6672,6673],{"class":155},"(url, response);\n",[149,6675,6676],{"class":151,"line":346},[149,6677,1414],{"class":155},[149,6679,6680],{"class":151,"line":359},[149,6681,1419],{"class":155},[149,6683,6684],{"class":151,"line":370},[149,6685,755],{"class":155},[16,6687,6688,6689,6692],{},"The browser controls the actual schedule. It considers battery level, network conditions, and how often the user visits your app before deciding when to run. The ",[117,6690,6691],{},"minInterval"," is a hint, not a guarantee.",[41,6694],{},[11,6696,6698],{"id":6697},"workbox-the-production-tool","Workbox: The Production Tool",[16,6700,6701,6702,6705],{},"Writing service worker code by hand is educational but error-prone in production. Google's ",[23,6703,6704],{},"Workbox"," library provides battle-tested implementations of all the caching strategies, plus tooling to generate the app shell file list automatically.",[135,6707,6709],{"id":6708},"workbox-with-vite","Workbox with Vite",[140,6711,6713],{"className":975,"code":6712,"language":977,"meta":145,"style":145},"// vite.config.js\nimport { VitePWA } from 'vite-plugin-pwa';\n\nexport default {\n  plugins: [\n    VitePWA({\n      registerType: 'autoUpdate',\n      workbox: {\n        // Pre-cache all build output\n        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],\n\n        // Runtime caching rules\n        runtimeCaching: [\n          {\n            // API calls: network first\n            urlPattern: /^https:\\/\\/yourapp\\.com\\/api\\//,\n            handler: 'NetworkFirst',\n            options: {\n              cacheName: 'api-cache',\n              expiration: { maxEntries: 100, maxAgeSeconds: 3600 },\n              networkTimeoutSeconds: 5,\n            },\n          },\n          {\n            // Images: stale while revalidate\n            urlPattern: /\\.(?:png|jpg|jpeg|svg|gif|webp)$/,\n            handler: 'StaleWhileRevalidate',\n            options: {\n              cacheName: 'image-cache',\n              expiration: { maxEntries: 60, maxAgeSeconds: 30 * 24 * 3600 },\n            },\n          },\n          {\n            // Google Fonts: cache first (they use content hashes)\n            urlPattern: /^https:\\/\\/fonts\\.(googleapis|gstatic)\\.com\\//,\n            handler: 'CacheFirst',\n            options: {\n              cacheName: 'font-cache',\n              expiration: { maxAgeSeconds: 365 * 24 * 3600 },\n            },\n          },\n        ],\n      },\n      manifest: {\n        name: 'Clarity App',\n        short_name: 'Clarity',\n        theme_color: '#1e3a5f',\n        icons: [\n          { src: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },\n          { src: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },\n        ],\n      },\n    }),\n  ],\n};\n",[117,6714,6715,6720,6734,6738,6748,6753,6760,6770,6775,6780,6790,6794,6799,6804,6809,6814,6851,6861,6866,6876,6891,6901,6906,6911,6915,6920,6962,6971,6975,6984,7008,7012,7016,7020,7025,7060,7069,7073,7082,7100,7104,7108,7113,7118,7123,7133,7143,7153,7158,7179,7197,7201,7205,7210,7214],{"__ignoreMap":145},[149,6716,6717],{"class":151,"line":152},[149,6718,6719],{"class":984},"// vite.config.js\n",[149,6721,6722,6724,6727,6729,6732],{"class":151,"line":159},[149,6723,5717],{"class":996},[149,6725,6726],{"class":155}," { VitePWA } ",[149,6728,5197],{"class":996},[149,6730,6731],{"class":169}," 'vite-plugin-pwa'",[149,6733,1053],{"class":155},[149,6735,6736],{"class":151,"line":176},[149,6737,991],{"emptyLinePlaceholder":990},[149,6739,6740,6743,6746],{"class":151,"line":189},[149,6741,6742],{"class":996},"export",[149,6744,6745],{"class":996}," default",[149,6747,1070],{"class":155},[149,6749,6750],{"class":151,"line":202},[149,6751,6752],{"class":155},"  plugins: [\n",[149,6754,6755,6758],{"class":151,"line":215},[149,6756,6757],{"class":781},"    VitePWA",[149,6759,4931],{"class":155},[149,6761,6762,6765,6768],{"class":151,"line":227},[149,6763,6764],{"class":155},"      registerType: ",[149,6766,6767],{"class":169},"'autoUpdate'",[149,6769,173],{"class":155},[149,6771,6772],{"class":151,"line":240},[149,6773,6774],{"class":155},"      workbox: {\n",[149,6776,6777],{"class":151,"line":253},[149,6778,6779],{"class":984},"        // Pre-cache all build output\n",[149,6781,6782,6785,6788],{"class":151,"line":266},[149,6783,6784],{"class":155},"        globPatterns: [",[149,6786,6787],{"class":169},"'**/*.{js,css,html,ico,png,svg,woff2}'",[149,6789,738],{"class":155},[149,6791,6792],{"class":151,"line":279},[149,6793,991],{"emptyLinePlaceholder":990},[149,6795,6796],{"class":151,"line":292},[149,6797,6798],{"class":984},"        // Runtime caching rules\n",[149,6800,6801],{"class":151,"line":305},[149,6802,6803],{"class":155},"        runtimeCaching: [\n",[149,6805,6806],{"class":151,"line":314},[149,6807,6808],{"class":155},"          {\n",[149,6810,6811],{"class":151,"line":320},[149,6812,6813],{"class":984},"            // API calls: network first\n",[149,6815,6816,6819,6822,6825,6828,6831,6834,6836,6839,6842,6845,6847,6849],{"class":151,"line":333},[149,6817,6818],{"class":155},"            urlPattern:",[149,6820,6821],{"class":169}," /",[149,6823,6824],{"class":996},"^",[149,6826,6827],{"class":169},"https:",[149,6829,6830],{"class":3085},"\\/\\/",[149,6832,6833],{"class":169},"yourapp",[149,6835,3086],{"class":3085},[149,6837,6838],{"class":169},"com",[149,6840,6841],{"class":3085},"\\/",[149,6843,6844],{"class":169},"api",[149,6846,6841],{"class":3085},[149,6848,1256],{"class":169},[149,6850,173],{"class":155},[149,6852,6853,6856,6859],{"class":151,"line":346},[149,6854,6855],{"class":155},"            handler: ",[149,6857,6858],{"class":169},"'NetworkFirst'",[149,6860,173],{"class":155},[149,6862,6863],{"class":151,"line":359},[149,6864,6865],{"class":155},"            options: {\n",[149,6867,6868,6871,6874],{"class":151,"line":370},[149,6869,6870],{"class":155},"              cacheName: ",[149,6872,6873],{"class":169},"'api-cache'",[149,6875,173],{"class":155},[149,6877,6878,6881,6883,6886,6889],{"class":151,"line":376},[149,6879,6880],{"class":155},"              expiration: { maxEntries: ",[149,6882,3889],{"class":162},[149,6884,6885],{"class":155},", maxAgeSeconds: ",[149,6887,6888],{"class":162},"3600",[149,6890,3526],{"class":155},[149,6892,6893,6896,6899],{"class":151,"line":381},[149,6894,6895],{"class":155},"              networkTimeoutSeconds: ",[149,6897,6898],{"class":162},"5",[149,6900,173],{"class":155},[149,6902,6903],{"class":151,"line":393},[149,6904,6905],{"class":155},"            },\n",[149,6907,6908],{"class":151,"line":405},[149,6909,6910],{"class":155},"          },\n",[149,6912,6913],{"class":151,"line":416},[149,6914,6808],{"class":155},[149,6916,6917],{"class":151,"line":425},[149,6918,6919],{"class":984},"            // Images: stale while revalidate\n",[149,6921,6922,6924,6926,6928,6931,6933,6936,6938,6941,6943,6946,6948,6951,6953,6956,6958,6960],{"class":151,"line":430},[149,6923,6818],{"class":155},[149,6925,6821],{"class":169},[149,6927,3086],{"class":3085},[149,6929,6930],{"class":169},"(?:png",[149,6932,3100],{"class":996},[149,6934,6935],{"class":169},"jpg",[149,6937,3100],{"class":996},[149,6939,6940],{"class":169},"jpeg",[149,6942,3100],{"class":996},[149,6944,6945],{"class":169},"svg",[149,6947,3100],{"class":996},[149,6949,6950],{"class":169},"gif",[149,6952,3100],{"class":996},[149,6954,6955],{"class":169},"webp)",[149,6957,3121],{"class":996},[149,6959,1256],{"class":169},[149,6961,173],{"class":155},[149,6963,6964,6966,6969],{"class":151,"line":435},[149,6965,6855],{"class":155},[149,6967,6968],{"class":169},"'StaleWhileRevalidate'",[149,6970,173],{"class":155},[149,6972,6973],{"class":151,"line":447},[149,6974,6865],{"class":155},[149,6976,6977,6979,6982],{"class":151,"line":459},[149,6978,6870],{"class":155},[149,6980,6981],{"class":169},"'image-cache'",[149,6983,173],{"class":155},[149,6985,6986,6988,6991,6993,6996,6998,7001,7003,7006],{"class":151,"line":470},[149,6987,6880],{"class":155},[149,6989,6990],{"class":162},"60",[149,6992,6885],{"class":155},[149,6994,6995],{"class":162},"30",[149,6997,6457],{"class":996},[149,6999,7000],{"class":162}," 24",[149,7002,6457],{"class":996},[149,7004,7005],{"class":162}," 3600",[149,7007,3526],{"class":155},[149,7009,7010],{"class":151,"line":479},[149,7011,6905],{"class":155},[149,7013,7014],{"class":151,"line":485},[149,7015,6910],{"class":155},[149,7017,7018],{"class":151,"line":491},[149,7019,6808],{"class":155},[149,7021,7022],{"class":151,"line":499},[149,7023,7024],{"class":984},"            // Google Fonts: cache first (they use content hashes)\n",[149,7026,7027,7029,7031,7033,7035,7037,7040,7042,7045,7047,7050,7052,7054,7056,7058],{"class":151,"line":504},[149,7028,6818],{"class":155},[149,7030,6821],{"class":169},[149,7032,6824],{"class":996},[149,7034,6827],{"class":169},[149,7036,6830],{"class":3085},[149,7038,7039],{"class":169},"fonts",[149,7041,3086],{"class":3085},[149,7043,7044],{"class":169},"(googleapis",[149,7046,3100],{"class":996},[149,7048,7049],{"class":169},"gstatic)",[149,7051,3086],{"class":3085},[149,7053,6838],{"class":169},[149,7055,6841],{"class":3085},[149,7057,1256],{"class":169},[149,7059,173],{"class":155},[149,7061,7062,7064,7067],{"class":151,"line":516},[149,7063,6855],{"class":155},[149,7065,7066],{"class":169},"'CacheFirst'",[149,7068,173],{"class":155},[149,7070,7071],{"class":151,"line":528},[149,7072,6865],{"class":155},[149,7074,7075,7077,7080],{"class":151,"line":539},[149,7076,6870],{"class":155},[149,7078,7079],{"class":169},"'font-cache'",[149,7081,173],{"class":155},[149,7083,7084,7087,7090,7092,7094,7096,7098],{"class":151,"line":550},[149,7085,7086],{"class":155},"              expiration: { maxAgeSeconds: ",[149,7088,7089],{"class":162},"365",[149,7091,6457],{"class":996},[149,7093,7000],{"class":162},[149,7095,6457],{"class":996},[149,7097,7005],{"class":162},[149,7099,3526],{"class":155},[149,7101,7102],{"class":151,"line":555},[149,7103,6905],{"class":155},[149,7105,7106],{"class":151,"line":560},[149,7107,6910],{"class":155},[149,7109,7110],{"class":151,"line":572},[149,7111,7112],{"class":155},"        ],\n",[149,7114,7115],{"class":151,"line":584},[149,7116,7117],{"class":155},"      },\n",[149,7119,7120],{"class":151,"line":595},[149,7121,7122],{"class":155},"      manifest: {\n",[149,7124,7125,7128,7131],{"class":151,"line":605},[149,7126,7127],{"class":155},"        name: ",[149,7129,7130],{"class":169},"'Clarity App'",[149,7132,173],{"class":155},[149,7134,7135,7138,7141],{"class":151,"line":610},[149,7136,7137],{"class":155},"        short_name: ",[149,7139,7140],{"class":169},"'Clarity'",[149,7142,173],{"class":155},[149,7144,7145,7148,7151],{"class":151,"line":615},[149,7146,7147],{"class":155},"        theme_color: ",[149,7149,7150],{"class":169},"'#1e3a5f'",[149,7152,173],{"class":155},[149,7154,7155],{"class":151,"line":623},[149,7156,7157],{"class":155},"        icons: [\n",[149,7159,7160,7163,7165,7168,7171,7174,7177],{"class":151,"line":628},[149,7161,7162],{"class":155},"          { src: ",[149,7164,5342],{"class":169},[149,7166,7167],{"class":155},", sizes: ",[149,7169,7170],{"class":169},"'192x192'",[149,7172,7173],{"class":155},", type: ",[149,7175,7176],{"class":169},"'image/png'",[149,7178,3526],{"class":155},[149,7180,7181,7183,7186,7188,7191,7193,7195],{"class":151,"line":641},[149,7182,7162],{"class":155},[149,7184,7185],{"class":169},"'/icons/icon-512x512.png'",[149,7187,7167],{"class":155},[149,7189,7190],{"class":169},"'512x512'",[149,7192,7173],{"class":155},[149,7194,7176],{"class":169},[149,7196,3526],{"class":155},[149,7198,7199],{"class":151,"line":654},[149,7200,7112],{"class":155},[149,7202,7203],{"class":151,"line":667},[149,7204,7117],{"class":155},[149,7206,7207],{"class":151,"line":680},[149,7208,7209],{"class":155},"    }),\n",[149,7211,7212],{"class":151,"line":711},[149,7213,488],{"class":155},[149,7215,7216],{"class":151,"line":716},[149,7217,7218],{"class":155},"};\n",[16,7220,7221,7222,7225,7226,7229,7230,7233],{},"Workbox also provides ",[117,7223,7224],{},"expiration"," settings to limit cache size and age: ",[117,7227,7228],{},"maxEntries"," caps how many responses are stored, and ",[117,7231,7232],{},"maxAgeSeconds"," evicts responses older than the threshold. Without these limits, your app's cache can grow unbounded on users' devices.",[41,7235],{},[11,7237,7239],{"id":7238},"auditing-your-pwa-with-lighthouse","Auditing Your PWA with Lighthouse",[16,7241,7242,7243,7246],{},"Google's ",[23,7244,7245],{},"Lighthouse"," tool (built into Chrome DevTools) is the standard way to audit a PWA. It checks your manifest, service worker, HTTPS, and performance, giving you a score and specific action items.",[16,7248,7249],{},"Run it from Chrome DevTools > Lighthouse > Generate report.",[16,7251,7252],{},"A fully installable PWA passes these Lighthouse checks:",[140,7254,7256],{"className":814,"code":7255,"language":816,"meta":145,"style":145},"PWA Installability Checklist:\n  [x] Served over HTTPS\n  [x] Has a registered service worker\n  [x] Has a web app manifest with:\n      [x] name or short_name\n      [x] icons (192px and 512px)\n      [x] start_url\n      [x] display: standalone, fullscreen, or minimal-ui\n  [x] Service worker responds to fetch events\n  [x] Manifest's start_url responds with 200 when offline\n\nPWA Best Practices:\n  [x] Has a \u003Cmeta name=\"theme-color\" /> tag\n  [x] Has a \u003Cmeta name=\"viewport\" /> tag\n  [x] Page loads on slow 3G in under 10 seconds\n  [x] Each page has a unique URL\n",[117,7257,7258,7263,7268,7273,7278,7283,7288,7293,7298,7303,7308,7312,7317,7322,7327,7332],{"__ignoreMap":145},[149,7259,7260],{"class":151,"line":152},[149,7261,7262],{},"PWA Installability Checklist:\n",[149,7264,7265],{"class":151,"line":159},[149,7266,7267],{},"  [x] Served over HTTPS\n",[149,7269,7270],{"class":151,"line":176},[149,7271,7272],{},"  [x] Has a registered service worker\n",[149,7274,7275],{"class":151,"line":189},[149,7276,7277],{},"  [x] Has a web app manifest with:\n",[149,7279,7280],{"class":151,"line":202},[149,7281,7282],{},"      [x] name or short_name\n",[149,7284,7285],{"class":151,"line":215},[149,7286,7287],{},"      [x] icons (192px and 512px)\n",[149,7289,7290],{"class":151,"line":227},[149,7291,7292],{},"      [x] start_url\n",[149,7294,7295],{"class":151,"line":240},[149,7296,7297],{},"      [x] display: standalone, fullscreen, or minimal-ui\n",[149,7299,7300],{"class":151,"line":253},[149,7301,7302],{},"  [x] Service worker responds to fetch events\n",[149,7304,7305],{"class":151,"line":266},[149,7306,7307],{},"  [x] Manifest's start_url responds with 200 when offline\n",[149,7309,7310],{"class":151,"line":279},[149,7311,991],{"emptyLinePlaceholder":990},[149,7313,7314],{"class":151,"line":292},[149,7315,7316],{},"PWA Best Practices:\n",[149,7318,7319],{"class":151,"line":305},[149,7320,7321],{},"  [x] Has a \u003Cmeta name=\"theme-color\" /> tag\n",[149,7323,7324],{"class":151,"line":314},[149,7325,7326],{},"  [x] Has a \u003Cmeta name=\"viewport\" /> tag\n",[149,7328,7329],{"class":151,"line":320},[149,7330,7331],{},"  [x] Page loads on slow 3G in under 10 seconds\n",[149,7333,7334],{"class":151,"line":333},[149,7335,7336],{},"  [x] Each page has a unique URL\n",[16,7338,7339],{},"From the command line:",[140,7341,7345],{"className":7342,"code":7343,"language":7344,"meta":145,"style":145},"language-bash shiki shiki-themes github-light","# Install Lighthouse CLI\nnpm install -g lighthouse\n\n# Run a PWA audit\nlighthouse https://yourapp.com --only-categories=pwa --output=html --output-path=./report.html\n\n# Open the report\nopen report.html\n","bash",[117,7346,7347,7352,7366,7370,7375,7392,7396,7401],{"__ignoreMap":145},[149,7348,7349],{"class":151,"line":152},[149,7350,7351],{"class":984},"# Install Lighthouse CLI\n",[149,7353,7354,7357,7360,7363],{"class":151,"line":159},[149,7355,7356],{"class":781},"npm",[149,7358,7359],{"class":169}," install",[149,7361,7362],{"class":162}," -g",[149,7364,7365],{"class":169}," lighthouse\n",[149,7367,7368],{"class":151,"line":176},[149,7369,991],{"emptyLinePlaceholder":990},[149,7371,7372],{"class":151,"line":189},[149,7373,7374],{"class":984},"# Run a PWA audit\n",[149,7376,7377,7380,7383,7386,7389],{"class":151,"line":202},[149,7378,7379],{"class":781},"lighthouse",[149,7381,7382],{"class":169}," https://yourapp.com",[149,7384,7385],{"class":162}," --only-categories=pwa",[149,7387,7388],{"class":162}," --output=html",[149,7390,7391],{"class":162}," --output-path=./report.html\n",[149,7393,7394],{"class":151,"line":215},[149,7395,991],{"emptyLinePlaceholder":990},[149,7397,7398],{"class":151,"line":227},[149,7399,7400],{"class":984},"# Open the report\n",[149,7402,7403,7405],{"class":151,"line":240},[149,7404,1540],{"class":781},[149,7406,7407],{"class":169}," report.html\n",[41,7409],{},[11,7411,7413],{"id":7412},"storage-where-pwas-keep-data","Storage: Where PWAs Keep Data",[16,7415,7416],{},"Service workers have access to several storage APIs for persisting data on the device:",[7418,7419,7420,7436],"table",{},[7421,7422,7423],"thead",{},[7424,7425,7426,7430,7433],"tr",{},[7427,7428,7429],"th",{},"API",[7427,7431,7432],{},"Best For",[7427,7434,7435],{},"Accessible From",[7437,7438,7439,7455,7465,7476],"tbody",{},[7424,7440,7441,7449,7452],{},[7442,7443,7444,7445,7448],"td",{},"Cache Storage (",[117,7446,7447],{},"caches.open()",")",[7442,7450,7451],{},"Network responses (HTML, CSS, JSON)",[7442,7453,7454],{},"SW + Page",[7424,7456,7457,7460,7463],{},[7442,7458,7459],{},"IndexedDB",[7442,7461,7462],{},"Structured app data (user content, outbox queue)",[7442,7464,7454],{},[7424,7466,7467,7470,7473],{},[7442,7468,7469],{},"localStorage",[7442,7471,7472],{},"Simple key-value (small settings)",[7442,7474,7475],{},"Page only, NOT SW. Synchronous.",[7424,7477,7478,7481,7484],{},[7442,7479,7480],{},"sessionStorage",[7442,7482,7483],{},"Tab-scoped temporary state",[7442,7485,7486],{},"Page only, NOT SW",[16,7488,7489,7490,7493,7494,7496,7497,7499],{},"For service worker code, you have two choices: ",[23,7491,7492],{},"Cache Storage"," for network responses and ",[23,7495,7459],{}," for application data. ",[117,7498,7469],{}," is synchronous and is not available in service workers at all.",[135,7501,7503],{"id":7502},"checking-available-storage","Checking Available Storage",[16,7505,7506],{},"The Storage Manager API tells you how much space your app is using and the browser's estimate of how much is available:",[140,7508,7510],{"className":975,"code":7509,"language":977,"meta":145,"style":145},"// main.js\n\nasync function checkStorageQuota() {\n  if ('storage' in navigator && 'estimate' in navigator.storage) {\n    const { usage, quota } = await navigator.storage.estimate();\n    const usedMB = (usage / 1024 / 1024).toFixed(2);\n    const quotaMB = (quota / 1024 / 1024).toFixed(2);\n    console.log(`Using ${usedMB} MB of ${quotaMB} MB`);\n  }\n}\n",[117,7511,7512,7516,7520,7531,7555,7583,7616,7646,7671,7675],{"__ignoreMap":145},[149,7513,7514],{"class":151,"line":152},[149,7515,4107],{"class":984},[149,7517,7518],{"class":151,"line":159},[149,7519,991],{"emptyLinePlaceholder":990},[149,7521,7522,7524,7526,7529],{"class":151,"line":176},[149,7523,997],{"class":996},[149,7525,1000],{"class":996},[149,7527,7528],{"class":781}," checkStorageQuota",[149,7530,1006],{"class":155},[149,7532,7533,7535,7537,7540,7542,7545,7547,7550,7552],{"class":151,"line":189},[149,7534,1011],{"class":996},[149,7536,1014],{"class":155},[149,7538,7539],{"class":169},"'storage'",[149,7541,1026],{"class":996},[149,7543,7544],{"class":155}," navigator ",[149,7546,4601],{"class":996},[149,7548,7549],{"class":169}," 'estimate'",[149,7551,1026],{"class":996},[149,7553,7554],{"class":155}," navigator.storage) {\n",[149,7556,7557,7559,7561,7564,7566,7569,7571,7573,7575,7578,7581],{"class":151,"line":202},[149,7558,1075],{"class":996},[149,7560,2950],{"class":155},[149,7562,7563],{"class":162},"usage",[149,7565,697],{"class":155},[149,7567,7568],{"class":162},"quota",[149,7570,2955],{"class":155},[149,7572,785],{"class":996},[149,7574,1084],{"class":996},[149,7576,7577],{"class":155}," navigator.storage.",[149,7579,7580],{"class":781},"estimate",[149,7582,2320],{"class":155},[149,7584,7585,7587,7590,7592,7595,7597,7600,7602,7604,7606,7609,7611,7614],{"class":151,"line":215},[149,7586,1075],{"class":996},[149,7588,7589],{"class":162}," usedMB",[149,7591,1081],{"class":996},[149,7593,7594],{"class":155}," (usage ",[149,7596,1256],{"class":996},[149,7598,7599],{"class":162}," 1024",[149,7601,6821],{"class":996},[149,7603,7599],{"class":162},[149,7605,1548],{"class":155},[149,7607,7608],{"class":781},"toFixed",[149,7610,1020],{"class":155},[149,7612,7613],{"class":162},"2",[149,7615,1045],{"class":155},[149,7617,7618,7620,7623,7625,7628,7630,7632,7634,7636,7638,7640,7642,7644],{"class":151,"line":227},[149,7619,1075],{"class":996},[149,7621,7622],{"class":162}," quotaMB",[149,7624,1081],{"class":996},[149,7626,7627],{"class":155}," (quota ",[149,7629,1256],{"class":996},[149,7631,7599],{"class":162},[149,7633,6821],{"class":996},[149,7635,7599],{"class":162},[149,7637,1548],{"class":155},[149,7639,7608],{"class":781},[149,7641,1020],{"class":155},[149,7643,7613],{"class":162},[149,7645,1045],{"class":155},[149,7647,7648,7650,7652,7654,7657,7660,7663,7666,7669],{"class":151,"line":240},[149,7649,1034],{"class":155},[149,7651,1037],{"class":781},[149,7653,1020],{"class":155},[149,7655,7656],{"class":169},"`Using ${",[149,7658,7659],{"class":155},"usedMB",[149,7661,7662],{"class":169},"} MB of ${",[149,7664,7665],{"class":155},"quotaMB",[149,7667,7668],{"class":169},"} MB`",[149,7670,1045],{"class":155},[149,7672,7673],{"class":151,"line":253},[149,7674,1058],{"class":155},[149,7676,7677],{"class":151,"line":266},[149,7678,755],{"class":155},[16,7680,7681],{},"Browsers can evict cache storage under storage pressure. To opt your app's storage into \"persistent\" mode (not evicted without user approval):",[140,7683,7685],{"className":975,"code":7684,"language":977,"meta":145,"style":145},"async function requestPersistentStorage() {\n  if (navigator.storage && navigator.storage.persist) {\n    const granted = await navigator.storage.persist();\n    console.log('Persistent storage granted:', granted);\n  }\n}\n",[117,7686,7687,7698,7710,7728,7742,7746],{"__ignoreMap":145},[149,7688,7689,7691,7693,7696],{"class":151,"line":152},[149,7690,997],{"class":996},[149,7692,1000],{"class":996},[149,7694,7695],{"class":781}," requestPersistentStorage",[149,7697,1006],{"class":155},[149,7699,7700,7702,7705,7707],{"class":151,"line":159},[149,7701,1011],{"class":996},[149,7703,7704],{"class":155}," (navigator.storage ",[149,7706,4601],{"class":996},[149,7708,7709],{"class":155}," navigator.storage.persist) {\n",[149,7711,7712,7714,7717,7719,7721,7723,7726],{"class":151,"line":176},[149,7713,1075],{"class":996},[149,7715,7716],{"class":162}," granted",[149,7718,1081],{"class":996},[149,7720,1084],{"class":996},[149,7722,7577],{"class":155},[149,7724,7725],{"class":781},"persist",[149,7727,2320],{"class":155},[149,7729,7730,7732,7734,7736,7739],{"class":151,"line":189},[149,7731,1034],{"class":155},[149,7733,1037],{"class":781},[149,7735,1020],{"class":155},[149,7737,7738],{"class":169},"'Persistent storage granted:'",[149,7740,7741],{"class":155},", granted);\n",[149,7743,7744],{"class":151,"line":202},[149,7745,1058],{"class":155},[149,7747,7748],{"class":151,"line":215},[149,7749,755],{"class":155},[41,7751],{},[11,7753,7755],{"id":7754},"common-pwa-pitfalls","Common PWA Pitfalls",[135,7757,7759],{"id":7758},"_1-caching-api-responses-that-contain-user-data","1. Caching API Responses That Contain User Data",[16,7761,7762,7763,7766],{},"Never cache responses that contain user-specific data (JWT tokens, personal data) in a shared cache. Use ",[117,7764,7765],{},"Cache-Control: private"," on the server for these responses, and check for it in your service worker:",[140,7768,7770],{"className":975,"code":7769,"language":977,"meta":145,"style":145},"// sw.js\nfunction isCacheable(response) {\n  const cacheControl = response.headers.get('Cache-Control') ?? '';\n  return !cacheControl.includes('no-store') && !cacheControl.includes('private');\n}\n",[117,7771,7772,7776,7789,7818,7851],{"__ignoreMap":145},[149,7773,7774],{"class":151,"line":152},[149,7775,1285],{"class":984},[149,7777,7778,7780,7783,7785,7787],{"class":151,"line":159},[149,7779,5063],{"class":996},[149,7781,7782],{"class":781}," isCacheable",[149,7784,1020],{"class":155},[149,7786,2037],{"class":1307},[149,7788,1730],{"class":155},[149,7790,7791,7793,7796,7798,7801,7804,7806,7809,7811,7813,7816],{"class":151,"line":176},[149,7792,1974],{"class":996},[149,7794,7795],{"class":162}," cacheControl",[149,7797,1081],{"class":996},[149,7799,7800],{"class":155}," response.headers.",[149,7802,7803],{"class":781},"get",[149,7805,1020],{"class":155},[149,7807,7808],{"class":169},"'Cache-Control'",[149,7810,1311],{"class":155},[149,7812,2096],{"class":996},[149,7814,7815],{"class":169}," ''",[149,7817,1053],{"class":155},[149,7819,7820,7822,7824,7827,7829,7831,7834,7836,7838,7840,7842,7844,7846,7849],{"class":151,"line":189},[149,7821,2090],{"class":996},[149,7823,2475],{"class":996},[149,7825,7826],{"class":155},"cacheControl.",[149,7828,2483],{"class":781},[149,7830,1020],{"class":155},[149,7832,7833],{"class":169},"'no-store'",[149,7835,1311],{"class":155},[149,7837,4601],{"class":996},[149,7839,2475],{"class":996},[149,7841,7826],{"class":155},[149,7843,2483],{"class":781},[149,7845,1020],{"class":155},[149,7847,7848],{"class":169},"'private'",[149,7850,1045],{"class":155},[149,7852,7853],{"class":151,"line":202},[149,7854,755],{"class":155},[135,7856,7858],{"id":7857},"_2-not-versioning-your-cache","2. Not Versioning Your Cache",[16,7860,7861],{},"If you never change the cache name, users on old versions of your app will keep serving stale files forever. Always increment the cache version when you deploy:",[140,7863,7865],{"className":975,"code":7864,"language":977,"meta":145,"style":145},"// Wrong: cache never changes\nconst CACHE = 'my-cache';\n\n// Right: version matches your deployment\nconst CACHE = 'app-shell-v14';\n",[117,7866,7867,7872,7886,7890,7895],{"__ignoreMap":145},[149,7868,7869],{"class":151,"line":152},[149,7870,7871],{"class":984},"// Wrong: cache never changes\n",[149,7873,7874,7876,7879,7881,7884],{"class":151,"line":159},[149,7875,1491],{"class":996},[149,7877,7878],{"class":162}," CACHE",[149,7880,1081],{"class":996},[149,7882,7883],{"class":169}," 'my-cache'",[149,7885,1053],{"class":155},[149,7887,7888],{"class":151,"line":176},[149,7889,991],{"emptyLinePlaceholder":990},[149,7891,7892],{"class":151,"line":189},[149,7893,7894],{"class":984},"// Right: version matches your deployment\n",[149,7896,7897,7899,7901,7903,7906],{"class":151,"line":202},[149,7898,1491],{"class":996},[149,7900,7878],{"class":162},[149,7902,1081],{"class":996},[149,7904,7905],{"class":169}," 'app-shell-v14'",[149,7907,1053],{"class":155},[135,7909,7911],{"id":7910},"_3-the-infinite-loop-trap","3. The Infinite Loop Trap",[16,7913,7914,7915,7917],{},"If your service worker fetches its own URL (e.g., a request to ",[117,7916,1252],{}," goes through the fetch handler), you can create an infinite loop. Always skip service worker requests:",[140,7919,7921],{"className":975,"code":7920,"language":977,"meta":145,"style":145},"// sw.js\nself.addEventListener('fetch', (event) => {\n  if (event.request.url.includes('/sw.js')) return;\n  // ... handle other requests\n});\n",[117,7922,7923,7927,7947,7966,7971],{"__ignoreMap":145},[149,7924,7925],{"class":151,"line":152},[149,7926,1285],{"class":984},[149,7928,7929,7931,7933,7935,7937,7939,7941,7943,7945],{"class":151,"line":159},[149,7930,1294],{"class":155},[149,7932,1123],{"class":781},[149,7934,1020],{"class":155},[149,7936,1301],{"class":169},[149,7938,1304],{"class":155},[149,7940,1308],{"class":1307},[149,7942,1311],{"class":155},[149,7944,1134],{"class":996},[149,7946,1070],{"class":155},[149,7948,7949,7951,7954,7956,7958,7960,7962,7964],{"class":151,"line":176},[149,7950,1011],{"class":996},[149,7952,7953],{"class":155}," (event.request.url.",[149,7955,2483],{"class":781},[149,7957,1020],{"class":155},[149,7959,1095],{"class":169},[149,7961,5113],{"class":155},[149,7963,1594],{"class":996},[149,7965,1053],{"class":155},[149,7967,7968],{"class":151,"line":189},[149,7969,7970],{"class":984},"  // ... handle other requests\n",[149,7972,7973],{"class":151,"line":202},[149,7974,1424],{"class":155},[135,7976,7978],{"id":7977},"_4-serving-stale-html-that-references-missing-assets","4. Serving Stale HTML That References Missing Assets",[16,7980,7981,7982,7985,7986,7989,7990,7993],{},"If you cache an old ",[117,7983,7984],{},"index.html"," that references ",[117,7987,7988],{},"/app.abc123.js",", but the user's app shell cache only has ",[117,7991,7992],{},"/app.def456.js",", the page will fail to load. The App Shell pattern solves this: always pre-cache HTML and its exact asset versions together under the same cache version name.",[135,7995,7997],{"id":7996},"_5-not-handling-the-update-flow","5. Not Handling the Update Flow",[16,7999,8000],{},"When you deploy a new version, users with the old service worker installed won't see updates until they close and reopen all tabs. Consider showing an \"Update available\" banner and prompting users to refresh:",[140,8002,8004],{"className":975,"code":8003,"language":977,"meta":145,"style":145},"// main.js\n\nconst registration = await navigator.serviceWorker.register('/sw.js');\n\nregistration.addEventListener('updatefound', () => {\n  const newWorker = registration.installing;\n\n  newWorker.addEventListener('statechange', () => {\n    if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {\n      // A new version is waiting: show an update banner\n      showUpdateBanner(() => {\n        newWorker.postMessage({ type: 'SKIP_WAITING' });\n        window.location.reload();\n      });\n    }\n  });\n});\n",[117,8005,8006,8010,8014,8034,8038,8055,8065,8069,8087,8104,8109,8120,8136,8145,8149,8153,8157],{"__ignoreMap":145},[149,8007,8008],{"class":151,"line":152},[149,8009,4107],{"class":984},[149,8011,8012],{"class":151,"line":159},[149,8013,991],{"emptyLinePlaceholder":990},[149,8015,8016,8018,8020,8022,8024,8026,8028,8030,8032],{"class":151,"line":176},[149,8017,1491],{"class":996},[149,8019,1078],{"class":162},[149,8021,1081],{"class":996},[149,8023,1084],{"class":996},[149,8025,1087],{"class":155},[149,8027,1090],{"class":781},[149,8029,1020],{"class":155},[149,8031,1095],{"class":169},[149,8033,1045],{"class":155},[149,8035,8036],{"class":151,"line":189},[149,8037,991],{"emptyLinePlaceholder":990},[149,8039,8040,8043,8045,8047,8049,8051,8053],{"class":151,"line":202},[149,8041,8042],{"class":155},"registration.",[149,8044,1123],{"class":781},[149,8046,1020],{"class":155},[149,8048,1128],{"class":169},[149,8050,1131],{"class":155},[149,8052,1134],{"class":996},[149,8054,1070],{"class":155},[149,8056,8057,8059,8061,8063],{"class":151,"line":215},[149,8058,1974],{"class":996},[149,8060,1144],{"class":162},[149,8062,1081],{"class":996},[149,8064,1149],{"class":155},[149,8066,8067],{"class":151,"line":227},[149,8068,991],{"emptyLinePlaceholder":990},[149,8070,8071,8074,8076,8078,8081,8083,8085],{"class":151,"line":240},[149,8072,8073],{"class":155},"  newWorker.",[149,8075,1123],{"class":781},[149,8077,1020],{"class":155},[149,8079,8080],{"class":169},"'statechange'",[149,8082,1131],{"class":155},[149,8084,1134],{"class":996},[149,8086,1070],{"class":155},[149,8088,8089,8091,8094,8096,8098,8101],{"class":151,"line":253},[149,8090,1756],{"class":996},[149,8092,8093],{"class":155}," (newWorker.state ",[149,8095,1910],{"class":996},[149,8097,4348],{"class":169},[149,8099,8100],{"class":996}," &&",[149,8102,8103],{"class":155}," navigator.serviceWorker.controller) {\n",[149,8105,8106],{"class":151,"line":266},[149,8107,8108],{"class":984},"      // A new version is waiting: show an update banner\n",[149,8110,8111,8114,8116,8118],{"class":151,"line":279},[149,8112,8113],{"class":781},"      showUpdateBanner",[149,8115,3657],{"class":155},[149,8117,1134],{"class":996},[149,8119,1070],{"class":155},[149,8121,8122,8125,8128,8130,8133],{"class":151,"line":292},[149,8123,8124],{"class":155},"        newWorker.",[149,8126,8127],{"class":781},"postMessage",[149,8129,5569],{"class":155},[149,8131,8132],{"class":169},"'SKIP_WAITING'",[149,8134,8135],{"class":155}," });\n",[149,8137,8138,8141,8143],{"class":151,"line":305},[149,8139,8140],{"class":155},"        window.location.",[149,8142,4049],{"class":781},[149,8144,2320],{"class":155},[149,8146,8147],{"class":151,"line":314},[149,8148,6258],{"class":155},[149,8150,8151],{"class":151,"line":320},[149,8152,482],{"class":155},[149,8154,8155],{"class":151,"line":333},[149,8156,2076],{"class":155},[149,8158,8159],{"class":151,"line":346},[149,8160,1424],{"class":155},[140,8162,8164],{"className":975,"code":8163,"language":977,"meta":145,"style":145},"// sw.js\nself.addEventListener('message', (event) => {\n  if (event.data?.type === 'SKIP_WAITING') {\n    self.skipWaiting();\n  }\n});\n",[117,8165,8166,8170,8191,8205,8214,8218],{"__ignoreMap":145},[149,8167,8168],{"class":151,"line":152},[149,8169,1285],{"class":984},[149,8171,8172,8174,8176,8178,8181,8183,8185,8187,8189],{"class":151,"line":159},[149,8173,1294],{"class":155},[149,8175,1123],{"class":781},[149,8177,1020],{"class":155},[149,8179,8180],{"class":169},"'message'",[149,8182,1304],{"class":155},[149,8184,1308],{"class":1307},[149,8186,1311],{"class":155},[149,8188,1134],{"class":996},[149,8190,1070],{"class":155},[149,8192,8193,8195,8198,8200,8203],{"class":151,"line":176},[149,8194,1011],{"class":996},[149,8196,8197],{"class":155}," (event.data?.type ",[149,8199,1910],{"class":996},[149,8201,8202],{"class":169}," 'SKIP_WAITING'",[149,8204,1730],{"class":155},[149,8206,8207,8210,8212],{"class":151,"line":189},[149,8208,8209],{"class":155},"    self.",[149,8211,2317],{"class":781},[149,8213,2320],{"class":155},[149,8215,8216],{"class":151,"line":202},[149,8217,1058],{"class":155},[149,8219,8220],{"class":151,"line":215},[149,8221,1424],{"class":155},[41,8223],{},[11,8225,8227],{"id":8226},"pwa-capabilities-by-platform","PWA Capabilities by Platform",[16,8229,8230],{},"Browser and OS support for PWA APIs is not uniform. Here is the current state:",[140,8232,8234],{"className":814,"code":8233,"language":816,"meta":145,"style":145},"Feature                     Chrome  Firefox  Safari  Edge\n                            (Android/Desktop)       (iOS 16.4+)\n---------------------------------------------------------------\nService Workers             Yes     Yes      Yes     Yes\nCache API                   Yes     Yes      Yes     Yes\nWeb App Manifest            Yes     Yes      Yes     Yes\nInstall prompt              Yes     No       No      Yes\nPush Notifications          Yes     Yes      Yes*    Yes\nBackground Sync             Yes     No       No      Yes\nPeriodic Background Sync    Yes     No       No      Yes\nBadging API                 Yes     No       No      Yes\nFile System Access API      Yes     No       No      Yes\nWeb Share API               Yes     No       Yes     Yes\n\n* iOS Push Notifications require iOS 16.4+ and the app must be installed\n",[117,8235,8236,8241,8246,8251,8256,8261,8266,8271,8276,8281,8286,8291,8296,8301,8305],{"__ignoreMap":145},[149,8237,8238],{"class":151,"line":152},[149,8239,8240],{},"Feature                     Chrome  Firefox  Safari  Edge\n",[149,8242,8243],{"class":151,"line":159},[149,8244,8245],{},"                            (Android/Desktop)       (iOS 16.4+)\n",[149,8247,8248],{"class":151,"line":176},[149,8249,8250],{},"---------------------------------------------------------------\n",[149,8252,8253],{"class":151,"line":189},[149,8254,8255],{},"Service Workers             Yes     Yes      Yes     Yes\n",[149,8257,8258],{"class":151,"line":202},[149,8259,8260],{},"Cache API                   Yes     Yes      Yes     Yes\n",[149,8262,8263],{"class":151,"line":215},[149,8264,8265],{},"Web App Manifest            Yes     Yes      Yes     Yes\n",[149,8267,8268],{"class":151,"line":227},[149,8269,8270],{},"Install prompt              Yes     No       No      Yes\n",[149,8272,8273],{"class":151,"line":240},[149,8274,8275],{},"Push Notifications          Yes     Yes      Yes*    Yes\n",[149,8277,8278],{"class":151,"line":253},[149,8279,8280],{},"Background Sync             Yes     No       No      Yes\n",[149,8282,8283],{"class":151,"line":266},[149,8284,8285],{},"Periodic Background Sync    Yes     No       No      Yes\n",[149,8287,8288],{"class":151,"line":279},[149,8289,8290],{},"Badging API                 Yes     No       No      Yes\n",[149,8292,8293],{"class":151,"line":292},[149,8294,8295],{},"File System Access API      Yes     No       No      Yes\n",[149,8297,8298],{"class":151,"line":305},[149,8299,8300],{},"Web Share API               Yes     No       Yes     Yes\n",[149,8302,8303],{"class":151,"line":314},[149,8304,991],{"emptyLinePlaceholder":990},[149,8306,8307],{"class":151,"line":320},[149,8308,8309],{},"* iOS Push Notifications require iOS 16.4+ and the app must be installed\n",[16,8311,8312],{},"iOS Safari added web push support in iOS 16.4 (March 2023), but it only works for installed PWAs, not for pages open in the browser. The user must first \"Add to Home Screen\" before push notifications will arrive.",[16,8314,8315],{},"This is why the iOS install prompt UI (guiding users to tap \"Share > Add to Home Screen\") is important for any app that wants to send push notifications to iPhone users.",[41,8317],{},[11,8319,8321],{"id":8320},"summary-building-a-pwa-checklist","Summary: Building a PWA Checklist",[16,8323,8324],{},"Use this checklist when shipping a PWA to production:",[140,8326,8328],{"className":814,"code":8327,"language":816,"meta":145,"style":145},"FOUNDATION\n  [ ] Site is served over HTTPS\n  [ ] manifest.json is linked in every HTML page\n  [ ] Service worker is registered\n  [ ] An /offline.html fallback page exists\n\nMANIFEST\n  [ ] name and short_name set\n  [ ] start_url includes analytics parameter (?source=pwa)\n  [ ] display: \"standalone\"\n  [ ] theme_color and background_color set\n  [ ] 192x192 and 512x512 icons provided\n  [ ] Maskable icon variants provided\n\nSERVICE WORKER\n  [ ] App shell pre-cached at install time\n  [ ] Cache versioned and old caches cleaned up on activate\n  [ ] Caching strategy chosen per resource type\n  [ ] POST requests and cross-origin requests skipped\n  [ ] Cache size limited with expiration rules\n  [ ] Update flow handled (update banner + skipWaiting)\n\nINSTALLATION\n  [ ] beforeinstallprompt captured and deferred\n  [ ] Install UI shown at appropriate moment\n  [ ] iOS \"Add to Home Screen\" instructions shown for Safari users\n  [ ] appinstalled event tracked in analytics\n\nPUSH NOTIFICATIONS (if using)\n  [ ] VAPID keys generated\n  [ ] Permission requested after user action\n  [ ] Subscription sent to server\n  [ ] Notification click opens the right URL\n  [ ] Subscription expiry (410) handled on server\n\nAUDITING\n  [ ] Lighthouse PWA audit passes\n  [ ] Tested offline in DevTools Network panel\n  [ ] Tested install flow on Android Chrome\n  [ ] Tested on iOS Safari (16.4+)\n",[117,8329,8330,8335,8340,8345,8350,8355,8359,8364,8369,8374,8379,8384,8389,8394,8398,8403,8408,8413,8418,8423,8428,8433,8437,8442,8447,8452,8457,8462,8466,8471,8476,8481,8486,8491,8496,8500,8505,8510,8515,8520],{"__ignoreMap":145},[149,8331,8332],{"class":151,"line":152},[149,8333,8334],{},"FOUNDATION\n",[149,8336,8337],{"class":151,"line":159},[149,8338,8339],{},"  [ ] Site is served over HTTPS\n",[149,8341,8342],{"class":151,"line":176},[149,8343,8344],{},"  [ ] manifest.json is linked in every HTML page\n",[149,8346,8347],{"class":151,"line":189},[149,8348,8349],{},"  [ ] Service worker is registered\n",[149,8351,8352],{"class":151,"line":202},[149,8353,8354],{},"  [ ] An /offline.html fallback page exists\n",[149,8356,8357],{"class":151,"line":215},[149,8358,991],{"emptyLinePlaceholder":990},[149,8360,8361],{"class":151,"line":227},[149,8362,8363],{},"MANIFEST\n",[149,8365,8366],{"class":151,"line":240},[149,8367,8368],{},"  [ ] name and short_name set\n",[149,8370,8371],{"class":151,"line":253},[149,8372,8373],{},"  [ ] start_url includes analytics parameter (?source=pwa)\n",[149,8375,8376],{"class":151,"line":266},[149,8377,8378],{},"  [ ] display: \"standalone\"\n",[149,8380,8381],{"class":151,"line":279},[149,8382,8383],{},"  [ ] theme_color and background_color set\n",[149,8385,8386],{"class":151,"line":292},[149,8387,8388],{},"  [ ] 192x192 and 512x512 icons provided\n",[149,8390,8391],{"class":151,"line":305},[149,8392,8393],{},"  [ ] Maskable icon variants provided\n",[149,8395,8396],{"class":151,"line":314},[149,8397,991],{"emptyLinePlaceholder":990},[149,8399,8400],{"class":151,"line":320},[149,8401,8402],{},"SERVICE WORKER\n",[149,8404,8405],{"class":151,"line":333},[149,8406,8407],{},"  [ ] App shell pre-cached at install time\n",[149,8409,8410],{"class":151,"line":346},[149,8411,8412],{},"  [ ] Cache versioned and old caches cleaned up on activate\n",[149,8414,8415],{"class":151,"line":359},[149,8416,8417],{},"  [ ] Caching strategy chosen per resource type\n",[149,8419,8420],{"class":151,"line":370},[149,8421,8422],{},"  [ ] POST requests and cross-origin requests skipped\n",[149,8424,8425],{"class":151,"line":376},[149,8426,8427],{},"  [ ] Cache size limited with expiration rules\n",[149,8429,8430],{"class":151,"line":381},[149,8431,8432],{},"  [ ] Update flow handled (update banner + skipWaiting)\n",[149,8434,8435],{"class":151,"line":393},[149,8436,991],{"emptyLinePlaceholder":990},[149,8438,8439],{"class":151,"line":405},[149,8440,8441],{},"INSTALLATION\n",[149,8443,8444],{"class":151,"line":416},[149,8445,8446],{},"  [ ] beforeinstallprompt captured and deferred\n",[149,8448,8449],{"class":151,"line":425},[149,8450,8451],{},"  [ ] Install UI shown at appropriate moment\n",[149,8453,8454],{"class":151,"line":430},[149,8455,8456],{},"  [ ] iOS \"Add to Home Screen\" instructions shown for Safari users\n",[149,8458,8459],{"class":151,"line":435},[149,8460,8461],{},"  [ ] appinstalled event tracked in analytics\n",[149,8463,8464],{"class":151,"line":447},[149,8465,991],{"emptyLinePlaceholder":990},[149,8467,8468],{"class":151,"line":459},[149,8469,8470],{},"PUSH NOTIFICATIONS (if using)\n",[149,8472,8473],{"class":151,"line":470},[149,8474,8475],{},"  [ ] VAPID keys generated\n",[149,8477,8478],{"class":151,"line":479},[149,8479,8480],{},"  [ ] Permission requested after user action\n",[149,8482,8483],{"class":151,"line":485},[149,8484,8485],{},"  [ ] Subscription sent to server\n",[149,8487,8488],{"class":151,"line":491},[149,8489,8490],{},"  [ ] Notification click opens the right URL\n",[149,8492,8493],{"class":151,"line":499},[149,8494,8495],{},"  [ ] Subscription expiry (410) handled on server\n",[149,8497,8498],{"class":151,"line":504},[149,8499,991],{"emptyLinePlaceholder":990},[149,8501,8502],{"class":151,"line":516},[149,8503,8504],{},"AUDITING\n",[149,8506,8507],{"class":151,"line":528},[149,8508,8509],{},"  [ ] Lighthouse PWA audit passes\n",[149,8511,8512],{"class":151,"line":539},[149,8513,8514],{},"  [ ] Tested offline in DevTools Network panel\n",[149,8516,8517],{"class":151,"line":550},[149,8518,8519],{},"  [ ] Tested install flow on Android Chrome\n",[149,8521,8522],{"class":151,"line":555},[149,8523,8524],{},"  [ ] Tested on iOS Safari (16.4+)\n",[41,8526],{},[11,8528,8530],{"id":8529},"what-to-explore-next","What to Explore Next",[16,8532,8533],{},"PWAs open the door to a broader set of browser capabilities that are evolving rapidly:",[69,8535,8536,8542,8548,8554,8560,8566],{},[72,8537,8538,8541],{},[23,8539,8540],{},"Web Share API:"," let users share content to other apps natively",[72,8543,8544,8547],{},[23,8545,8546],{},"Web Bluetooth and Web USB:"," connect to hardware devices from the browser",[72,8549,8550,8553],{},[23,8551,8552],{},"File System Access API:"," read and write files on the user's local filesystem",[72,8555,8556,8559],{},[23,8557,8558],{},"Screen Wake Lock API:"," prevent the screen from sleeping during active use (maps, recipes, workouts)",[72,8561,8562,8565],{},[23,8563,8564],{},"Badging API:"," show a notification count on the app icon in the taskbar",[72,8567,8568,8571],{},[23,8569,8570],{},"Project Fugu:"," Google's initiative to close the remaining gap between web and native capabilities",[16,8573,8574],{},"The web platform is closing the gap with native faster than ever. PWAs are no longer a compromise, they are a genuinely competitive choice.",[16,8576,8577],{},"Thanks for reading!",[3811,8579,8580],{},"html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html pre.shiki code .s691h, html code.shiki .s691h{--shiki-default:#22863A;--shiki-default-font-weight:bold}",{"title":145,"searchDepth":159,"depth":159,"links":8582},[8583,8584,8585,8586,8591,8597,8606,8607,8613,8619,8623,8624,8627,8628,8631,8638,8639,8640],{"id":13,"depth":159,"text":14},{"id":45,"depth":159,"text":46},{"id":99,"depth":159,"text":100},{"id":125,"depth":159,"text":126,"children":8587},[8588,8589,8590],{"id":137,"depth":176,"text":138},{"id":802,"depth":176,"text":803},{"id":887,"depth":176,"text":888},{"id":905,"depth":159,"text":906,"children":8592},[8593,8594,8595,8596],{"id":916,"depth":176,"text":917},{"id":949,"depth":176,"text":950},{"id":968,"depth":176,"text":969},{"id":1267,"depth":176,"text":1268},{"id":1432,"depth":159,"text":1433,"children":8598},[8599,8600,8601,8602,8603,8604,8605],{"id":1439,"depth":176,"text":1440},{"id":1476,"depth":176,"text":1477},{"id":1685,"depth":176,"text":1686},{"id":1941,"depth":176,"text":1942},{"id":2106,"depth":176,"text":2107},{"id":2337,"depth":176,"text":2338},{"id":2581,"depth":176,"text":2582},{"id":3696,"depth":159,"text":3697},{"id":4080,"depth":159,"text":4081,"children":8608},[8609,8611,8612],{"id":4087,"depth":176,"text":8610},"The beforeinstallprompt Event",{"id":4444,"depth":176,"text":4445},{"id":4524,"depth":176,"text":4525},{"id":4627,"depth":159,"text":4628,"children":8614},[8615,8616,8617,8618],{"id":4634,"depth":176,"text":4635},{"id":4867,"depth":176,"text":4868},{"id":5237,"depth":176,"text":5238},{"id":5694,"depth":176,"text":5695},{"id":5945,"depth":159,"text":5946,"children":8620},[8621,8622],{"id":5955,"depth":176,"text":5956},{"id":6066,"depth":176,"text":6067},{"id":6333,"depth":159,"text":6334},{"id":6697,"depth":159,"text":6698,"children":8625},[8626],{"id":6708,"depth":176,"text":6709},{"id":7238,"depth":159,"text":7239},{"id":7412,"depth":159,"text":7413,"children":8629},[8630],{"id":7502,"depth":176,"text":7503},{"id":7754,"depth":159,"text":7755,"children":8632},[8633,8634,8635,8636,8637],{"id":7758,"depth":176,"text":7759},{"id":7857,"depth":176,"text":7858},{"id":7910,"depth":176,"text":7911},{"id":7977,"depth":176,"text":7978},{"id":7996,"depth":176,"text":7997},{"id":8226,"depth":159,"text":8227},{"id":8320,"depth":159,"text":8321},{"id":8529,"depth":159,"text":8530},"Web Fundamentals","/blog-covers/progressive-web-apps-deep-dive.png","2026-04-27","A comprehensive guide to Progressive Web Apps covering service workers, caching strategies, the Web App Manifest, push notifications, background sync, and everything you need to build installable, offline-capable web apps.","md",{},"/posts/progressive-web-apps-deep-dive","35 min read",{"title":5,"description":8644},{"loc":8647,"lastmod":8643},"posts/progressive-web-apps-deep-dive",[8653,8654,8655,8656,8657,4627,8658,8659],"pwa","service-workers","web-app-manifest","caching","offline","browser","frontend","wKwfY1rblz6ng_DOptTJ0yxLluRw95X3kXsZZaE-FQI",1777897734648]