[{"data":1,"prerenderedAt":20214},["ShallowReactive",2],{"blog-posts":3},{"posts":4,"totalPosts":101,"totalPages":64,"currentPage":58,"startIndex":58,"endIndex":89},[5,4810,13312,14276,16826,17558],{"id":6,"title":7,"author":8,"body":9,"category":4790,"cover":4791,"date":4792,"description":4793,"extension":4794,"featured":67,"meta":4795,"navigation":67,"path":4796,"published":67,"readingTime":4797,"seo":4798,"sitemap":4799,"stem":4800,"tags":4801,"updated":4792,"__hash__":4809},"posts/posts/how-the-web-works.md","How the Web Works: From Browser to Server (Simple Explanation)","Kashyap Kumar",{"type":10,"value":11,"toc":4728},"minimark",[12,17,26,29,32,35,38,42,45,153,156,158,162,165,185,299,308,326,328,332,342,347,350,354,357,512,515,533,548,554,564,568,571,693,697,700,778,784,808,815,819,826,846,853,855,859,869,873,876,942,948,954,958,965,1021,1024,1028,1031,1034,1036,1040,1047,1055,1059,1062,1201,1205,1211,1214,1253,1256,1260,1309,1312,1314,1318,1324,1328,1331,1383,1386,1395,1417,1427,1433,1439,1453,1459,1469,1473,1476,1612,1626,1630,1636,1674,1677,1681,1741,1744,1746,1750,1756,1760,1767,1888,1894,1900,1904,1907,2006,2013,2017,2020,2023,2371,2374,2378,2381,2523,2526,2541,2548,2550,2554,2557,2561,2640,2644,2647,2773,2776,2790,2794,2812,2817,2860,2874,2880,2888,2908,2922,2924,2928,2935,2939,3108,3112,3119,3235,3238,3293,3297,3304,3377,3380,3420,3435,3439,3453,3547,3564,3568,3575,3593,3597,3604,3607,3621,3624,3628,3635,3646,3649,3661,3664,3686,3697,3699,3703,3706,3710,3713,3798,3801,3827,3830,3834,3837,3849,3861,3864,3866,3870,3873,3877,3972,3976,3979,3989,4007,4013,4021,4025,4028,4093,4100,4102,4106,4109,4358,4361,4363,4367,4370,4453,4456,4458,4462,4465,4540,4547,4572,4575,4577,4581,4584,4646,4649,4651,4655,4658,4718,4721,4724],[13,14,16],"h2",{"id":15},"what-actually-happens-when-you-visit-a-website","What Actually Happens When You Visit a Website?",[18,19,20,21,25],"p",{},"You open a browser, type ",[22,23,24],"code",{},"https://example.com",", press Enter, and a webpage appears in less than a second. It feels like magic.",[18,27,28],{},"It is not magic. It is a beautifully orchestrated sequence of steps involving your operating system, dozens of servers around the world, and your browser working together in perfect coordination. Every single time.",[18,30,31],{},"This post is a complete, honest explanation of that sequence. Whether you are brand new to tech or a seasoned backend developer, you will find value here. Beginners will get clear analogies and simple explanations. Experienced developers will find the full technical depth: packet-level details, TLS negotiation, HTTP header semantics, browser rendering pipelines, and more.",[18,33,34],{},"Let us walk through the entire journey, step by step.",[36,37],"hr",{},[13,39,41],{"id":40},"the-big-picture","The Big Picture",[18,43,44],{},"Before we zoom in on each step, here is the full end-to-end flow at a glance:",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-txt shiki shiki-themes github-light","You type: https://example.com\n\n  [1] Browser checks its own DNS cache\n  [2] OS checks its local DNS cache + /etc/hosts\n  [3] Recursive DNS resolver queries the DNS hierarchy\n        Root Server --> .com TLD Server --> Authoritative NS\n  [4] DNS returns an IP address (e.g. 93.184.216.34)\n  [5] Browser opens a TCP connection (3-way handshake)\n  [6] TLS handshake (for HTTPS): establishes encrypted channel\n  [7] Browser sends an HTTP GET request\n  [8] Server receives request, runs application logic\n  [9] Server queries database (if needed)\n [10] Server builds and sends HTTP response (HTML, JSON, etc.)\n [11] Browser parses HTML, fetches sub-resources (CSS, JS, images)\n [12] Browser builds DOM + CSSOM --> Render Tree --> Layout --> Paint\n [13] Webpage appears on screen\n","txt","",[22,53,54,62,69,75,81,87,93,99,105,111,117,123,129,135,141,147],{"__ignoreMap":51},[55,56,59],"span",{"class":57,"line":58},"line",1,[55,60,61],{},"You type: https://example.com\n",[55,63,65],{"class":57,"line":64},2,[55,66,68],{"emptyLinePlaceholder":67},true,"\n",[55,70,72],{"class":57,"line":71},3,[55,73,74],{},"  [1] Browser checks its own DNS cache\n",[55,76,78],{"class":57,"line":77},4,[55,79,80],{},"  [2] OS checks its local DNS cache + /etc/hosts\n",[55,82,84],{"class":57,"line":83},5,[55,85,86],{},"  [3] Recursive DNS resolver queries the DNS hierarchy\n",[55,88,90],{"class":57,"line":89},6,[55,91,92],{},"        Root Server --> .com TLD Server --> Authoritative NS\n",[55,94,96],{"class":57,"line":95},7,[55,97,98],{},"  [4] DNS returns an IP address (e.g. 93.184.216.34)\n",[55,100,102],{"class":57,"line":101},8,[55,103,104],{},"  [5] Browser opens a TCP connection (3-way handshake)\n",[55,106,108],{"class":57,"line":107},9,[55,109,110],{},"  [6] TLS handshake (for HTTPS): establishes encrypted channel\n",[55,112,114],{"class":57,"line":113},10,[55,115,116],{},"  [7] Browser sends an HTTP GET request\n",[55,118,120],{"class":57,"line":119},11,[55,121,122],{},"  [8] Server receives request, runs application logic\n",[55,124,126],{"class":57,"line":125},12,[55,127,128],{},"  [9] Server queries database (if needed)\n",[55,130,132],{"class":57,"line":131},13,[55,133,134],{}," [10] Server builds and sends HTTP response (HTML, JSON, etc.)\n",[55,136,138],{"class":57,"line":137},14,[55,139,140],{}," [11] Browser parses HTML, fetches sub-resources (CSS, JS, images)\n",[55,142,144],{"class":57,"line":143},15,[55,145,146],{}," [12] Browser builds DOM + CSSOM --> Render Tree --> Layout --> Paint\n",[55,148,150],{"class":57,"line":149},16,[55,151,152],{}," [13] Webpage appears on screen\n",[18,154,155],{},"Each of these steps has significant depth. Let us explore them one by one.",[36,157],{},[13,159,161],{"id":160},"step-1-anatomy-of-a-url","Step 1: Anatomy of a URL",[18,163,164],{},"Before the browser can do anything, it needs to understand what you typed. A URL (Uniform Resource Locator) is not just an address, it is a structured specification that contains multiple distinct pieces of information.",[46,166,168],{"className":48,"code":167,"language":50,"meta":51,"style":51},"https://www.example.com:443/blog/article?id=42&lang=en#comments\n  |        |       |     |       |              |          |\nscheme   subdomain domain port   path         query     fragment\n",[22,169,170,175,180],{"__ignoreMap":51},[55,171,172],{"class":57,"line":58},[55,173,174],{},"https://www.example.com:443/blog/article?id=42&lang=en#comments\n",[55,176,177],{"class":57,"line":64},[55,178,179],{},"  |        |       |     |       |              |          |\n",[55,181,182],{"class":57,"line":71},[55,183,184],{},"scheme   subdomain domain port   path         query     fragment\n",[186,187,188,204],"table",{},[189,190,191],"thead",{},[192,193,194,198,201],"tr",{},[195,196,197],"th",{},"Part",[195,199,200],{},"Example",[195,202,203],{},"What it means",[205,206,207,221,234,247,260,273,286],"tbody",{},[192,208,209,213,218],{},[210,211,212],"td",{},"Scheme",[210,214,215],{},[22,216,217],{},"https",[210,219,220],{},"Which protocol to use (HTTP or HTTPS)",[192,222,223,226,231],{},[210,224,225],{},"Subdomain",[210,227,228],{},[22,229,230],{},"www",[210,232,233],{},"Subdivision of the domain",[192,235,236,239,244],{},[210,237,238],{},"Domain",[210,240,241],{},[22,242,243],{},"example.com",[210,245,246],{},"The human-readable name of the server",[192,248,249,252,257],{},[210,250,251],{},"Port",[210,253,254],{},[22,255,256],{},"443",[210,258,259],{},"Which \"door\" on the server to knock on (443 is default for HTTPS)",[192,261,262,265,270],{},[210,263,264],{},"Path",[210,266,267],{},[22,268,269],{},"/blog/article",[210,271,272],{},"Which resource on the server to request",[192,274,275,278,283],{},[210,276,277],{},"Query string",[210,279,280],{},[22,281,282],{},"?id=42&lang=en",[210,284,285],{},"Key-value pairs of extra data sent to the server",[192,287,288,291,296],{},[210,289,290],{},"Fragment",[210,292,293],{},[22,294,295],{},"#comments",[210,297,298],{},"Scrolls to a section on the page (never sent to the server)",[300,301,302],"blockquote",{},[18,303,304,305,307],{},"The fragment (",[22,306,295],{},") is handled entirely by the browser. It is stripped out before the request leaves your machine. The server never sees it.",[18,309,310,311,314,315,317,318,321,322,325],{},"The browser now knows: use HTTPS, contact ",[22,312,313],{},"www.example.com",", ask for ",[22,316,269],{},", and pass ",[22,319,320],{},"id=42"," and ",[22,323,324],{},"lang=en"," as parameters. But it does not have an IP address yet. That is where DNS comes in.",[36,327],{},[13,329,331],{"id":330},"step-2-dns-the-internets-phone-book","Step 2: DNS (The Internet's Phone Book)",[18,333,334,335,338,339,341],{},"Every device on the internet has a numeric IP address (like ",[22,336,337],{},"93.184.216.34","). Humans are bad at remembering numbers, so we invented domain names like ",[22,340,243],{},". DNS (Domain Name System) is the global distributed system that translates one into the other.",[343,344,346],"h3",{"id":345},"why-not-just-use-ip-addresses","Why Not Just Use IP Addresses?",[18,348,349],{},"Imagine if every phone contact was stored as a 12-digit number instead of a name. You would never memorize them. And if a company moved to a new building (changed servers), they would have to tell everyone their new phone number. DNS lets you update the mapping in one place and everyone finds the new server automatically.",[343,351,353],{"id":352},"the-dns-resolution-process","The DNS Resolution Process",[18,355,356],{},"DNS resolution is not a single lookup. It is a recursive journey through a hierarchy of specialized servers.",[46,358,360],{"className":48,"code":359,"language":50,"meta":51,"style":51},"                          Your Browser\n                               |\n                    (1) Check browser DNS cache\n                               |\n                    (2) Check OS cache + /etc/hosts\n                               |\n                    (3) Ask Recursive Resolver\n                         (usually your ISP\n                          or 8.8.8.8 / 1.1.1.1)\n                               |\n             +-----------------+-----------------+\n             |                                   |\n    (4) Ask Root Server             (7) Root says: ask .com TLD\n          (13 clusters             servers for \"example.com\"\n          worldwide)                             |\n                                    (8) Ask .com TLD Server\n                                                 |\n                                    (9) TLD says: ask\n                                    ns1.example.com (authoritative)\n                                                 |\n                                   (10) Ask Authoritative NS\n                                                 |\n                                   (11) Returns IP: 93.184.216.34\n                                                 |\n                    (12) Recursive Resolver returns IP to browser\n                               |\n                    (13) Browser caches IP with TTL\n                               |\n                    (14) Browser connects to 93.184.216.34\n",[22,361,362,367,372,377,381,386,390,395,400,405,409,414,419,424,429,434,439,445,451,457,462,468,473,479,484,490,495,501,506],{"__ignoreMap":51},[55,363,364],{"class":57,"line":58},[55,365,366],{},"                          Your Browser\n",[55,368,369],{"class":57,"line":64},[55,370,371],{},"                               |\n",[55,373,374],{"class":57,"line":71},[55,375,376],{},"                    (1) Check browser DNS cache\n",[55,378,379],{"class":57,"line":77},[55,380,371],{},[55,382,383],{"class":57,"line":83},[55,384,385],{},"                    (2) Check OS cache + /etc/hosts\n",[55,387,388],{"class":57,"line":89},[55,389,371],{},[55,391,392],{"class":57,"line":95},[55,393,394],{},"                    (3) Ask Recursive Resolver\n",[55,396,397],{"class":57,"line":101},[55,398,399],{},"                         (usually your ISP\n",[55,401,402],{"class":57,"line":107},[55,403,404],{},"                          or 8.8.8.8 / 1.1.1.1)\n",[55,406,407],{"class":57,"line":113},[55,408,371],{},[55,410,411],{"class":57,"line":119},[55,412,413],{},"             +-----------------+-----------------+\n",[55,415,416],{"class":57,"line":125},[55,417,418],{},"             |                                   |\n",[55,420,421],{"class":57,"line":131},[55,422,423],{},"    (4) Ask Root Server             (7) Root says: ask .com TLD\n",[55,425,426],{"class":57,"line":137},[55,427,428],{},"          (13 clusters             servers for \"example.com\"\n",[55,430,431],{"class":57,"line":143},[55,432,433],{},"          worldwide)                             |\n",[55,435,436],{"class":57,"line":149},[55,437,438],{},"                                    (8) Ask .com TLD Server\n",[55,440,442],{"class":57,"line":441},17,[55,443,444],{},"                                                 |\n",[55,446,448],{"class":57,"line":447},18,[55,449,450],{},"                                    (9) TLD says: ask\n",[55,452,454],{"class":57,"line":453},19,[55,455,456],{},"                                    ns1.example.com (authoritative)\n",[55,458,460],{"class":57,"line":459},20,[55,461,444],{},[55,463,465],{"class":57,"line":464},21,[55,466,467],{},"                                   (10) Ask Authoritative NS\n",[55,469,471],{"class":57,"line":470},22,[55,472,444],{},[55,474,476],{"class":57,"line":475},23,[55,477,478],{},"                                   (11) Returns IP: 93.184.216.34\n",[55,480,482],{"class":57,"line":481},24,[55,483,444],{},[55,485,487],{"class":57,"line":486},25,[55,488,489],{},"                    (12) Recursive Resolver returns IP to browser\n",[55,491,493],{"class":57,"line":492},26,[55,494,371],{},[55,496,498],{"class":57,"line":497},27,[55,499,500],{},"                    (13) Browser caches IP with TTL\n",[55,502,504],{"class":57,"line":503},28,[55,505,371],{},[55,507,509],{"class":57,"line":508},29,[55,510,511],{},"                    (14) Browser connects to 93.184.216.34\n",[18,513,514],{},"Let us break down the players:",[18,516,517,521,522,525,526,525,529,532],{},[518,519,520],"strong",{},"Root Name Servers:"," There are 13 logical root name servers (operated by groups like ICANN, NASA, and Verisign). They do not know IP addresses of individual domains, but they know which servers are responsible for each TLD (Top Level Domain) like ",[22,523,524],{},".com",", ",[22,527,528],{},".org",[22,530,531],{},".io",".",[18,534,535,538,539,541,542,544,545,532],{},[518,536,537],{},"TLD Name Servers:"," These know which authoritative name server is responsible for each domain under their TLD. The ",[22,540,524],{}," TLD server knows that ",[22,543,243],{}," is managed by ",[22,546,547],{},"ns1.example.com",[18,549,550,553],{},[518,551,552],{},"Authoritative Name Servers:"," These are the final authority. They hold the actual DNS records for a domain. When you register a domain and set up hosting, you configure these records (called \"A records\", \"CNAME records\", etc.).",[18,555,556,559,560,563],{},[518,557,558],{},"Recursive Resolver:"," This is the engine that does the hard work. It is a server (usually run by your ISP, or a public resolver like Google's ",[22,561,562],{},"8.8.8.8",") that takes your query, walks the hierarchy, collects the answer, and returns it to you. It also caches the result for efficiency.",[343,565,567],{"id":566},"dns-record-types","DNS Record Types",[18,569,570],{},"DNS is not just about IP addresses. It stores several types of records:",[186,572,573,585],{},[189,574,575],{},[192,576,577,580,583],{},[195,578,579],{},"Record Type",[195,581,582],{},"Purpose",[195,584,200],{},[205,586,587,602,617,632,647,662,677],{},[192,588,589,594,597],{},[210,590,591],{},[22,592,593],{},"A",[210,595,596],{},"Maps domain to IPv4 address",[210,598,599],{},[22,600,601],{},"example.com --> 93.184.216.34",[192,603,604,609,612],{},[210,605,606],{},[22,607,608],{},"AAAA",[210,610,611],{},"Maps domain to IPv6 address",[210,613,614],{},[22,615,616],{},"example.com --> 2606:2800:220:1:248:1893:25c8:1946",[192,618,619,624,627],{},[210,620,621],{},[22,622,623],{},"CNAME",[210,625,626],{},"Alias from one domain to another",[210,628,629],{},[22,630,631],{},"www.example.com --> example.com",[192,633,634,639,642],{},[210,635,636],{},[22,637,638],{},"MX",[210,640,641],{},"Mail server for the domain",[210,643,644],{},[22,645,646],{},"example.com --> mail.example.com",[192,648,649,654,657],{},[210,650,651],{},[22,652,653],{},"TXT",[210,655,656],{},"Arbitrary text (used for SPF, DKIM, domain verification)",[210,658,659],{},[22,660,661],{},"\"v=spf1 include:...\"",[192,663,664,669,672],{},[210,665,666],{},[22,667,668],{},"NS",[210,670,671],{},"Which servers are authoritative for this domain",[210,673,674],{},[22,675,676],{},"example.com --> ns1.example.com",[192,678,679,684,687],{},[210,680,681],{},[22,682,683],{},"TTL",[210,685,686],{},"Time To Live, how long resolvers should cache this record",[210,688,689,692],{},[22,690,691],{},"3600"," (1 hour)",[343,694,696],{"id":695},"dns-lookup-in-practice","DNS Lookup in Practice",[18,698,699],{},"You can run a DNS lookup yourself right now. Open your terminal:",[46,701,705],{"className":702,"code":703,"language":704,"meta":51,"style":51},"language-bash shiki shiki-themes github-light","# Simple lookup\ndig example.com\n\n# Look for a specific record type\ndig example.com MX\n\n# Trace the entire resolution path\ndig +trace example.com\n\n# Use a specific DNS server\ndig @8.8.8.8 example.com\n","bash",[22,706,707,713,723,727,732,742,746,751,760,764,769],{"__ignoreMap":51},[55,708,709],{"class":57,"line":58},[55,710,712],{"class":711},"sAwPA","# Simple lookup\n",[55,714,715,719],{"class":57,"line":64},[55,716,718],{"class":717},"s7eDp","dig",[55,720,722],{"class":721},"sYBdl"," example.com\n",[55,724,725],{"class":57,"line":71},[55,726,68],{"emptyLinePlaceholder":67},[55,728,729],{"class":57,"line":77},[55,730,731],{"class":711},"# Look for a specific record type\n",[55,733,734,736,739],{"class":57,"line":83},[55,735,718],{"class":717},[55,737,738],{"class":721}," example.com",[55,740,741],{"class":721}," MX\n",[55,743,744],{"class":57,"line":89},[55,745,68],{"emptyLinePlaceholder":67},[55,747,748],{"class":57,"line":95},[55,749,750],{"class":711},"# Trace the entire resolution path\n",[55,752,753,755,758],{"class":57,"line":101},[55,754,718],{"class":717},[55,756,757],{"class":721}," +trace",[55,759,722],{"class":721},[55,761,762],{"class":57,"line":107},[55,763,68],{"emptyLinePlaceholder":67},[55,765,766],{"class":57,"line":113},[55,767,768],{"class":711},"# Use a specific DNS server\n",[55,770,771,773,776],{"class":57,"line":119},[55,772,718],{"class":717},[55,774,775],{"class":721}," @8.8.8.8",[55,777,722],{"class":721},[18,779,780,781,783],{},"A typical ",[22,782,718],{}," response looks like this:",[46,785,787],{"className":48,"code":786,"language":50,"meta":51,"style":51},";; ANSWER SECTION:\nexample.com.    86400   IN  A   93.184.216.34\n\n; name         TTL   class type  value\n",[22,788,789,794,799,803],{"__ignoreMap":51},[55,790,791],{"class":57,"line":58},[55,792,793],{},";; ANSWER SECTION:\n",[55,795,796],{"class":57,"line":64},[55,797,798],{},"example.com.    86400   IN  A   93.184.216.34\n",[55,800,801],{"class":57,"line":71},[55,802,68],{"emptyLinePlaceholder":67},[55,804,805],{"class":57,"line":77},[55,806,807],{},"; name         TTL   class type  value\n",[18,809,810,811,814],{},"The TTL here is ",[22,812,813],{},"86400"," seconds (24 hours). This means resolvers can cache this record for 24 hours before asking again. Lowering the TTL is important before moving a site to a new server, so DNS changes propagate quickly.",[343,816,818],{"id":817},"etchosts-the-override-file","/etc/hosts: The Override File",[18,820,821,822,825],{},"Before your OS even contacts a DNS server, it checks a local file called ",[22,823,824],{},"/etc/hosts",". You can put entries there to override DNS:",[46,827,829],{"className":48,"code":828,"language":50,"meta":51,"style":51},"# /etc/hosts\n127.0.0.1   localhost\n192.168.1.10  my-dev-server.local\n",[22,830,831,836,841],{"__ignoreMap":51},[55,832,833],{"class":57,"line":58},[55,834,835],{},"# /etc/hosts\n",[55,837,838],{"class":57,"line":64},[55,839,840],{},"127.0.0.1   localhost\n",[55,842,843],{"class":57,"line":71},[55,844,845],{},"192.168.1.10  my-dev-server.local\n",[18,847,848,849,852],{},"This is why developers often add entries like ",[22,850,851],{},"127.0.0.1 myapp.test"," to test local sites with real domain names. It bypasses DNS entirely.",[36,854],{},[13,856,858],{"id":857},"step-3-tcpip-how-packets-travel-across-the-world","Step 3: TCP/IP (How Packets Travel Across the World)",[18,860,861,862,864,865,868],{},"Now the browser has an IP address: ",[22,863,337],{},". But the internet does not send whole files at once. It breaks everything into small chunks called ",[518,866,867],{},"packets",", sends them independently across the network, and reassembles them at the destination. This is the job of the TCP/IP suite.",[343,870,872],{"id":871},"the-internet-protocol-stack","The Internet Protocol Stack",[18,874,875],{},"The internet is built on a layered model. Each layer handles a specific job and hands work to the layer below it:",[46,877,879],{"className":48,"code":878,"language":50,"meta":51,"style":51},"+-----------------------------+\n|   Application Layer         |  HTTP, HTTPS, DNS, SMTP\n|   (What data means)         |\n+-----------------------------+\n|   Transport Layer           |  TCP, UDP\n|   (Reliable delivery)       |\n+-----------------------------+\n|   Internet Layer            |  IP (IPv4, IPv6)\n|   (Routing packets)         |\n+-----------------------------+\n|   Network Access Layer      |  Ethernet, Wi-Fi\n|   (Physical transmission)   |\n+-----------------------------+\n",[22,880,881,886,891,896,900,905,910,914,919,924,928,933,938],{"__ignoreMap":51},[55,882,883],{"class":57,"line":58},[55,884,885],{},"+-----------------------------+\n",[55,887,888],{"class":57,"line":64},[55,889,890],{},"|   Application Layer         |  HTTP, HTTPS, DNS, SMTP\n",[55,892,893],{"class":57,"line":71},[55,894,895],{},"|   (What data means)         |\n",[55,897,898],{"class":57,"line":77},[55,899,885],{},[55,901,902],{"class":57,"line":83},[55,903,904],{},"|   Transport Layer           |  TCP, UDP\n",[55,906,907],{"class":57,"line":89},[55,908,909],{},"|   (Reliable delivery)       |\n",[55,911,912],{"class":57,"line":95},[55,913,885],{},[55,915,916],{"class":57,"line":101},[55,917,918],{},"|   Internet Layer            |  IP (IPv4, IPv6)\n",[55,920,921],{"class":57,"line":107},[55,922,923],{},"|   (Routing packets)         |\n",[55,925,926],{"class":57,"line":113},[55,927,885],{},[55,929,930],{"class":57,"line":119},[55,931,932],{},"|   Network Access Layer      |  Ethernet, Wi-Fi\n",[55,934,935],{"class":57,"line":125},[55,936,937],{},"|   (Physical transmission)   |\n",[55,939,940],{"class":57,"line":131},[55,941,885],{},[18,943,944,947],{},[518,945,946],{},"IP (Internet Protocol)"," handles addressing and routing. It labels each packet with a source IP address and a destination IP address, and routers around the world forward packets toward the destination based on routing tables.",[18,949,950,953],{},[518,951,952],{},"TCP (Transmission Control Protocol)"," adds reliability on top of IP. It guarantees that packets arrive, that they arrive in order, and that lost packets are retransmitted. This is critical for loading web pages correctly.",[343,955,957],{"id":956},"the-tcp-3-way-handshake","The TCP 3-Way Handshake",[18,959,960,961,964],{},"Before any data is sent, TCP establishes a ",[518,962,963],{},"connection"," between the browser and the server. This takes exactly 3 steps:",[46,966,968],{"className":48,"code":967,"language":50,"meta":51,"style":51},"Browser (Client)                      Server\n      |                                  |\n      |-------- SYN (seq=x) ----------->|   \"Hello, I want to connect\"\n      |                                  |\n      |\u003C------- SYN-ACK (seq=y,ack=x+1)-|   \"Hello! I acknowledge, ready?\"\n      |                                  |\n      |-------- ACK (ack=y+1) ---------->|   \"Great, let us talk\"\n      |                                  |\n      |    (connection established)      |\n      |                                  |\n      |====== Data can now flow =========|\n",[22,969,970,975,980,985,989,994,998,1003,1007,1012,1016],{"__ignoreMap":51},[55,971,972],{"class":57,"line":58},[55,973,974],{},"Browser (Client)                      Server\n",[55,976,977],{"class":57,"line":64},[55,978,979],{},"      |                                  |\n",[55,981,982],{"class":57,"line":71},[55,983,984],{},"      |-------- SYN (seq=x) ----------->|   \"Hello, I want to connect\"\n",[55,986,987],{"class":57,"line":77},[55,988,979],{},[55,990,991],{"class":57,"line":83},[55,992,993],{},"      |\u003C------- SYN-ACK (seq=y,ack=x+1)-|   \"Hello! I acknowledge, ready?\"\n",[55,995,996],{"class":57,"line":89},[55,997,979],{},[55,999,1000],{"class":57,"line":95},[55,1001,1002],{},"      |-------- ACK (ack=y+1) ---------->|   \"Great, let us talk\"\n",[55,1004,1005],{"class":57,"line":101},[55,1006,979],{},[55,1008,1009],{"class":57,"line":107},[55,1010,1011],{},"      |    (connection established)      |\n",[55,1013,1014],{"class":57,"line":113},[55,1015,979],{},[55,1017,1018],{"class":57,"line":119},[55,1019,1020],{},"      |====== Data can now flow =========|\n",[18,1022,1023],{},"SYN means \"synchronize\". Each side sends its initial sequence number, and the other side acknowledges it. After this handshake, both sides are synchronized and data can flow. This roundtrip takes time, roughly one RTT (Round Trip Time), which might be 10ms on a local network or 150ms to a server on the other side of the world.",[343,1025,1027],{"id":1026},"why-not-just-use-udp","Why Not Just Use UDP?",[18,1029,1030],{},"UDP (User Datagram Protocol) is a simpler transport protocol. It sends packets without handshakes, without ordering guarantees, and without retransmission. It is faster but unreliable.",[18,1032,1033],{},"UDP is used when speed matters more than accuracy: video calls, live game updates, DNS queries. For web pages, you need every byte to arrive correctly, so TCP is the right choice (though HTTP/3 switches to QUIC, which is UDP-based but adds its own reliability layer).",[36,1035],{},[13,1037,1039],{"id":1038},"step-4-tls-the-encrypted-tunnel-for-https","Step 4: TLS, the Encrypted Tunnel (for HTTPS)",[18,1041,1042,1043,1046],{},"If the URL starts with ",[22,1044,1045],{},"https://",", the browser must now establish a secure, encrypted connection using TLS (Transport Layer Security). TLS runs on top of TCP and ensures that no one eavesdropping on the network can read or tamper with your data.",[300,1048,1049],{},[18,1050,1051,1054],{},[518,1052,1053],{},"HTTP vs HTTPS:"," HTTP sends everything as plain text. Any router or ISP between you and the server can read it. HTTPS encrypts the entire conversation. Always use HTTPS.",[343,1056,1058],{"id":1057},"the-tls-handshake","The TLS Handshake",[18,1060,1061],{},"The TLS handshake is more complex than TCP's, but the goal is clear: agree on encryption settings and verify the server's identity, all without ever sending a secret key over the wire.",[46,1063,1065],{"className":48,"code":1064,"language":50,"meta":51,"style":51},"Browser (Client)                            Server\n      |                                        |\n      |-- ClientHello ------------------------>|\n      |   (TLS version, cipher suites,         |\n      |    random number A)                    |\n      |                                        |\n      |\u003C-- ServerHello ----------------------- |\n      |    (chosen cipher suite,               |\n      |     random number B,                   |\n      |     server's TLS certificate)          |\n      |                                        |\n      | [Browser validates certificate]        |\n      |  - Is it signed by a trusted CA?       |\n      |  - Does it match the domain?           |\n      |  - Is it expired?                      |\n      |                                        |\n      |-- Key Exchange ----------------------->|\n      |   (using server's public key to        |\n      |    encrypt a pre-master secret,        |\n      |    or via Diffie-Hellman key exchange) |\n      |                                        |\n      | [Both sides derive session keys        |\n      |  from random A + random B + secret]    |\n      |                                        |\n      |-- Finished (encrypted) -------------->|\n      |\u003C-- Finished (encrypted) --------------|\n      |                                        |\n      |===== All further data is encrypted ====|\n",[22,1066,1067,1072,1077,1082,1087,1092,1096,1101,1106,1111,1116,1120,1125,1130,1135,1140,1144,1149,1154,1159,1164,1168,1173,1178,1182,1187,1192,1196],{"__ignoreMap":51},[55,1068,1069],{"class":57,"line":58},[55,1070,1071],{},"Browser (Client)                            Server\n",[55,1073,1074],{"class":57,"line":64},[55,1075,1076],{},"      |                                        |\n",[55,1078,1079],{"class":57,"line":71},[55,1080,1081],{},"      |-- ClientHello ------------------------>|\n",[55,1083,1084],{"class":57,"line":77},[55,1085,1086],{},"      |   (TLS version, cipher suites,         |\n",[55,1088,1089],{"class":57,"line":83},[55,1090,1091],{},"      |    random number A)                    |\n",[55,1093,1094],{"class":57,"line":89},[55,1095,1076],{},[55,1097,1098],{"class":57,"line":95},[55,1099,1100],{},"      |\u003C-- ServerHello ----------------------- |\n",[55,1102,1103],{"class":57,"line":101},[55,1104,1105],{},"      |    (chosen cipher suite,               |\n",[55,1107,1108],{"class":57,"line":107},[55,1109,1110],{},"      |     random number B,                   |\n",[55,1112,1113],{"class":57,"line":113},[55,1114,1115],{},"      |     server's TLS certificate)          |\n",[55,1117,1118],{"class":57,"line":119},[55,1119,1076],{},[55,1121,1122],{"class":57,"line":125},[55,1123,1124],{},"      | [Browser validates certificate]        |\n",[55,1126,1127],{"class":57,"line":131},[55,1128,1129],{},"      |  - Is it signed by a trusted CA?       |\n",[55,1131,1132],{"class":57,"line":137},[55,1133,1134],{},"      |  - Does it match the domain?           |\n",[55,1136,1137],{"class":57,"line":143},[55,1138,1139],{},"      |  - Is it expired?                      |\n",[55,1141,1142],{"class":57,"line":149},[55,1143,1076],{},[55,1145,1146],{"class":57,"line":441},[55,1147,1148],{},"      |-- Key Exchange ----------------------->|\n",[55,1150,1151],{"class":57,"line":447},[55,1152,1153],{},"      |   (using server's public key to        |\n",[55,1155,1156],{"class":57,"line":453},[55,1157,1158],{},"      |    encrypt a pre-master secret,        |\n",[55,1160,1161],{"class":57,"line":459},[55,1162,1163],{},"      |    or via Diffie-Hellman key exchange) |\n",[55,1165,1166],{"class":57,"line":464},[55,1167,1076],{},[55,1169,1170],{"class":57,"line":470},[55,1171,1172],{},"      | [Both sides derive session keys        |\n",[55,1174,1175],{"class":57,"line":475},[55,1176,1177],{},"      |  from random A + random B + secret]    |\n",[55,1179,1180],{"class":57,"line":481},[55,1181,1076],{},[55,1183,1184],{"class":57,"line":486},[55,1185,1186],{},"      |-- Finished (encrypted) -------------->|\n",[55,1188,1189],{"class":57,"line":492},[55,1190,1191],{},"      |\u003C-- Finished (encrypted) --------------|\n",[55,1193,1194],{"class":57,"line":497},[55,1195,1076],{},[55,1197,1198],{"class":57,"line":503},[55,1199,1200],{},"      |===== All further data is encrypted ====|\n",[343,1202,1204],{"id":1203},"tls-certificates-proving-identity","TLS Certificates: Proving Identity",[18,1206,1207,1208,1210],{},"When a server presents a TLS certificate, it is saying: \"I am ",[22,1209,243],{},", and here is my signed proof.\" The proof is a digital signature from a Certificate Authority (CA), a trusted third party like Let's Encrypt, DigiCert, or Comodo.",[18,1212,1213],{},"Your browser comes pre-installed with a list of trusted root CAs. If the server's certificate is signed (directly or through a chain) by one of these CAs, the browser trusts it. If not, you see that scary red warning.",[46,1215,1217],{"className":48,"code":1216,"language":50,"meta":51,"style":51},"Certificate Chain:\n\n  Root CA (pre-trusted by browser)\n      |\n      +--> Intermediate CA (signed by Root)\n                |\n                +--> example.com certificate (signed by Intermediate)\n",[22,1218,1219,1224,1228,1233,1238,1243,1248],{"__ignoreMap":51},[55,1220,1221],{"class":57,"line":58},[55,1222,1223],{},"Certificate Chain:\n",[55,1225,1226],{"class":57,"line":64},[55,1227,68],{"emptyLinePlaceholder":67},[55,1229,1230],{"class":57,"line":71},[55,1231,1232],{},"  Root CA (pre-trusted by browser)\n",[55,1234,1235],{"class":57,"line":77},[55,1236,1237],{},"      |\n",[55,1239,1240],{"class":57,"line":83},[55,1241,1242],{},"      +--> Intermediate CA (signed by Root)\n",[55,1244,1245],{"class":57,"line":89},[55,1246,1247],{},"                |\n",[55,1249,1250],{"class":57,"line":95},[55,1251,1252],{},"                +--> example.com certificate (signed by Intermediate)\n",[18,1254,1255],{},"Browsers do not trust the end certificate directly. They walk the chain up to a root they already trust.",[343,1257,1259],{"id":1258},"what-tls-protects-against","What TLS Protects Against",[186,1261,1262,1274],{},[189,1263,1264],{},[192,1265,1266,1269,1271],{},[195,1267,1268],{},"Threat",[195,1270,203],{},[195,1272,1273],{},"How TLS helps",[205,1275,1276,1287,1298],{},[192,1277,1278,1281,1284],{},[210,1279,1280],{},"Eavesdropping",[210,1282,1283],{},"Someone reads your data in transit",[210,1285,1286],{},"Encryption makes data unreadable",[192,1288,1289,1292,1295],{},[210,1290,1291],{},"Tampering",[210,1293,1294],{},"Someone modifies data in transit",[210,1296,1297],{},"MAC (Message Auth Code) detects changes",[192,1299,1300,1303,1306],{},[210,1301,1302],{},"Impersonation",[210,1304,1305],{},"A fake server pretends to be your bank",[210,1307,1308],{},"Certificate verification proves identity",[18,1310,1311],{},"After the TLS handshake, the browser and server share a symmetric encryption key (fast, unlike asymmetric encryption). All HTTP traffic from this point is encrypted using that key.",[36,1313],{},[13,1315,1317],{"id":1316},"step-5-the-http-request","Step 5: The HTTP Request",[18,1319,1320,1321,532],{},"With a TCP connection established and TLS negotiated, the browser is finally ready to ask for the page. It sends an ",[518,1322,1323],{},"HTTP request",[343,1325,1327],{"id":1326},"http-request-structure","HTTP Request Structure",[18,1329,1330],{},"An HTTP request has three parts: a request line, headers, and an optional body.",[46,1332,1336],{"className":1333,"code":1334,"language":1335,"meta":51,"style":51},"language-http shiki shiki-themes github-light","GET /blog/article?id=42&lang=en HTTP/1.1\nHost: www.example.com\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\nAccept-Language: en-US,en;q=0.5\nAccept-Encoding: gzip, deflate, br\nConnection: keep-alive\nCookie: session_id=abc123; theme=dark\nCache-Control: max-age=0\n","http",[22,1337,1338,1343,1348,1353,1358,1363,1368,1373,1378],{"__ignoreMap":51},[55,1339,1340],{"class":57,"line":58},[55,1341,1342],{},"GET /blog/article?id=42&lang=en HTTP/1.1\n",[55,1344,1345],{"class":57,"line":64},[55,1346,1347],{},"Host: www.example.com\n",[55,1349,1350],{"class":57,"line":71},[55,1351,1352],{},"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...\n",[55,1354,1355],{"class":57,"line":77},[55,1356,1357],{},"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n",[55,1359,1360],{"class":57,"line":83},[55,1361,1362],{},"Accept-Language: en-US,en;q=0.5\n",[55,1364,1365],{"class":57,"line":89},[55,1366,1367],{},"Accept-Encoding: gzip, deflate, br\n",[55,1369,1370],{"class":57,"line":95},[55,1371,1372],{},"Connection: keep-alive\n",[55,1374,1375],{"class":57,"line":101},[55,1376,1377],{},"Cookie: session_id=abc123; theme=dark\n",[55,1379,1380],{"class":57,"line":107},[55,1381,1382],{},"Cache-Control: max-age=0\n",[18,1384,1385],{},"Let us break this down line by line:",[18,1387,1388,1391,1392],{},[518,1389,1390],{},"Request Line:"," ",[22,1393,1394],{},"GET /blog/article?id=42&lang=en HTTP/1.1",[1396,1397,1398,1405,1411],"ul",{},[1399,1400,1401,1404],"li",{},[22,1402,1403],{},"GET",": The HTTP method (see methods below)",[1399,1406,1407,1410],{},[22,1408,1409],{},"/blog/article?id=42&lang=en",": The path + query string",[1399,1412,1413,1416],{},[22,1414,1415],{},"HTTP/1.1",": The protocol version",[18,1418,1419,1422,1423,1426],{},[518,1420,1421],{},"Host Header:"," The domain name. This is crucial because one server can host hundreds of different websites on the same IP address. The ",[22,1424,1425],{},"Host"," header tells the server which website you want. This is called virtual hosting.",[18,1428,1429,1432],{},[518,1430,1431],{},"User-Agent:"," Identifies the browser and OS. Servers can use this to send different content to mobile vs desktop browsers (though responsive CSS is preferred).",[18,1434,1435,1438],{},[518,1436,1437],{},"Accept:"," Tells the server what types of content the browser understands, in order of preference.",[18,1440,1441,1444,1445,1448,1449,1452],{},[518,1442,1443],{},"Accept-Encoding:"," The browser advertises which compression algorithms it supports. Servers typically respond with ",[22,1446,1447],{},"gzip"," or ",[22,1450,1451],{},"br"," (Brotli) compressed content, which can reduce file size by 70-90%.",[18,1454,1455,1458],{},[518,1456,1457],{},"Cookie:"," Cookies are sent with every request to the domain. This is how the server recognizes returning users and maintains sessions.",[18,1460,1461,1464,1465,1468],{},[518,1462,1463],{},"Cache-Control:"," Hints about caching behavior. ",[22,1466,1467],{},"max-age=0"," tells the server the browser wants a fresh copy.",[343,1470,1472],{"id":1471},"http-methods","HTTP Methods",[18,1474,1475],{},"Different methods have different meanings and different semantics:",[186,1477,1478,1496],{},[189,1479,1480],{},[192,1481,1482,1485,1487,1490,1493],{},[195,1483,1484],{},"Method",[195,1486,582],{},[195,1488,1489],{},"Has Body?",[195,1491,1492],{},"Safe?",[195,1494,1495],{},"Idempotent?",[205,1497,1498,1515,1531,1547,1563,1580,1596],{},[192,1499,1500,1504,1507,1510,1513],{},[210,1501,1502],{},[22,1503,1403],{},[210,1505,1506],{},"Retrieve a resource",[210,1508,1509],{},"No",[210,1511,1512],{},"Yes",[210,1514,1512],{},[192,1516,1517,1522,1525,1527,1529],{},[210,1518,1519],{},[22,1520,1521],{},"POST",[210,1523,1524],{},"Create a new resource",[210,1526,1512],{},[210,1528,1509],{},[210,1530,1509],{},[192,1532,1533,1538,1541,1543,1545],{},[210,1534,1535],{},[22,1536,1537],{},"PUT",[210,1539,1540],{},"Replace a resource entirely",[210,1542,1512],{},[210,1544,1509],{},[210,1546,1512],{},[192,1548,1549,1554,1557,1559,1561],{},[210,1550,1551],{},[22,1552,1553],{},"PATCH",[210,1555,1556],{},"Partially update a resource",[210,1558,1512],{},[210,1560,1509],{},[210,1562,1509],{},[192,1564,1565,1570,1573,1576,1578],{},[210,1566,1567],{},[22,1568,1569],{},"DELETE",[210,1571,1572],{},"Delete a resource",[210,1574,1575],{},"Sometimes",[210,1577,1509],{},[210,1579,1512],{},[192,1581,1582,1587,1590,1592,1594],{},[210,1583,1584],{},[22,1585,1586],{},"HEAD",[210,1588,1589],{},"Like GET but no response body",[210,1591,1509],{},[210,1593,1512],{},[210,1595,1512],{},[192,1597,1598,1603,1606,1608,1610],{},[210,1599,1600],{},[22,1601,1602],{},"OPTIONS",[210,1604,1605],{},"Ask what methods are allowed",[210,1607,1509],{},[210,1609,1512],{},[210,1611,1512],{},[18,1613,1614,1617,1618,1621,1622,1625],{},[518,1615,1616],{},"Safe"," means the method does not change server state (only reads data).\n",[518,1619,1620],{},"Idempotent"," means calling the same method multiple times has the same result as calling it once. ",[22,1623,1624],{},"DELETE /users/42"," twice should not error the second time (the user is already gone).",[343,1627,1629],{"id":1628},"a-post-request-with-a-body","A POST Request with a Body",[18,1631,1632,1633,1635],{},"When you submit a login form, the browser sends a ",[22,1634,1521],{}," request with a body:",[46,1637,1639],{"className":1333,"code":1638,"language":1335,"meta":51,"style":51},"POST /api/login HTTP/1.1\nHost: www.example.com\nContent-Type: application/json\nContent-Length: 47\nAccept: application/json\n\n{\"email\": \"user@example.com\", \"password\": \"...\"}\n",[22,1640,1641,1646,1650,1655,1660,1665,1669],{"__ignoreMap":51},[55,1642,1643],{"class":57,"line":58},[55,1644,1645],{},"POST /api/login HTTP/1.1\n",[55,1647,1648],{"class":57,"line":64},[55,1649,1347],{},[55,1651,1652],{"class":57,"line":71},[55,1653,1654],{},"Content-Type: application/json\n",[55,1656,1657],{"class":57,"line":77},[55,1658,1659],{},"Content-Length: 47\n",[55,1661,1662],{"class":57,"line":83},[55,1663,1664],{},"Accept: application/json\n",[55,1666,1667],{"class":57,"line":89},[55,1668,68],{"emptyLinePlaceholder":67},[55,1670,1671],{"class":57,"line":95},[55,1672,1673],{},"{\"email\": \"user@example.com\", \"password\": \"...\"}\n",[18,1675,1676],{},"The blank line between headers and body is mandatory in HTTP. It tells the parser \"headers are done, body starts here.\"",[343,1678,1680],{"id":1679},"http11-vs-http2-vs-http3","HTTP/1.1 vs HTTP/2 vs HTTP/3",[186,1682,1683,1696],{},[189,1684,1685],{},[192,1686,1687,1690,1693],{},[195,1688,1689],{},"Version",[195,1691,1692],{},"Year",[195,1694,1695],{},"Key Feature",[205,1697,1698,1709,1719,1730],{},[192,1699,1700,1703,1706],{},[210,1701,1702],{},"HTTP/1.0",[210,1704,1705],{},"1996",[210,1707,1708],{},"One request per connection",[192,1710,1711,1713,1716],{},[210,1712,1415],{},[210,1714,1715],{},"1997",[210,1717,1718],{},"Keep-alive (reuse connection), pipelining",[192,1720,1721,1724,1727],{},[210,1722,1723],{},"HTTP/2",[210,1725,1726],{},"2015",[210,1728,1729],{},"Multiplexing (multiple requests on one connection), header compression, server push",[192,1731,1732,1735,1738],{},[210,1733,1734],{},"HTTP/3",[210,1736,1737],{},"2022",[210,1739,1740],{},"Built on QUIC/UDP, 0-RTT connection setup, no head-of-line blocking",[18,1742,1743],{},"HTTP/2 multiplexing is a big deal. With HTTP/1.1, if you have 30 CSS files, images, and scripts to load, browsers open 6 parallel TCP connections per domain and pipeline requests. HTTP/2 handles all 30 over a single connection with no waiting.",[36,1745],{},[13,1747,1749],{"id":1748},"step-6-the-server-receives-the-request","Step 6: The Server Receives the Request",[18,1751,1752,1753,1755],{},"The request has traveled across the internet, through routers, across fiber optic cables or wireless networks, and has arrived at a server running at IP address ",[22,1754,337],{},". What happens on the server side?",[343,1757,1759],{"id":1758},"the-web-server","The Web Server",[18,1761,1762,1763,1766],{},"The first program that receives your request is a ",[518,1764,1765],{},"web server",": software like Nginx, Apache, or Caddy. Think of the web server as a receptionist in a large office building. It handles the initial connection, reads the request, and decides who should handle it.",[46,1768,1770],{"className":48,"code":1769,"language":50,"meta":51,"style":51},"Incoming request\n      |\n      v\n+-----------+\n|   Nginx   |  (Web Server / Reverse Proxy)\n|           |\n|  - Handles TLS termination\n|  - Serves static files directly (HTML, CSS, images)\n|  - Proxies dynamic requests to app server\n+-----------+\n      |\n      | (if dynamic content needed)\n      v\n+------------------+\n|  App Server      |  (Node.js, PHP-FPM, Gunicorn, etc.)\n|                  |\n|  - Runs your code\n|  - Handles business logic\n|  - Queries database\n+------------------+\n      |\n      v\n+------------------+\n|  Database        |  (PostgreSQL, MySQL, Redis, etc.)\n+------------------+\n",[22,1771,1772,1777,1781,1786,1791,1796,1801,1806,1811,1816,1820,1824,1829,1833,1838,1843,1848,1853,1858,1863,1867,1871,1875,1879,1884],{"__ignoreMap":51},[55,1773,1774],{"class":57,"line":58},[55,1775,1776],{},"Incoming request\n",[55,1778,1779],{"class":57,"line":64},[55,1780,1237],{},[55,1782,1783],{"class":57,"line":71},[55,1784,1785],{},"      v\n",[55,1787,1788],{"class":57,"line":77},[55,1789,1790],{},"+-----------+\n",[55,1792,1793],{"class":57,"line":83},[55,1794,1795],{},"|   Nginx   |  (Web Server / Reverse Proxy)\n",[55,1797,1798],{"class":57,"line":89},[55,1799,1800],{},"|           |\n",[55,1802,1803],{"class":57,"line":95},[55,1804,1805],{},"|  - Handles TLS termination\n",[55,1807,1808],{"class":57,"line":101},[55,1809,1810],{},"|  - Serves static files directly (HTML, CSS, images)\n",[55,1812,1813],{"class":57,"line":107},[55,1814,1815],{},"|  - Proxies dynamic requests to app server\n",[55,1817,1818],{"class":57,"line":113},[55,1819,1790],{},[55,1821,1822],{"class":57,"line":119},[55,1823,1237],{},[55,1825,1826],{"class":57,"line":125},[55,1827,1828],{},"      | (if dynamic content needed)\n",[55,1830,1831],{"class":57,"line":131},[55,1832,1785],{},[55,1834,1835],{"class":57,"line":137},[55,1836,1837],{},"+------------------+\n",[55,1839,1840],{"class":57,"line":143},[55,1841,1842],{},"|  App Server      |  (Node.js, PHP-FPM, Gunicorn, etc.)\n",[55,1844,1845],{"class":57,"line":149},[55,1846,1847],{},"|                  |\n",[55,1849,1850],{"class":57,"line":441},[55,1851,1852],{},"|  - Runs your code\n",[55,1854,1855],{"class":57,"line":447},[55,1856,1857],{},"|  - Handles business logic\n",[55,1859,1860],{"class":57,"line":453},[55,1861,1862],{},"|  - Queries database\n",[55,1864,1865],{"class":57,"line":459},[55,1866,1837],{},[55,1868,1869],{"class":57,"line":464},[55,1870,1237],{},[55,1872,1873],{"class":57,"line":470},[55,1874,1785],{},[55,1876,1877],{"class":57,"line":475},[55,1878,1837],{},[55,1880,1881],{"class":57,"line":481},[55,1882,1883],{},"|  Database        |  (PostgreSQL, MySQL, Redis, etc.)\n",[55,1885,1886],{"class":57,"line":486},[55,1887,1837],{},[18,1889,1890,1893],{},[518,1891,1892],{},"Static files"," are files that are the same for every user: images, fonts, CSS files, downloaded PDFs. Nginx can serve these directly from disk without touching your application code. This is extremely fast.",[18,1895,1896,1899],{},[518,1897,1898],{},"Dynamic content"," is content that changes per user or per request: your inbox page, a product page with live inventory, an API response with your profile data. For these, Nginx forwards the request to the application server.",[343,1901,1903],{"id":1902},"nginx-configuration-simplified","Nginx Configuration (Simplified)",[18,1905,1906],{},"Here is what a simplified Nginx config for a web application looks like:",[46,1908,1912],{"className":1909,"code":1910,"language":1911,"meta":51,"style":51},"language-nginx shiki shiki-themes github-light","server {\n    listen 443 ssl;\n    server_name example.com;\n\n    # Serve static files directly\n    location /static/ {\n        root /var/www/myapp;\n        expires 1y;\n        add_header Cache-Control \"public, immutable\";\n    }\n\n    # Forward everything else to Node.js app\n    location / {\n        proxy_pass http://127.0.0.1:3000;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}\n","nginx",[22,1913,1914,1919,1924,1929,1933,1938,1943,1948,1953,1958,1963,1967,1972,1977,1982,1987,1992,1997,2001],{"__ignoreMap":51},[55,1915,1916],{"class":57,"line":58},[55,1917,1918],{},"server {\n",[55,1920,1921],{"class":57,"line":64},[55,1922,1923],{},"    listen 443 ssl;\n",[55,1925,1926],{"class":57,"line":71},[55,1927,1928],{},"    server_name example.com;\n",[55,1930,1931],{"class":57,"line":77},[55,1932,68],{"emptyLinePlaceholder":67},[55,1934,1935],{"class":57,"line":83},[55,1936,1937],{},"    # Serve static files directly\n",[55,1939,1940],{"class":57,"line":89},[55,1941,1942],{},"    location /static/ {\n",[55,1944,1945],{"class":57,"line":95},[55,1946,1947],{},"        root /var/www/myapp;\n",[55,1949,1950],{"class":57,"line":101},[55,1951,1952],{},"        expires 1y;\n",[55,1954,1955],{"class":57,"line":107},[55,1956,1957],{},"        add_header Cache-Control \"public, immutable\";\n",[55,1959,1960],{"class":57,"line":113},[55,1961,1962],{},"    }\n",[55,1964,1965],{"class":57,"line":119},[55,1966,68],{"emptyLinePlaceholder":67},[55,1968,1969],{"class":57,"line":125},[55,1970,1971],{},"    # Forward everything else to Node.js app\n",[55,1973,1974],{"class":57,"line":131},[55,1975,1976],{},"    location / {\n",[55,1978,1979],{"class":57,"line":137},[55,1980,1981],{},"        proxy_pass http://127.0.0.1:3000;\n",[55,1983,1984],{"class":57,"line":143},[55,1985,1986],{},"        proxy_set_header Host $host;\n",[55,1988,1989],{"class":57,"line":149},[55,1990,1991],{},"        proxy_set_header X-Real-IP $remote_addr;\n",[55,1993,1994],{"class":57,"line":441},[55,1995,1996],{},"        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[55,1998,1999],{"class":57,"line":447},[55,2000,1962],{},[55,2002,2003],{"class":57,"line":453},[55,2004,2005],{},"}\n",[18,2007,2008,2009,2012],{},"Notice ",[22,2010,2011],{},"proxy_set_header X-Real-IP $remote_addr",". When Nginx forwards the request, the app server sees Nginx's IP, not the user's IP. This header passes the real client IP to the app so it can be logged or rate-limited correctly.",[343,2014,2016],{"id":2015},"the-application-server","The Application Server",[18,2018,2019],{},"This is where your code runs. The app server (Node.js, Python/Django, PHP/Laravel, Ruby on Rails, etc.) receives the forwarded request and executes your business logic.",[18,2021,2022],{},"Here is pseudo-code for what a typical request handler does:",[46,2024,2028],{"className":2025,"code":2026,"language":2027,"meta":51,"style":51},"language-javascript shiki shiki-themes github-light","// Pseudo-code: handling GET /blog/article?id=42\n\nasync function handleArticleRequest(request) {\n    const articleId = request.query.id;\n\n    // 1. Validate input\n    if (!articleId || !isInteger(articleId)) {\n        return response(400, { error: \"Invalid article ID\" });\n    }\n\n    // 2. Check cache (Redis) first\n    const cached = await redis.get(`article:${articleId}`);\n    if (cached) {\n        return response(200, JSON.parse(cached));\n    }\n\n    // 3. Query the database\n    const article = await db.query(\n        \"SELECT * FROM articles WHERE id = ? AND published = true\",\n        [articleId]\n    );\n\n    if (!article) {\n        return response(404, { error: \"Article not found\" });\n    }\n\n    // 4. Cache the result for 1 hour\n    await redis.setex(`article:${articleId}`, 3600, JSON.stringify(article));\n\n    // 5. Send the response\n    return response(200, article);\n}\n","javascript",[22,2029,2030,2035,2039,2062,2077,2081,2086,2112,2134,2138,2142,2147,2179,2186,2210,2214,2218,2223,2243,2251,2256,2261,2265,2276,2294,2298,2302,2307,2341,2345,2351,2366],{"__ignoreMap":51},[55,2031,2032],{"class":57,"line":58},[55,2033,2034],{"class":711},"// Pseudo-code: handling GET /blog/article?id=42\n",[55,2036,2037],{"class":57,"line":64},[55,2038,68],{"emptyLinePlaceholder":67},[55,2040,2041,2045,2048,2051,2055,2059],{"class":57,"line":71},[55,2042,2044],{"class":2043},"sD7c4","async",[55,2046,2047],{"class":2043}," function",[55,2049,2050],{"class":717}," handleArticleRequest",[55,2052,2054],{"class":2053},"sgsFI","(",[55,2056,2058],{"class":2057},"sqxcx","request",[55,2060,2061],{"class":2053},") {\n",[55,2063,2064,2067,2071,2074],{"class":57,"line":77},[55,2065,2066],{"class":2043},"    const",[55,2068,2070],{"class":2069},"sYu0t"," articleId",[55,2072,2073],{"class":2043}," =",[55,2075,2076],{"class":2053}," request.query.id;\n",[55,2078,2079],{"class":57,"line":83},[55,2080,68],{"emptyLinePlaceholder":67},[55,2082,2083],{"class":57,"line":89},[55,2084,2085],{"class":711},"    // 1. Validate input\n",[55,2087,2088,2091,2094,2097,2100,2103,2106,2109],{"class":57,"line":95},[55,2089,2090],{"class":2043},"    if",[55,2092,2093],{"class":2053}," (",[55,2095,2096],{"class":2043},"!",[55,2098,2099],{"class":2053},"articleId ",[55,2101,2102],{"class":2043},"||",[55,2104,2105],{"class":2043}," !",[55,2107,2108],{"class":717},"isInteger",[55,2110,2111],{"class":2053},"(articleId)) {\n",[55,2113,2114,2117,2120,2122,2125,2128,2131],{"class":57,"line":101},[55,2115,2116],{"class":2043},"        return",[55,2118,2119],{"class":717}," response",[55,2121,2054],{"class":2053},[55,2123,2124],{"class":2069},"400",[55,2126,2127],{"class":2053},", { error: ",[55,2129,2130],{"class":721},"\"Invalid article ID\"",[55,2132,2133],{"class":2053}," });\n",[55,2135,2136],{"class":57,"line":107},[55,2137,1962],{"class":2053},[55,2139,2140],{"class":57,"line":113},[55,2141,68],{"emptyLinePlaceholder":67},[55,2143,2144],{"class":57,"line":119},[55,2145,2146],{"class":711},"    // 2. Check cache (Redis) first\n",[55,2148,2149,2151,2154,2156,2159,2162,2165,2167,2170,2173,2176],{"class":57,"line":125},[55,2150,2066],{"class":2043},[55,2152,2153],{"class":2069}," cached",[55,2155,2073],{"class":2043},[55,2157,2158],{"class":2043}," await",[55,2160,2161],{"class":2053}," redis.",[55,2163,2164],{"class":717},"get",[55,2166,2054],{"class":2053},[55,2168,2169],{"class":721},"`article:${",[55,2171,2172],{"class":2053},"articleId",[55,2174,2175],{"class":721},"}`",[55,2177,2178],{"class":2053},");\n",[55,2180,2181,2183],{"class":57,"line":131},[55,2182,2090],{"class":2043},[55,2184,2185],{"class":2053}," (cached) {\n",[55,2187,2188,2190,2192,2194,2197,2199,2202,2204,2207],{"class":57,"line":137},[55,2189,2116],{"class":2043},[55,2191,2119],{"class":717},[55,2193,2054],{"class":2053},[55,2195,2196],{"class":2069},"200",[55,2198,525],{"class":2053},[55,2200,2201],{"class":2069},"JSON",[55,2203,532],{"class":2053},[55,2205,2206],{"class":717},"parse",[55,2208,2209],{"class":2053},"(cached));\n",[55,2211,2212],{"class":57,"line":143},[55,2213,1962],{"class":2053},[55,2215,2216],{"class":57,"line":149},[55,2217,68],{"emptyLinePlaceholder":67},[55,2219,2220],{"class":57,"line":441},[55,2221,2222],{"class":711},"    // 3. Query the database\n",[55,2224,2225,2227,2230,2232,2234,2237,2240],{"class":57,"line":447},[55,2226,2066],{"class":2043},[55,2228,2229],{"class":2069}," article",[55,2231,2073],{"class":2043},[55,2233,2158],{"class":2043},[55,2235,2236],{"class":2053}," db.",[55,2238,2239],{"class":717},"query",[55,2241,2242],{"class":2053},"(\n",[55,2244,2245,2248],{"class":57,"line":453},[55,2246,2247],{"class":721},"        \"SELECT * FROM articles WHERE id = ? AND published = true\"",[55,2249,2250],{"class":2053},",\n",[55,2252,2253],{"class":57,"line":459},[55,2254,2255],{"class":2053},"        [articleId]\n",[55,2257,2258],{"class":57,"line":464},[55,2259,2260],{"class":2053},"    );\n",[55,2262,2263],{"class":57,"line":470},[55,2264,68],{"emptyLinePlaceholder":67},[55,2266,2267,2269,2271,2273],{"class":57,"line":475},[55,2268,2090],{"class":2043},[55,2270,2093],{"class":2053},[55,2272,2096],{"class":2043},[55,2274,2275],{"class":2053},"article) {\n",[55,2277,2278,2280,2282,2284,2287,2289,2292],{"class":57,"line":481},[55,2279,2116],{"class":2043},[55,2281,2119],{"class":717},[55,2283,2054],{"class":2053},[55,2285,2286],{"class":2069},"404",[55,2288,2127],{"class":2053},[55,2290,2291],{"class":721},"\"Article not found\"",[55,2293,2133],{"class":2053},[55,2295,2296],{"class":57,"line":486},[55,2297,1962],{"class":2053},[55,2299,2300],{"class":57,"line":492},[55,2301,68],{"emptyLinePlaceholder":67},[55,2303,2304],{"class":57,"line":497},[55,2305,2306],{"class":711},"    // 4. Cache the result for 1 hour\n",[55,2308,2309,2312,2314,2317,2319,2321,2323,2325,2327,2329,2331,2333,2335,2338],{"class":57,"line":503},[55,2310,2311],{"class":2043},"    await",[55,2313,2161],{"class":2053},[55,2315,2316],{"class":717},"setex",[55,2318,2054],{"class":2053},[55,2320,2169],{"class":721},[55,2322,2172],{"class":2053},[55,2324,2175],{"class":721},[55,2326,525],{"class":2053},[55,2328,691],{"class":2069},[55,2330,525],{"class":2053},[55,2332,2201],{"class":2069},[55,2334,532],{"class":2053},[55,2336,2337],{"class":717},"stringify",[55,2339,2340],{"class":2053},"(article));\n",[55,2342,2343],{"class":57,"line":508},[55,2344,68],{"emptyLinePlaceholder":67},[55,2346,2348],{"class":57,"line":2347},30,[55,2349,2350],{"class":711},"    // 5. Send the response\n",[55,2352,2354,2357,2359,2361,2363],{"class":57,"line":2353},31,[55,2355,2356],{"class":2043},"    return",[55,2358,2119],{"class":717},[55,2360,2054],{"class":2053},[55,2362,2196],{"class":2069},[55,2364,2365],{"class":2053},", article);\n",[55,2367,2369],{"class":57,"line":2368},32,[55,2370,2005],{"class":2053},[18,2372,2373],{},"Notice the caching step. Databases are relatively slow for read-heavy workloads. A cache like Redis stores frequently-read data in memory, where reads take microseconds instead of milliseconds. Well-designed applications serve the majority of read requests from cache, dramatically reducing database load.",[343,2375,2377],{"id":2376},"the-database-query","The Database Query",[18,2379,2380],{},"When the cache misses, the app talks to the database. A database server (PostgreSQL, MySQL, SQLite, MongoDB) stores data on disk, maintains indexes for fast lookups, and processes SQL queries.",[46,2382,2386],{"className":2383,"code":2384,"language":2385,"meta":51,"style":51},"language-sql shiki shiki-themes github-light","SELECT\n    a.id,\n    a.title,\n    a.body,\n    a.published_at,\n    u.name AS author_name\nFROM articles a\nJOIN users u ON u.id = a.author_id\nWHERE a.id = 42\n  AND a.published = true;\n","sql",[22,2387,2388,2393,2405,2416,2427,2438,2454,2462,2490,2506],{"__ignoreMap":51},[55,2389,2390],{"class":57,"line":58},[55,2391,2392],{"class":2043},"SELECT\n",[55,2394,2395,2398,2400,2403],{"class":57,"line":64},[55,2396,2397],{"class":2069},"    a",[55,2399,532],{"class":2053},[55,2401,2402],{"class":2069},"id",[55,2404,2250],{"class":2053},[55,2406,2407,2409,2411,2414],{"class":57,"line":71},[55,2408,2397],{"class":2069},[55,2410,532],{"class":2053},[55,2412,2413],{"class":2069},"title",[55,2415,2250],{"class":2053},[55,2417,2418,2420,2422,2425],{"class":57,"line":77},[55,2419,2397],{"class":2069},[55,2421,532],{"class":2053},[55,2423,2424],{"class":2069},"body",[55,2426,2250],{"class":2053},[55,2428,2429,2431,2433,2436],{"class":57,"line":83},[55,2430,2397],{"class":2069},[55,2432,532],{"class":2053},[55,2434,2435],{"class":2069},"published_at",[55,2437,2250],{"class":2053},[55,2439,2440,2443,2445,2448,2451],{"class":57,"line":89},[55,2441,2442],{"class":2069},"    u",[55,2444,532],{"class":2053},[55,2446,2447],{"class":2069},"name",[55,2449,2450],{"class":2043}," AS",[55,2452,2453],{"class":2053}," author_name\n",[55,2455,2456,2459],{"class":57,"line":95},[55,2457,2458],{"class":2043},"FROM",[55,2460,2461],{"class":2053}," articles a\n",[55,2463,2464,2467,2470,2473,2476,2478,2480,2482,2485,2487],{"class":57,"line":101},[55,2465,2466],{"class":2043},"JOIN",[55,2468,2469],{"class":2053}," users u ",[55,2471,2472],{"class":2043},"ON",[55,2474,2475],{"class":2069}," u",[55,2477,532],{"class":2053},[55,2479,2402],{"class":2069},[55,2481,2073],{"class":2043},[55,2483,2484],{"class":2069}," a",[55,2486,532],{"class":2053},[55,2488,2489],{"class":2069},"author_id\n",[55,2491,2492,2495,2497,2499,2501,2503],{"class":57,"line":107},[55,2493,2494],{"class":2043},"WHERE",[55,2496,2484],{"class":2069},[55,2498,532],{"class":2053},[55,2500,2402],{"class":2069},[55,2502,2073],{"class":2043},[55,2504,2505],{"class":2069}," 42\n",[55,2507,2508,2511,2513,2515,2518,2520],{"class":57,"line":113},[55,2509,2510],{"class":2043},"  AND",[55,2512,2484],{"class":2069},[55,2514,532],{"class":2053},[55,2516,2517],{"class":2069},"published",[55,2519,2073],{"class":2043},[55,2521,2522],{"class":2053}," true;\n",[18,2524,2525],{},"The database engine:",[2527,2528,2529,2532,2535,2538],"ol",{},[1399,2530,2531],{},"Parses the SQL statement",[1399,2533,2534],{},"Plans the most efficient query execution path (using indexes where available)",[1399,2536,2537],{},"Reads the relevant rows from disk (or from its own buffer cache in memory)",[1399,2539,2540],{},"Returns the result set to the application",[18,2542,2543,2544,2547],{},"Indexes are critical for performance. Without an index on ",[22,2545,2546],{},"articles.id",", the database would scan every single row in the table to find article #42. With an index (which most primary key columns have automatically), it jumps directly to the right row in microseconds.",[36,2549],{},[13,2551,2553],{"id":2552},"step-7-the-http-response","Step 7: The HTTP Response",[18,2555,2556],{},"After processing the request, the server sends back an HTTP response. Like the request, it has a structure: a status line, headers, and a body.",[343,2558,2560],{"id":2559},"http-response-structure","HTTP Response Structure",[46,2562,2564],{"className":1333,"code":2563,"language":1335,"meta":51,"style":51},"HTTP/1.1 200 OK\nContent-Type: text/html; charset=UTF-8\nContent-Encoding: gzip\nContent-Length: 12840\nCache-Control: public, max-age=3600\nETag: \"abc123def456\"\nLast-Modified: Sun, 20 Apr 2026 10:00:00 GMT\nSet-Cookie: session_id=xyz789; HttpOnly; Secure; SameSite=Strict\nX-Request-ID: req_a1b2c3d4\nStrict-Transport-Security: max-age=31536000; includeSubDomains\n\n\u003C!DOCTYPE html>\n\u003Chtml>\n...\n\u003C/html>\n",[22,2565,2566,2571,2576,2581,2586,2591,2596,2601,2606,2611,2616,2620,2625,2630,2635],{"__ignoreMap":51},[55,2567,2568],{"class":57,"line":58},[55,2569,2570],{},"HTTP/1.1 200 OK\n",[55,2572,2573],{"class":57,"line":64},[55,2574,2575],{},"Content-Type: text/html; charset=UTF-8\n",[55,2577,2578],{"class":57,"line":71},[55,2579,2580],{},"Content-Encoding: gzip\n",[55,2582,2583],{"class":57,"line":77},[55,2584,2585],{},"Content-Length: 12840\n",[55,2587,2588],{"class":57,"line":83},[55,2589,2590],{},"Cache-Control: public, max-age=3600\n",[55,2592,2593],{"class":57,"line":89},[55,2594,2595],{},"ETag: \"abc123def456\"\n",[55,2597,2598],{"class":57,"line":95},[55,2599,2600],{},"Last-Modified: Sun, 20 Apr 2026 10:00:00 GMT\n",[55,2602,2603],{"class":57,"line":101},[55,2604,2605],{},"Set-Cookie: session_id=xyz789; HttpOnly; Secure; SameSite=Strict\n",[55,2607,2608],{"class":57,"line":107},[55,2609,2610],{},"X-Request-ID: req_a1b2c3d4\n",[55,2612,2613],{"class":57,"line":113},[55,2614,2615],{},"Strict-Transport-Security: max-age=31536000; includeSubDomains\n",[55,2617,2618],{"class":57,"line":119},[55,2619,68],{"emptyLinePlaceholder":67},[55,2621,2622],{"class":57,"line":125},[55,2623,2624],{},"\u003C!DOCTYPE html>\n",[55,2626,2627],{"class":57,"line":131},[55,2628,2629],{},"\u003Chtml>\n",[55,2631,2632],{"class":57,"line":137},[55,2633,2634],{},"...\n",[55,2636,2637],{"class":57,"line":143},[55,2638,2639],{},"\u003C/html>\n",[343,2641,2643],{"id":2642},"http-status-codes","HTTP Status Codes",[18,2645,2646],{},"The three-digit status code tells the browser whether the request succeeded and what kind of result to expect:",[46,2648,2650],{"className":48,"code":2649,"language":50,"meta":51,"style":51},"1xx  Informational\n  100 Continue            : server got headers, client should send body\n\n2xx  Success\n  200 OK                  : standard success\n  201 Created             : resource was created (after POST)\n  204 No Content          : success but no body (after DELETE)\n\n3xx  Redirection\n  301 Moved Permanently   : URL changed forever (update your bookmarks/links)\n  302 Found               : temporary redirect\n  304 Not Modified        : cached version is still fresh (no body sent)\n\n4xx  Client Errors\n  400 Bad Request         : malformed request\n  401 Unauthorized        : authentication required\n  403 Forbidden           : authenticated but not allowed\n  404 Not Found           : resource does not exist\n  429 Too Many Requests   : rate limited\n\n5xx  Server Errors\n  500 Internal Server Error   : something crashed on the server\n  502 Bad Gateway             : Nginx could not reach the app server\n  503 Service Unavailable     : server is overloaded or down\n  504 Gateway Timeout         : app server took too long to respond\n",[22,2651,2652,2657,2662,2666,2671,2676,2681,2686,2690,2695,2700,2705,2710,2714,2719,2724,2729,2734,2739,2744,2748,2753,2758,2763,2768],{"__ignoreMap":51},[55,2653,2654],{"class":57,"line":58},[55,2655,2656],{},"1xx  Informational\n",[55,2658,2659],{"class":57,"line":64},[55,2660,2661],{},"  100 Continue            : server got headers, client should send body\n",[55,2663,2664],{"class":57,"line":71},[55,2665,68],{"emptyLinePlaceholder":67},[55,2667,2668],{"class":57,"line":77},[55,2669,2670],{},"2xx  Success\n",[55,2672,2673],{"class":57,"line":83},[55,2674,2675],{},"  200 OK                  : standard success\n",[55,2677,2678],{"class":57,"line":89},[55,2679,2680],{},"  201 Created             : resource was created (after POST)\n",[55,2682,2683],{"class":57,"line":95},[55,2684,2685],{},"  204 No Content          : success but no body (after DELETE)\n",[55,2687,2688],{"class":57,"line":101},[55,2689,68],{"emptyLinePlaceholder":67},[55,2691,2692],{"class":57,"line":107},[55,2693,2694],{},"3xx  Redirection\n",[55,2696,2697],{"class":57,"line":113},[55,2698,2699],{},"  301 Moved Permanently   : URL changed forever (update your bookmarks/links)\n",[55,2701,2702],{"class":57,"line":119},[55,2703,2704],{},"  302 Found               : temporary redirect\n",[55,2706,2707],{"class":57,"line":125},[55,2708,2709],{},"  304 Not Modified        : cached version is still fresh (no body sent)\n",[55,2711,2712],{"class":57,"line":131},[55,2713,68],{"emptyLinePlaceholder":67},[55,2715,2716],{"class":57,"line":137},[55,2717,2718],{},"4xx  Client Errors\n",[55,2720,2721],{"class":57,"line":143},[55,2722,2723],{},"  400 Bad Request         : malformed request\n",[55,2725,2726],{"class":57,"line":149},[55,2727,2728],{},"  401 Unauthorized        : authentication required\n",[55,2730,2731],{"class":57,"line":441},[55,2732,2733],{},"  403 Forbidden           : authenticated but not allowed\n",[55,2735,2736],{"class":57,"line":447},[55,2737,2738],{},"  404 Not Found           : resource does not exist\n",[55,2740,2741],{"class":57,"line":453},[55,2742,2743],{},"  429 Too Many Requests   : rate limited\n",[55,2745,2746],{"class":57,"line":459},[55,2747,68],{"emptyLinePlaceholder":67},[55,2749,2750],{"class":57,"line":464},[55,2751,2752],{},"5xx  Server Errors\n",[55,2754,2755],{"class":57,"line":470},[55,2756,2757],{},"  500 Internal Server Error   : something crashed on the server\n",[55,2759,2760],{"class":57,"line":475},[55,2761,2762],{},"  502 Bad Gateway             : Nginx could not reach the app server\n",[55,2764,2765],{"class":57,"line":481},[55,2766,2767],{},"  503 Service Unavailable     : server is overloaded or down\n",[55,2769,2770],{"class":57,"line":486},[55,2771,2772],{},"  504 Gateway Timeout         : app server took too long to respond\n",[18,2774,2775],{},"The difference between 401 and 403 trips many developers up:",[1396,2777,2778,2784],{},[1399,2779,2780,2783],{},[518,2781,2782],{},"401:"," \"Who are you? Please log in first.\"",[1399,2785,2786,2789],{},[518,2787,2788],{},"403:"," \"I know who you are. You are just not allowed to do this.\"",[343,2791,2793],{"id":2792},"important-response-headers","Important Response Headers",[18,2795,2796,2799,2800,2803,2804,2807,2808,2811],{},[518,2797,2798],{},"Content-Type:"," Tells the browser how to interpret the body. ",[22,2801,2802],{},"text/html"," means render as HTML. ",[22,2805,2806],{},"application/json"," means parse as JSON. ",[22,2809,2810],{},"image/png"," means display as an image. Getting this wrong causes browsers to display raw text instead of rendering a page.",[18,2813,2814,2816],{},[518,2815,1463],{}," Controls caching behavior for both browsers and CDNs:",[46,2818,2820],{"className":48,"code":2819,"language":50,"meta":51,"style":51},"Cache-Control: public, max-age=86400\n  (anyone can cache this for 24 hours)\n\nCache-Control: private, max-age=300\n  (only the browser can cache it, not CDNs, for 5 minutes)\n\nCache-Control: no-store\n  (never cache this, for sensitive data like banking pages)\n",[22,2821,2822,2827,2832,2836,2841,2846,2850,2855],{"__ignoreMap":51},[55,2823,2824],{"class":57,"line":58},[55,2825,2826],{},"Cache-Control: public, max-age=86400\n",[55,2828,2829],{"class":57,"line":64},[55,2830,2831],{},"  (anyone can cache this for 24 hours)\n",[55,2833,2834],{"class":57,"line":71},[55,2835,68],{"emptyLinePlaceholder":67},[55,2837,2838],{"class":57,"line":77},[55,2839,2840],{},"Cache-Control: private, max-age=300\n",[55,2842,2843],{"class":57,"line":83},[55,2844,2845],{},"  (only the browser can cache it, not CDNs, for 5 minutes)\n",[55,2847,2848],{"class":57,"line":89},[55,2849,68],{"emptyLinePlaceholder":67},[55,2851,2852],{"class":57,"line":95},[55,2853,2854],{},"Cache-Control: no-store\n",[55,2856,2857],{"class":57,"line":101},[55,2858,2859],{},"  (never cache this, for sensitive data like banking pages)\n",[18,2861,2862,2865,2866,2869,2870,2873],{},[518,2863,2864],{},"ETag:"," A fingerprint of the content. When the browser has a cached copy and wants to check if it is still fresh, it sends ",[22,2867,2868],{},"If-None-Match: \"abc123def456\"",". The server checks: if content has not changed, it responds with ",[22,2871,2872],{},"304 Not Modified"," and no body. This saves bandwidth significantly.",[18,2875,2876,2879],{},[518,2877,2878],{},"Set-Cookie:"," Sets cookies in the browser. The security flags matter:",[46,2881,2882],{"className":1333,"code":2605,"language":1335,"meta":51,"style":51},[22,2883,2884],{"__ignoreMap":51},[55,2885,2886],{"class":57,"line":58},[55,2887,2605],{},[1396,2889,2890,2896,2902],{},[1399,2891,2892,2895],{},[22,2893,2894],{},"HttpOnly",": JavaScript cannot read this cookie (prevents XSS stealing sessions)",[1399,2897,2898,2901],{},[22,2899,2900],{},"Secure",": Only sent over HTTPS connections",[1399,2903,2904,2907],{},[22,2905,2906],{},"SameSite=Strict",": Not sent with cross-site requests (prevents CSRF attacks)",[18,2909,2910,2913,2914,2917,2918,2921],{},[518,2911,2912],{},"Strict-Transport-Security (HSTS):"," Tells the browser to always use HTTPS for this domain, even if someone types ",[22,2915,2916],{},"http://",". The ",[22,2919,2920],{},"max-age=31536000"," means this rule should be remembered for 1 year.",[36,2923],{},[13,2925,2927],{"id":2926},"step-8-browser-rendering-turning-bytes-into-pixels","Step 8: Browser Rendering (Turning Bytes into Pixels)",[18,2929,2930,2931,2934],{},"The browser has received the HTTP response. It now needs to turn raw bytes into a visual webpage. This process is called the ",[518,2932,2933],{},"Critical Rendering Path"," and it has several distinct stages.",[343,2936,2938],{"id":2937},"the-full-rendering-pipeline","The Full Rendering Pipeline",[46,2940,2942],{"className":48,"code":2941,"language":50,"meta":51,"style":51},"  Bytes (raw data from network)\n      |\n      v\n  Characters (decoded with UTF-8)\n      |\n      v\n  Tokens (\u003C/html>, \u003Chead>, \u003Cbody>, etc.)\n      |\n      +------------------------+\n      |                        |\n      v                        v\n    DOM                      CSSOM\n  (Document Object          (CSS Object\n   Model)                    Model)\n      |                        |\n      +----------+-------------+\n                 |\n                 v\n           Render Tree\n      (visible nodes with styles)\n                 |\n                 v\n              Layout\n      (calculate sizes and positions)\n                 |\n                 v\n              Paint\n      (draw pixels to layers)\n                 |\n                 v\n           Compositing\n      (combine layers into final frame)\n                 |\n                 v\n         Screen Display\n",[22,2943,2944,2949,2953,2957,2962,2966,2970,2975,2979,2984,2989,2994,2999,3004,3009,3013,3018,3023,3028,3033,3038,3042,3046,3051,3056,3060,3064,3069,3074,3078,3082,3087,3092,3097,3102],{"__ignoreMap":51},[55,2945,2946],{"class":57,"line":58},[55,2947,2948],{},"  Bytes (raw data from network)\n",[55,2950,2951],{"class":57,"line":64},[55,2952,1237],{},[55,2954,2955],{"class":57,"line":71},[55,2956,1785],{},[55,2958,2959],{"class":57,"line":77},[55,2960,2961],{},"  Characters (decoded with UTF-8)\n",[55,2963,2964],{"class":57,"line":83},[55,2965,1237],{},[55,2967,2968],{"class":57,"line":89},[55,2969,1785],{},[55,2971,2972],{"class":57,"line":95},[55,2973,2974],{},"  Tokens (\u003C/html>, \u003Chead>, \u003Cbody>, etc.)\n",[55,2976,2977],{"class":57,"line":101},[55,2978,1237],{},[55,2980,2981],{"class":57,"line":107},[55,2982,2983],{},"      +------------------------+\n",[55,2985,2986],{"class":57,"line":113},[55,2987,2988],{},"      |                        |\n",[55,2990,2991],{"class":57,"line":119},[55,2992,2993],{},"      v                        v\n",[55,2995,2996],{"class":57,"line":125},[55,2997,2998],{},"    DOM                      CSSOM\n",[55,3000,3001],{"class":57,"line":131},[55,3002,3003],{},"  (Document Object          (CSS Object\n",[55,3005,3006],{"class":57,"line":137},[55,3007,3008],{},"   Model)                    Model)\n",[55,3010,3011],{"class":57,"line":143},[55,3012,2988],{},[55,3014,3015],{"class":57,"line":149},[55,3016,3017],{},"      +----------+-------------+\n",[55,3019,3020],{"class":57,"line":441},[55,3021,3022],{},"                 |\n",[55,3024,3025],{"class":57,"line":447},[55,3026,3027],{},"                 v\n",[55,3029,3030],{"class":57,"line":453},[55,3031,3032],{},"           Render Tree\n",[55,3034,3035],{"class":57,"line":459},[55,3036,3037],{},"      (visible nodes with styles)\n",[55,3039,3040],{"class":57,"line":464},[55,3041,3022],{},[55,3043,3044],{"class":57,"line":470},[55,3045,3027],{},[55,3047,3048],{"class":57,"line":475},[55,3049,3050],{},"              Layout\n",[55,3052,3053],{"class":57,"line":481},[55,3054,3055],{},"      (calculate sizes and positions)\n",[55,3057,3058],{"class":57,"line":486},[55,3059,3022],{},[55,3061,3062],{"class":57,"line":492},[55,3063,3027],{},[55,3065,3066],{"class":57,"line":497},[55,3067,3068],{},"              Paint\n",[55,3070,3071],{"class":57,"line":503},[55,3072,3073],{},"      (draw pixels to layers)\n",[55,3075,3076],{"class":57,"line":508},[55,3077,3022],{},[55,3079,3080],{"class":57,"line":2347},[55,3081,3027],{},[55,3083,3084],{"class":57,"line":2353},[55,3085,3086],{},"           Compositing\n",[55,3088,3089],{"class":57,"line":2368},[55,3090,3091],{},"      (combine layers into final frame)\n",[55,3093,3095],{"class":57,"line":3094},33,[55,3096,3022],{},[55,3098,3100],{"class":57,"line":3099},34,[55,3101,3027],{},[55,3103,3105],{"class":57,"line":3104},35,[55,3106,3107],{},"         Screen Display\n",[343,3109,3111],{"id":3110},"parsing-html-building-the-dom","Parsing HTML: Building the DOM",[18,3113,3114,3115,3118],{},"The browser reads the HTML bytes and tokenizes them into a tree structure called the ",[518,3116,3117],{},"Document Object Model (DOM)",". Each HTML tag becomes a node in the tree.",[46,3120,3124],{"className":3121,"code":3122,"language":3123,"meta":51,"style":51},"language-html shiki shiki-themes github-light","\u003C!DOCTYPE html>\n\u003Chtml>\n  \u003Chead>\n    \u003Ctitle>My Page\u003C/title>\n  \u003C/head>\n  \u003Cbody>\n    \u003Ch1>Hello World\u003C/h1>\n    \u003Cp>A paragraph.\u003C/p>\n  \u003C/body>\n\u003C/html>\n","html",[22,3125,3126,3141,3150,3160,3174,3183,3191,3205,3218,3226],{"__ignoreMap":51},[55,3127,3128,3131,3135,3138],{"class":57,"line":58},[55,3129,3130],{"class":2053},"\u003C!",[55,3132,3134],{"class":3133},"shJU0","DOCTYPE",[55,3136,3137],{"class":717}," html",[55,3139,3140],{"class":2053},">\n",[55,3142,3143,3146,3148],{"class":57,"line":64},[55,3144,3145],{"class":2053},"\u003C",[55,3147,3123],{"class":3133},[55,3149,3140],{"class":2053},[55,3151,3152,3155,3158],{"class":57,"line":71},[55,3153,3154],{"class":2053},"  \u003C",[55,3156,3157],{"class":3133},"head",[55,3159,3140],{"class":2053},[55,3161,3162,3165,3167,3170,3172],{"class":57,"line":77},[55,3163,3164],{"class":2053},"    \u003C",[55,3166,2413],{"class":3133},[55,3168,3169],{"class":2053},">My Page\u003C/",[55,3171,2413],{"class":3133},[55,3173,3140],{"class":2053},[55,3175,3176,3179,3181],{"class":57,"line":83},[55,3177,3178],{"class":2053},"  \u003C/",[55,3180,3157],{"class":3133},[55,3182,3140],{"class":2053},[55,3184,3185,3187,3189],{"class":57,"line":89},[55,3186,3154],{"class":2053},[55,3188,2424],{"class":3133},[55,3190,3140],{"class":2053},[55,3192,3193,3195,3198,3201,3203],{"class":57,"line":95},[55,3194,3164],{"class":2053},[55,3196,3197],{"class":3133},"h1",[55,3199,3200],{"class":2053},">Hello World\u003C/",[55,3202,3197],{"class":3133},[55,3204,3140],{"class":2053},[55,3206,3207,3209,3211,3214,3216],{"class":57,"line":101},[55,3208,3164],{"class":2053},[55,3210,18],{"class":3133},[55,3212,3213],{"class":2053},">A paragraph.\u003C/",[55,3215,18],{"class":3133},[55,3217,3140],{"class":2053},[55,3219,3220,3222,3224],{"class":57,"line":107},[55,3221,3178],{"class":2053},[55,3223,2424],{"class":3133},[55,3225,3140],{"class":2053},[55,3227,3228,3231,3233],{"class":57,"line":113},[55,3229,3230],{"class":2053},"\u003C/",[55,3232,3123],{"class":3133},[55,3234,3140],{"class":2053},[18,3236,3237],{},"This becomes:",[46,3239,3241],{"className":48,"code":3240,"language":50,"meta":51,"style":51},"Document\n  └── html\n        ├── head\n        │     └── title\n        │           └── \"My Page\"\n        └── body\n              ├── h1\n              │     └── \"Hello World\"\n              └── p\n                    └── \"A paragraph.\"\n",[22,3242,3243,3248,3253,3258,3263,3268,3273,3278,3283,3288],{"__ignoreMap":51},[55,3244,3245],{"class":57,"line":58},[55,3246,3247],{},"Document\n",[55,3249,3250],{"class":57,"line":64},[55,3251,3252],{},"  └── html\n",[55,3254,3255],{"class":57,"line":71},[55,3256,3257],{},"        ├── head\n",[55,3259,3260],{"class":57,"line":77},[55,3261,3262],{},"        │     └── title\n",[55,3264,3265],{"class":57,"line":83},[55,3266,3267],{},"        │           └── \"My Page\"\n",[55,3269,3270],{"class":57,"line":89},[55,3271,3272],{},"        └── body\n",[55,3274,3275],{"class":57,"line":95},[55,3276,3277],{},"              ├── h1\n",[55,3279,3280],{"class":57,"line":101},[55,3281,3282],{},"              │     └── \"Hello World\"\n",[55,3284,3285],{"class":57,"line":107},[55,3286,3287],{},"              └── p\n",[55,3289,3290],{"class":57,"line":113},[55,3291,3292],{},"                    └── \"A paragraph.\"\n",[343,3294,3296],{"id":3295},"parsing-css-building-the-cssom","Parsing CSS: Building the CSSOM",[18,3298,3299,3300,3303],{},"Simultaneously (or as CSS files arrive), the browser parses CSS into the ",[518,3301,3302],{},"CSS Object Model (CSSOM)",", a similar tree structure that maps selectors to computed styles.",[46,3305,3309],{"className":3306,"code":3307,"language":3308,"meta":51,"style":51},"language-css shiki shiki-themes github-light","body { font-family: sans-serif; }\nh1   { color: blue; font-size: 2em; }\np    { color: gray; }\n","css",[22,3310,3311,3330,3361],{"__ignoreMap":51},[55,3312,3313,3315,3318,3321,3324,3327],{"class":57,"line":58},[55,3314,2424],{"class":3133},[55,3316,3317],{"class":2053}," { ",[55,3319,3320],{"class":2069},"font-family",[55,3322,3323],{"class":2053},": ",[55,3325,3326],{"class":2069},"sans-serif",[55,3328,3329],{"class":2053},"; }\n",[55,3331,3332,3334,3337,3340,3342,3345,3348,3351,3353,3356,3359],{"class":57,"line":64},[55,3333,3197],{"class":3133},[55,3335,3336],{"class":2053},"   { ",[55,3338,3339],{"class":2069},"color",[55,3341,3323],{"class":2053},[55,3343,3344],{"class":2069},"blue",[55,3346,3347],{"class":2053},"; ",[55,3349,3350],{"class":2069},"font-size",[55,3352,3323],{"class":2053},[55,3354,3355],{"class":2069},"2",[55,3357,3358],{"class":2043},"em",[55,3360,3329],{"class":2053},[55,3362,3363,3365,3368,3370,3372,3375],{"class":57,"line":71},[55,3364,18],{"class":3133},[55,3366,3367],{"class":2053},"    { ",[55,3369,3339],{"class":2069},[55,3371,3323],{"class":2053},[55,3373,3374],{"class":2069},"gray",[55,3376,3329],{"class":2053},[18,3378,3379],{},"Becomes:",[46,3381,3383],{"className":48,"code":3382,"language":50,"meta":51,"style":51},"body\n  ├── font-family: sans-serif\n  └── h1\n        ├── color: blue\n        └── font-size: 2em\n  └── p\n        └── color: gray\n",[22,3384,3385,3390,3395,3400,3405,3410,3415],{"__ignoreMap":51},[55,3386,3387],{"class":57,"line":58},[55,3388,3389],{},"body\n",[55,3391,3392],{"class":57,"line":64},[55,3393,3394],{},"  ├── font-family: sans-serif\n",[55,3396,3397],{"class":57,"line":71},[55,3398,3399],{},"  └── h1\n",[55,3401,3402],{"class":57,"line":77},[55,3403,3404],{},"        ├── color: blue\n",[55,3406,3407],{"class":57,"line":83},[55,3408,3409],{},"        └── font-size: 2em\n",[55,3411,3412],{"class":57,"line":89},[55,3413,3414],{},"  └── p\n",[55,3416,3417],{"class":57,"line":95},[55,3418,3419],{},"        └── color: gray\n",[18,3421,3422,3423,3426,3427,3430,3431,3434],{},"CSS is ",[518,3424,3425],{},"render-blocking",": the browser cannot display anything until all CSS is downloaded and parsed. This is why you put ",[22,3428,3429],{},"\u003Clink rel=\"stylesheet\">"," in the ",[22,3432,3433],{},"\u003Chead>",": you want CSS to arrive before the browser starts painting. A slow CSS file delays the entire page.",[343,3436,3438],{"id":3437},"javascript-the-complication","JavaScript: The Complication",[18,3440,3441,3442,3445,3446,3449,3450,532],{},"When the HTML parser encounters a ",[22,3443,3444],{},"\u003Cscript>"," tag, by default it ",[518,3447,3448],{},"stops parsing HTML",", downloads the script, executes it (because JS can modify the DOM), and only then resumes parsing. This is called ",[518,3451,3452],{},"parser blocking",[46,3454,3456],{"className":3121,"code":3455,"language":3123,"meta":51,"style":51},"\u003C!-- Blocks HTML parsing until script loads and executes -->\n\u003Cscript src=\"analytics.js\">\u003C/script>\n\n\u003C!-- async: downloads in parallel, executes when ready (order not guaranteed) -->\n\u003Cscript src=\"analytics.js\" async>\u003C/script>\n\n\u003C!-- defer: downloads in parallel, executes after HTML is parsed (order preserved) -->\n\u003Cscript src=\"main.js\" defer>\u003C/script>\n",[22,3457,3458,3463,3486,3490,3495,3516,3520,3525],{"__ignoreMap":51},[55,3459,3460],{"class":57,"line":58},[55,3461,3462],{"class":711},"\u003C!-- Blocks HTML parsing until script loads and executes -->\n",[55,3464,3465,3467,3470,3473,3476,3479,3482,3484],{"class":57,"line":64},[55,3466,3145],{"class":2053},[55,3468,3469],{"class":3133},"script",[55,3471,3472],{"class":717}," src",[55,3474,3475],{"class":2053},"=",[55,3477,3478],{"class":721},"\"analytics.js\"",[55,3480,3481],{"class":2053},">\u003C/",[55,3483,3469],{"class":3133},[55,3485,3140],{"class":2053},[55,3487,3488],{"class":57,"line":71},[55,3489,68],{"emptyLinePlaceholder":67},[55,3491,3492],{"class":57,"line":77},[55,3493,3494],{"class":711},"\u003C!-- async: downloads in parallel, executes when ready (order not guaranteed) -->\n",[55,3496,3497,3499,3501,3503,3505,3507,3510,3512,3514],{"class":57,"line":83},[55,3498,3145],{"class":2053},[55,3500,3469],{"class":3133},[55,3502,3472],{"class":717},[55,3504,3475],{"class":2053},[55,3506,3478],{"class":721},[55,3508,3509],{"class":717}," async",[55,3511,3481],{"class":2053},[55,3513,3469],{"class":3133},[55,3515,3140],{"class":2053},[55,3517,3518],{"class":57,"line":89},[55,3519,68],{"emptyLinePlaceholder":67},[55,3521,3522],{"class":57,"line":95},[55,3523,3524],{"class":711},"\u003C!-- defer: downloads in parallel, executes after HTML is parsed (order preserved) -->\n",[55,3526,3527,3529,3531,3533,3535,3538,3541,3543,3545],{"class":57,"line":101},[55,3528,3145],{"class":2053},[55,3530,3469],{"class":3133},[55,3532,3472],{"class":717},[55,3534,3475],{"class":2053},[55,3536,3537],{"class":721},"\"main.js\"",[55,3539,3540],{"class":717}," defer",[55,3542,3481],{"class":2053},[55,3544,3469],{"class":3133},[55,3546,3140],{"class":2053},[18,3548,3549,3550,3552,3553,3556,3557,3560,3561,3563],{},"Best practice: put ",[22,3551,3444],{}," tags at the end of ",[22,3554,3555],{},"\u003Cbody>",", or use ",[22,3558,3559],{},"defer"," for application scripts and ",[22,3562,2044],{}," for independent scripts like analytics.",[343,3565,3567],{"id":3566},"the-render-tree-combining-dom-and-cssom","The Render Tree: Combining DOM and CSSOM",[18,3569,3570,3571,3574],{},"The browser combines the DOM and CSSOM into a ",[518,3572,3573],{},"Render Tree",". This tree contains only the visible elements, with their computed styles attached.",[18,3576,3577,3578,525,3581,525,3583,525,3585,3588,3589,3592],{},"Elements that are hidden (",[22,3579,3580],{},"display: none",[22,3582,3433],{},[22,3584,3444],{},[22,3586,3587],{},"\u003Cstyle>",") are excluded from the Render Tree. Elements that are invisible but still occupy space (",[22,3590,3591],{},"visibility: hidden",") are included.",[343,3594,3596],{"id":3595},"layout-calculating-geometry","Layout: Calculating Geometry",[18,3598,3599,3600,3603],{},"With the Render Tree, the browser calculates the exact position and size of every element: this is the ",[518,3601,3602],{},"Layout"," stage (also called Reflow).",[18,3605,3606],{},"The browser considers:",[1396,3608,3609,3612,3615,3618],{},[1399,3610,3611],{},"The viewport size (window width and height)",[1399,3613,3614],{},"The box model (margin, border, padding, content)",[1399,3616,3617],{},"Flexbox and Grid rules",[1399,3619,3620],{},"Relative and absolute positioning",[18,3622,3623],{},"Layout is expensive because changing one element's size can affect everything around it. This is why causing layout \"thrash\" in JavaScript (alternating reads and writes to the DOM in a loop) is a significant performance problem.",[343,3625,3627],{"id":3626},"paint-and-compositing-drawing-pixels","Paint and Compositing: Drawing Pixels",[18,3629,3630,3631,3634],{},"After layout, the browser knows where everything goes. Now it ",[518,3632,3633],{},"paints",", filling in the pixels for each element's background, text, borders, shadows, and images.",[18,3636,3637,3638,3641,3642,3645],{},"Modern browsers split the page into ",[518,3639,3640],{},"layers"," and paint each one independently. A fixed navigation bar is on its own layer. Animated elements get their own layers. The GPU then ",[518,3643,3644],{},"composites"," all layers into the final image you see.",[18,3647,3648],{},"Properties that trigger only compositing (and are therefore very fast to animate):",[1396,3650,3651,3656],{},[1399,3652,3653],{},[22,3654,3655],{},"transform: translate(), scale(), rotate()",[1399,3657,3658],{},[22,3659,3660],{},"opacity",[18,3662,3663],{},"Properties that trigger layout (very expensive to animate):",[1396,3665,3666],{},[1399,3667,3668,525,3671,525,3674,525,3677,525,3680,525,3683],{},[22,3669,3670],{},"width",[22,3672,3673],{},"height",[22,3675,3676],{},"margin",[22,3678,3679],{},"padding",[22,3681,3682],{},"top",[22,3684,3685],{},"left",[18,3687,3688,3689,3692,3693,321,3695,532],{},"This is why performant CSS animations use ",[22,3690,3691],{},"transform"," instead of changing ",[22,3694,3685],{},[22,3696,3682],{},[36,3698],{},[13,3700,3702],{"id":3701},"step-9-sub-resources-and-the-waterfall","Step 9: Sub-resources and the Waterfall",[18,3704,3705],{},"Loading the initial HTML is just the beginning. The HTML contains references to many other resources: CSS files, JavaScript files, images, fonts, icons. The browser discovers these references as it parses the HTML and fetches them in parallel.",[343,3707,3709],{"id":3708},"the-network-waterfall","The Network Waterfall",[18,3711,3712],{},"A typical web page load looks like this in the browser's Network panel:",[46,3714,3716],{"className":48,"code":3715,"language":50,"meta":51,"style":51},"0ms     100ms    200ms    300ms    400ms    500ms    600ms\n|--------|--------|--------|--------|--------|--------|\n                                                        \n[DNS]                    \n      [TCP+TLS]           \n               [HTML  ]   \n               [CSS1     ]\n               [CSS2     ]\n                    [JS1          ]\n                    [JS2          ]\n                         [logo.png     ]\n                         [hero.jpg             ]\n                         [font.woff2       ]\n                              [analytics.js    ]\n                                        |\n                                   Page ready!\n",[22,3717,3718,3723,3728,3733,3738,3743,3748,3753,3758,3763,3768,3773,3778,3783,3788,3793],{"__ignoreMap":51},[55,3719,3720],{"class":57,"line":58},[55,3721,3722],{},"0ms     100ms    200ms    300ms    400ms    500ms    600ms\n",[55,3724,3725],{"class":57,"line":64},[55,3726,3727],{},"|--------|--------|--------|--------|--------|--------|\n",[55,3729,3730],{"class":57,"line":71},[55,3731,3732],{},"                                                        \n",[55,3734,3735],{"class":57,"line":77},[55,3736,3737],{},"[DNS]                    \n",[55,3739,3740],{"class":57,"line":83},[55,3741,3742],{},"      [TCP+TLS]           \n",[55,3744,3745],{"class":57,"line":89},[55,3746,3747],{},"               [HTML  ]   \n",[55,3749,3750],{"class":57,"line":95},[55,3751,3752],{},"               [CSS1     ]\n",[55,3754,3755],{"class":57,"line":101},[55,3756,3757],{},"               [CSS2     ]\n",[55,3759,3760],{"class":57,"line":107},[55,3761,3762],{},"                    [JS1          ]\n",[55,3764,3765],{"class":57,"line":113},[55,3766,3767],{},"                    [JS2          ]\n",[55,3769,3770],{"class":57,"line":119},[55,3771,3772],{},"                         [logo.png     ]\n",[55,3774,3775],{"class":57,"line":125},[55,3776,3777],{},"                         [hero.jpg             ]\n",[55,3779,3780],{"class":57,"line":131},[55,3781,3782],{},"                         [font.woff2       ]\n",[55,3784,3785],{"class":57,"line":137},[55,3786,3787],{},"                              [analytics.js    ]\n",[55,3789,3790],{"class":57,"line":143},[55,3791,3792],{},"                                        |\n",[55,3794,3795],{"class":57,"line":149},[55,3796,3797],{},"                                   Page ready!\n",[18,3799,3800],{},"Each resource requires the browser to:",[2527,3802,3803,3806,3809,3812,3815,3818,3821,3824],{},[1399,3804,3805],{},"Parse the URL in the HTML",[1399,3807,3808],{},"Check its local cache",[1399,3810,3811],{},"Open a TCP connection (or reuse an existing one with Keep-Alive)",[1399,3813,3814],{},"Perform a TLS handshake (for new connections)",[1399,3816,3817],{},"Send the HTTP request",[1399,3819,3820],{},"Wait for the response",[1399,3822,3823],{},"Download the body",[1399,3825,3826],{},"Parse/decode the resource",[18,3828,3829],{},"HTTP/2 helps by multiplexing many requests over one connection. But network round trips still cost time.",[343,3831,3833],{"id":3832},"critical-resources-vs-non-critical-resources","Critical Resources vs Non-Critical Resources",[18,3835,3836],{},"Not all resources are equal. Some block rendering:",[18,3838,3839,3842,3843,3845,3846,3848],{},[518,3840,3841],{},"Render-blocking resources:"," CSS in ",[22,3844,3433],{},", synchronous ",[22,3847,3444],{}," tags. The browser cannot paint anything until these are processed.",[18,3850,3851,3854,3855,3857,3858,3860],{},[518,3852,3853],{},"Non-blocking resources:"," Images, ",[22,3856,2044],{},"/",[22,3859,3559],{}," scripts, resources loaded lazily. The browser can paint the page and load these in the background.",[18,3862,3863],{},"Performance optimization is largely about minimizing the amount of render-blocking work the browser must do before it can show the user something.",[36,3865],{},[13,3867,3869],{"id":3868},"step-10-caching-making-everything-faster","Step 10: Caching (Making Everything Faster)",[18,3871,3872],{},"A full round trip from browser to server and back has costs: network latency, DNS lookup, TCP handshake, TLS negotiation, server processing time. Caches exist at multiple layers to avoid repeating this work.",[343,3874,3876],{"id":3875},"the-caching-layers","The Caching Layers",[46,3878,3880],{"className":48,"code":3879,"language":50,"meta":51,"style":51},"User\n  |\n  +--> Browser Cache\n  |      (stores responses on disk)\n  |\n  +--> Service Worker Cache\n  |      (programmable cache in the browser)\n  |\n  +--> CDN (Content Delivery Network)\n  |      (caches at edge locations near users)\n  |\n  +--> Reverse Proxy Cache (Nginx, Varnish)\n  |      (caches at the server edge)\n  |\n  +--> Application Cache (Redis, Memcached)\n  |      (caches database query results)\n  |\n  +--> Database Buffer Cache\n         (database keeps hot data in memory)\n",[22,3881,3882,3887,3892,3897,3902,3906,3911,3916,3920,3925,3930,3934,3939,3944,3948,3953,3958,3962,3967],{"__ignoreMap":51},[55,3883,3884],{"class":57,"line":58},[55,3885,3886],{},"User\n",[55,3888,3889],{"class":57,"line":64},[55,3890,3891],{},"  |\n",[55,3893,3894],{"class":57,"line":71},[55,3895,3896],{},"  +--> Browser Cache\n",[55,3898,3899],{"class":57,"line":77},[55,3900,3901],{},"  |      (stores responses on disk)\n",[55,3903,3904],{"class":57,"line":83},[55,3905,3891],{},[55,3907,3908],{"class":57,"line":89},[55,3909,3910],{},"  +--> Service Worker Cache\n",[55,3912,3913],{"class":57,"line":95},[55,3914,3915],{},"  |      (programmable cache in the browser)\n",[55,3917,3918],{"class":57,"line":101},[55,3919,3891],{},[55,3921,3922],{"class":57,"line":107},[55,3923,3924],{},"  +--> CDN (Content Delivery Network)\n",[55,3926,3927],{"class":57,"line":113},[55,3928,3929],{},"  |      (caches at edge locations near users)\n",[55,3931,3932],{"class":57,"line":119},[55,3933,3891],{},[55,3935,3936],{"class":57,"line":125},[55,3937,3938],{},"  +--> Reverse Proxy Cache (Nginx, Varnish)\n",[55,3940,3941],{"class":57,"line":131},[55,3942,3943],{},"  |      (caches at the server edge)\n",[55,3945,3946],{"class":57,"line":137},[55,3947,3891],{},[55,3949,3950],{"class":57,"line":143},[55,3951,3952],{},"  +--> Application Cache (Redis, Memcached)\n",[55,3954,3955],{"class":57,"line":149},[55,3956,3957],{},"  |      (caches database query results)\n",[55,3959,3960],{"class":57,"line":441},[55,3961,3891],{},[55,3963,3964],{"class":57,"line":447},[55,3965,3966],{},"  +--> Database Buffer Cache\n",[55,3968,3969],{"class":57,"line":453},[55,3970,3971],{},"         (database keeps hot data in memory)\n",[343,3973,3975],{"id":3974},"browser-caching-in-detail","Browser Caching in Detail",[18,3977,3978],{},"The browser stores downloaded resources locally. On the next visit, before making a network request, it checks:",[18,3980,3981,3984,3985,3988],{},[518,3982,3983],{},"Is the resource still fresh?"," (Has the ",[22,3986,3987],{},"max-age"," elapsed?)",[1396,3990,3991,3994],{},[1399,3992,3993],{},"Yes: Use cached copy. No network request at all. Instant.",[1399,3995,3996,3997,4000,4001,1448,4004],{},"No: Send a ",[518,3998,3999],{},"conditional request"," with ",[22,4002,4003],{},"If-None-Match: \"etag-value\"",[22,4005,4006],{},"If-Modified-Since: date",[18,4008,4009,4012],{},[518,4010,4011],{},"Did the server say the cache is still valid?"," (304 Not Modified)",[1396,4014,4015,4018],{},[1399,4016,4017],{},"Yes: Use cached copy. No body transferred. Very fast.",[1399,4019,4020],{},"No: Download fresh copy. Normal response.",[343,4022,4024],{"id":4023},"cache-busting","Cache Busting",[18,4026,4027],{},"CSS and JavaScript files are often given fingerprinted filenames to force browsers to download new versions when code changes:",[46,4029,4031],{"className":3121,"code":4030,"language":3123,"meta":51,"style":51},"\u003C!-- Without cache busting: browser might serve stale CSS -->\n\u003Clink rel=\"stylesheet\" href=\"/styles.css\">\n\n\u003C!-- With cache busting: new hash = new file = fresh download -->\n\u003Clink rel=\"stylesheet\" href=\"/styles.a3f9b2.css\">\n",[22,4032,4033,4038,4063,4067,4072],{"__ignoreMap":51},[55,4034,4035],{"class":57,"line":58},[55,4036,4037],{"class":711},"\u003C!-- Without cache busting: browser might serve stale CSS -->\n",[55,4039,4040,4042,4045,4048,4050,4053,4056,4058,4061],{"class":57,"line":64},[55,4041,3145],{"class":2053},[55,4043,4044],{"class":3133},"link",[55,4046,4047],{"class":717}," rel",[55,4049,3475],{"class":2053},[55,4051,4052],{"class":721},"\"stylesheet\"",[55,4054,4055],{"class":717}," href",[55,4057,3475],{"class":2053},[55,4059,4060],{"class":721},"\"/styles.css\"",[55,4062,3140],{"class":2053},[55,4064,4065],{"class":57,"line":71},[55,4066,68],{"emptyLinePlaceholder":67},[55,4068,4069],{"class":57,"line":77},[55,4070,4071],{"class":711},"\u003C!-- With cache busting: new hash = new file = fresh download -->\n",[55,4073,4074,4076,4078,4080,4082,4084,4086,4088,4091],{"class":57,"line":83},[55,4075,3145],{"class":2053},[55,4077,4044],{"class":3133},[55,4079,4047],{"class":717},[55,4081,3475],{"class":2053},[55,4083,4052],{"class":721},[55,4085,4055],{"class":717},[55,4087,3475],{"class":2053},[55,4089,4090],{"class":721},"\"/styles.a3f9b2.css\"",[55,4092,3140],{"class":2053},[18,4094,4095,4096,4099],{},"Build tools like Vite, Webpack, and esbuild do this automatically. The CSS file itself can have ",[22,4097,4098],{},"Cache-Control: public, max-age=31536000, immutable"," (cache for 1 year) because the filename changes whenever the content changes.",[36,4101],{},[13,4103,4105],{"id":4104},"putting-it-all-together-the-full-timeline","Putting It All Together: The Full Timeline",[18,4107,4108],{},"Here is the complete journey one more time, annotated with timing characteristics for a realistic page load:",[46,4110,4112],{"className":48,"code":4111,"language":50,"meta":51,"style":51},"t=0ms\n  User presses Enter\n\nt=1ms\n  Browser checks cache for DNS record\n  (cache hit: skip to t=3ms)\n  (cache miss: DNS query begins)\n\nt=50ms  [DNS lookup]\n  Recursive resolver queries root, TLD, authoritative NS\n  Returns: 93.184.216.34\n\nt=51ms  [TCP handshake begins]\n  SYN sent to 93.184.216.34:443\n\nt=100ms [TCP + TLS handshake complete]\n  SYN-ACK received, ACK sent\n  TLS ClientHello/ServerHello exchanged\n  Encryption keys derived\n\nt=101ms [HTTP request sent]\n  GET /blog/article?id=42 HTTP/2\n\nt=150ms [HTTP response headers received]\n  200 OK, Content-Type: text/html\n\nt=180ms [HTML body fully received]\n  Browser begins parsing HTML\n\nt=185ms [CSS files discovered in \u003Chead>]\n  Browser sends parallel requests for CSS files\n\nt=220ms [CSS fully downloaded and parsed]\n  CSSOM built\n\nt=225ms [Render tree built]\n  DOM + CSSOM merged\n\nt=226ms [Layout calculated]\n\nt=228ms [First Contentful Paint (FCP)]\n  User sees something on screen!\n\nt=250ms [JavaScript files downloaded and executed]\n\nt=400ms [All images and fonts loaded]\n\nt=401ms [Page fully interactive]\n  All event listeners attached\n",[22,4113,4114,4119,4124,4128,4133,4138,4143,4148,4152,4157,4162,4167,4171,4176,4181,4185,4190,4195,4200,4205,4209,4214,4219,4223,4228,4233,4237,4242,4247,4251,4256,4261,4265,4270,4275,4279,4285,4291,4296,4302,4307,4313,4319,4324,4330,4335,4341,4346,4352],{"__ignoreMap":51},[55,4115,4116],{"class":57,"line":58},[55,4117,4118],{},"t=0ms\n",[55,4120,4121],{"class":57,"line":64},[55,4122,4123],{},"  User presses Enter\n",[55,4125,4126],{"class":57,"line":71},[55,4127,68],{"emptyLinePlaceholder":67},[55,4129,4130],{"class":57,"line":77},[55,4131,4132],{},"t=1ms\n",[55,4134,4135],{"class":57,"line":83},[55,4136,4137],{},"  Browser checks cache for DNS record\n",[55,4139,4140],{"class":57,"line":89},[55,4141,4142],{},"  (cache hit: skip to t=3ms)\n",[55,4144,4145],{"class":57,"line":95},[55,4146,4147],{},"  (cache miss: DNS query begins)\n",[55,4149,4150],{"class":57,"line":101},[55,4151,68],{"emptyLinePlaceholder":67},[55,4153,4154],{"class":57,"line":107},[55,4155,4156],{},"t=50ms  [DNS lookup]\n",[55,4158,4159],{"class":57,"line":113},[55,4160,4161],{},"  Recursive resolver queries root, TLD, authoritative NS\n",[55,4163,4164],{"class":57,"line":119},[55,4165,4166],{},"  Returns: 93.184.216.34\n",[55,4168,4169],{"class":57,"line":125},[55,4170,68],{"emptyLinePlaceholder":67},[55,4172,4173],{"class":57,"line":131},[55,4174,4175],{},"t=51ms  [TCP handshake begins]\n",[55,4177,4178],{"class":57,"line":137},[55,4179,4180],{},"  SYN sent to 93.184.216.34:443\n",[55,4182,4183],{"class":57,"line":143},[55,4184,68],{"emptyLinePlaceholder":67},[55,4186,4187],{"class":57,"line":149},[55,4188,4189],{},"t=100ms [TCP + TLS handshake complete]\n",[55,4191,4192],{"class":57,"line":441},[55,4193,4194],{},"  SYN-ACK received, ACK sent\n",[55,4196,4197],{"class":57,"line":447},[55,4198,4199],{},"  TLS ClientHello/ServerHello exchanged\n",[55,4201,4202],{"class":57,"line":453},[55,4203,4204],{},"  Encryption keys derived\n",[55,4206,4207],{"class":57,"line":459},[55,4208,68],{"emptyLinePlaceholder":67},[55,4210,4211],{"class":57,"line":464},[55,4212,4213],{},"t=101ms [HTTP request sent]\n",[55,4215,4216],{"class":57,"line":470},[55,4217,4218],{},"  GET /blog/article?id=42 HTTP/2\n",[55,4220,4221],{"class":57,"line":475},[55,4222,68],{"emptyLinePlaceholder":67},[55,4224,4225],{"class":57,"line":481},[55,4226,4227],{},"t=150ms [HTTP response headers received]\n",[55,4229,4230],{"class":57,"line":486},[55,4231,4232],{},"  200 OK, Content-Type: text/html\n",[55,4234,4235],{"class":57,"line":492},[55,4236,68],{"emptyLinePlaceholder":67},[55,4238,4239],{"class":57,"line":497},[55,4240,4241],{},"t=180ms [HTML body fully received]\n",[55,4243,4244],{"class":57,"line":503},[55,4245,4246],{},"  Browser begins parsing HTML\n",[55,4248,4249],{"class":57,"line":508},[55,4250,68],{"emptyLinePlaceholder":67},[55,4252,4253],{"class":57,"line":2347},[55,4254,4255],{},"t=185ms [CSS files discovered in \u003Chead>]\n",[55,4257,4258],{"class":57,"line":2353},[55,4259,4260],{},"  Browser sends parallel requests for CSS files\n",[55,4262,4263],{"class":57,"line":2368},[55,4264,68],{"emptyLinePlaceholder":67},[55,4266,4267],{"class":57,"line":3094},[55,4268,4269],{},"t=220ms [CSS fully downloaded and parsed]\n",[55,4271,4272],{"class":57,"line":3099},[55,4273,4274],{},"  CSSOM built\n",[55,4276,4277],{"class":57,"line":3104},[55,4278,68],{"emptyLinePlaceholder":67},[55,4280,4282],{"class":57,"line":4281},36,[55,4283,4284],{},"t=225ms [Render tree built]\n",[55,4286,4288],{"class":57,"line":4287},37,[55,4289,4290],{},"  DOM + CSSOM merged\n",[55,4292,4294],{"class":57,"line":4293},38,[55,4295,68],{"emptyLinePlaceholder":67},[55,4297,4299],{"class":57,"line":4298},39,[55,4300,4301],{},"t=226ms [Layout calculated]\n",[55,4303,4305],{"class":57,"line":4304},40,[55,4306,68],{"emptyLinePlaceholder":67},[55,4308,4310],{"class":57,"line":4309},41,[55,4311,4312],{},"t=228ms [First Contentful Paint (FCP)]\n",[55,4314,4316],{"class":57,"line":4315},42,[55,4317,4318],{},"  User sees something on screen!\n",[55,4320,4322],{"class":57,"line":4321},43,[55,4323,68],{"emptyLinePlaceholder":67},[55,4325,4327],{"class":57,"line":4326},44,[55,4328,4329],{},"t=250ms [JavaScript files downloaded and executed]\n",[55,4331,4333],{"class":57,"line":4332},45,[55,4334,68],{"emptyLinePlaceholder":67},[55,4336,4338],{"class":57,"line":4337},46,[55,4339,4340],{},"t=400ms [All images and fonts loaded]\n",[55,4342,4344],{"class":57,"line":4343},47,[55,4345,68],{"emptyLinePlaceholder":67},[55,4347,4349],{"class":57,"line":4348},48,[55,4350,4351],{},"t=401ms [Page fully interactive]\n",[55,4353,4355],{"class":57,"line":4354},49,[55,4356,4357],{},"  All event listeners attached\n",[18,4359,4360],{},"The specific times vary enormously based on network speed, server location, file sizes, and server performance. The sequence, however, is always the same.",[36,4362],{},[13,4364,4366],{"id":4365},"common-performance-metrics-and-what-they-measure","Common Performance Metrics and What They Measure",[18,4368,4369],{},"When developers talk about web performance, they use specific metrics that correspond to points in the rendering timeline:",[186,4371,4372,4385],{},[189,4373,4374],{},[192,4375,4376,4379,4382],{},[195,4377,4378],{},"Metric",[195,4380,4381],{},"What it measures",[195,4383,4384],{},"Good threshold",[205,4386,4387,4398,4409,4420,4431,4442],{},[192,4388,4389,4392,4395],{},[210,4390,4391],{},"TTFB (Time to First Byte)",[210,4393,4394],{},"When did the first byte of the HTML response arrive?",[210,4396,4397],{},"\u003C 800ms",[192,4399,4400,4403,4406],{},[210,4401,4402],{},"FCP (First Contentful Paint)",[210,4404,4405],{},"When did the user see any content?",[210,4407,4408],{},"\u003C 1.8s",[192,4410,4411,4414,4417],{},[210,4412,4413],{},"LCP (Largest Contentful Paint)",[210,4415,4416],{},"When did the main content appear?",[210,4418,4419],{},"\u003C 2.5s",[192,4421,4422,4425,4428],{},[210,4423,4424],{},"TBT (Total Blocking Time)",[210,4426,4427],{},"How long was the main thread blocked by JS?",[210,4429,4430],{},"\u003C 200ms",[192,4432,4433,4436,4439],{},[210,4434,4435],{},"CLS (Cumulative Layout Shift)",[210,4437,4438],{},"How much did content jump around during load?",[210,4440,4441],{},"\u003C 0.1",[192,4443,4444,4447,4450],{},[210,4445,4446],{},"TTI (Time to Interactive)",[210,4448,4449],{},"When could the user interact with the page?",[210,4451,4452],{},"\u003C 3.8s",[18,4454,4455],{},"Google uses Core Web Vitals (LCP, TBT, CLS) as a ranking signal in search results. Performance is not just a user experience concern; it affects SEO directly.",[36,4457],{},[13,4459,4461],{"id":4460},"security-considerations-at-each-layer","Security Considerations at Each Layer",[18,4463,4464],{},"The web's architecture introduces potential attack surfaces at every layer. Here is a brief map:",[186,4466,4467,4480],{},[189,4468,4469],{},[192,4470,4471,4474,4477],{},[195,4472,4473],{},"Layer",[195,4475,4476],{},"Common Attack",[195,4478,4479],{},"Defense",[205,4481,4482,4493,4504,4515,4526],{},[192,4483,4484,4487,4490],{},[210,4485,4486],{},"DNS",[210,4488,4489],{},"DNS spoofing, cache poisoning",[210,4491,4492],{},"DNSSEC, use trusted resolvers",[192,4494,4495,4498,4501],{},[210,4496,4497],{},"Transport",[210,4499,4500],{},"Man-in-the-middle attacks",[210,4502,4503],{},"TLS, HSTS, certificate pinning",[192,4505,4506,4509,4512],{},[210,4507,4508],{},"HTTP",[210,4510,4511],{},"Cookie theft, session hijacking",[210,4513,4514],{},"HttpOnly/Secure cookies, SameSite",[192,4516,4517,4520,4523],{},[210,4518,4519],{},"Application",[210,4521,4522],{},"SQL injection, XSS, CSRF",[210,4524,4525],{},"Parameterized queries, CSP, CSRF tokens",[192,4527,4528,4531,4534],{},[210,4529,4530],{},"Caching",[210,4532,4533],{},"Serving stale/wrong content to wrong user",[210,4535,4536,4539],{},[22,4537,4538],{},"Cache-Control: private"," for user data",[18,4541,4542,4543,4546],{},"The ",[22,4544,4545],{},"Content-Security-Policy"," (CSP) header is worth highlighting. It tells the browser which sources of scripts, styles, and images are allowed:",[46,4548,4550],{"className":48,"code":4549,"language":50,"meta":51,"style":51},"Content-Security-Policy: default-src 'self';\n  script-src 'self' https://cdn.example.com;\n  img-src 'self' data: https:;\n  style-src 'self' 'unsafe-inline';\n",[22,4551,4552,4557,4562,4567],{"__ignoreMap":51},[55,4553,4554],{"class":57,"line":58},[55,4555,4556],{},"Content-Security-Policy: default-src 'self';\n",[55,4558,4559],{"class":57,"line":64},[55,4560,4561],{},"  script-src 'self' https://cdn.example.com;\n",[55,4563,4564],{"class":57,"line":71},[55,4565,4566],{},"  img-src 'self' data: https:;\n",[55,4568,4569],{"class":57,"line":77},[55,4570,4571],{},"  style-src 'self' 'unsafe-inline';\n",[18,4573,4574],{},"This prevents XSS attacks from injecting and executing malicious scripts, even if an attacker manages to inject HTML into your page.",[36,4576],{},[13,4578,4580],{"id":4579},"summary-the-30-second-version","Summary: The 30-Second Version",[18,4582,4583],{},"Here is the entire web request lifecycle condensed:",[2527,4585,4586,4592,4598,4604,4610,4616,4622,4628,4634,4640],{},[1399,4587,4588,4591],{},[518,4589,4590],{},"URL parsing:"," Browser breaks the URL into scheme, host, port, path, query",[1399,4593,4594,4597],{},[518,4595,4596],{},"DNS:"," Domain name translated to an IP address through a hierarchy of servers",[1399,4599,4600,4603],{},[518,4601,4602],{},"TCP handshake:"," 3-packet exchange establishes a reliable connection",[1399,4605,4606,4609],{},[518,4607,4608],{},"TLS handshake:"," Encryption keys negotiated, server identity verified (HTTPS only)",[1399,4611,4612,4615],{},[518,4613,4614],{},"HTTP request:"," Browser sends a structured message asking for the resource",[1399,4617,4618,4621],{},[518,4619,4620],{},"Server processing:"," Web server, app server, and database work together to generate a response",[1399,4623,4624,4627],{},[518,4625,4626],{},"HTTP response:"," Server sends status code, headers, and the resource body",[1399,4629,4630,4633],{},[518,4631,4632],{},"Browser rendering:"," Browser parses HTML into DOM, CSS into CSSOM, builds Render Tree, calculates Layout, Paints pixels",[1399,4635,4636,4639],{},[518,4637,4638],{},"Sub-resources:"," Browser fetches CSS, JS, images, fonts referenced in the HTML",[1399,4641,4642,4645],{},[518,4643,4644],{},"Interactivity:"," JavaScript executes, event listeners attach, page becomes interactive",[18,4647,4648],{},"Every single time you visit a webpage, this entire sequence runs, often in under a second. The fact that it works reliably at the scale of billions of requests per second, across unreliable networks, heterogeneous hardware, and wildly different software stacks, is one of the genuine engineering achievements of our time.",[36,4650],{},[13,4652,4654],{"id":4653},"what-to-learn-next","What to Learn Next",[18,4656,4657],{},"If this post sparked your curiosity, here are natural next topics to explore:",[1396,4659,4660,4666,4672,4678,4684,4690,4706],{},[1399,4661,4662,4665],{},[518,4663,4664],{},"HTTP/2 and HTTP/3 in depth:"," multiplexing, server push, QUIC protocol",[1399,4667,4668,4671],{},[518,4669,4670],{},"TLS 1.3:"," how the modern handshake is faster and more secure than TLS 1.2",[1399,4673,4674,4677],{},[518,4675,4676],{},"Service Workers:"," JavaScript that intercepts network requests and enables offline web apps",[1399,4679,4680,4683],{},[518,4681,4682],{},"CDNs:"," how content delivery networks distribute your content globally and reduce latency",[1399,4685,4686,4689],{},[518,4687,4688],{},"DNS security (DNSSEC):"," cryptographic verification of DNS responses",[1399,4691,4692,4695,4696,525,4699,525,4702,4705],{},[518,4693,4694],{},"Web performance optimization:"," Core Web Vitals, resource hints (",[22,4697,4698],{},"preload",[22,4700,4701],{},"prefetch",[22,4703,4704],{},"preconnect","), image optimization",[1399,4707,4708,1391,4711,525,4714,4717],{},[518,4709,4710],{},"HTTP caching strategies:",[22,4712,4713],{},"stale-while-revalidate",[22,4715,4716],{},"immutable",", cache hierarchies",[18,4719,4720],{},"The web is a deep rabbit hole. The further you go, the more elegant the design becomes.",[18,4722,4723],{},"Thanks for reading!",[4725,4726,4727],"style",{},"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}",{"title":51,"searchDepth":64,"depth":64,"links":4729},[4730,4731,4732,4733,4740,4745,4750,4756,4762,4767,4776,4780,4785,4786,4787,4788,4789],{"id":15,"depth":64,"text":16},{"id":40,"depth":64,"text":41},{"id":160,"depth":64,"text":161},{"id":330,"depth":64,"text":331,"children":4734},[4735,4736,4737,4738,4739],{"id":345,"depth":71,"text":346},{"id":352,"depth":71,"text":353},{"id":566,"depth":71,"text":567},{"id":695,"depth":71,"text":696},{"id":817,"depth":71,"text":818},{"id":857,"depth":64,"text":858,"children":4741},[4742,4743,4744],{"id":871,"depth":71,"text":872},{"id":956,"depth":71,"text":957},{"id":1026,"depth":71,"text":1027},{"id":1038,"depth":64,"text":1039,"children":4746},[4747,4748,4749],{"id":1057,"depth":71,"text":1058},{"id":1203,"depth":71,"text":1204},{"id":1258,"depth":71,"text":1259},{"id":1316,"depth":64,"text":1317,"children":4751},[4752,4753,4754,4755],{"id":1326,"depth":71,"text":1327},{"id":1471,"depth":71,"text":1472},{"id":1628,"depth":71,"text":1629},{"id":1679,"depth":71,"text":1680},{"id":1748,"depth":64,"text":1749,"children":4757},[4758,4759,4760,4761],{"id":1758,"depth":71,"text":1759},{"id":1902,"depth":71,"text":1903},{"id":2015,"depth":71,"text":2016},{"id":2376,"depth":71,"text":2377},{"id":2552,"depth":64,"text":2553,"children":4763},[4764,4765,4766],{"id":2559,"depth":71,"text":2560},{"id":2642,"depth":71,"text":2643},{"id":2792,"depth":71,"text":2793},{"id":2926,"depth":64,"text":2927,"children":4768},[4769,4770,4771,4772,4773,4774,4775],{"id":2937,"depth":71,"text":2938},{"id":3110,"depth":71,"text":3111},{"id":3295,"depth":71,"text":3296},{"id":3437,"depth":71,"text":3438},{"id":3566,"depth":71,"text":3567},{"id":3595,"depth":71,"text":3596},{"id":3626,"depth":71,"text":3627},{"id":3701,"depth":64,"text":3702,"children":4777},[4778,4779],{"id":3708,"depth":71,"text":3709},{"id":3832,"depth":71,"text":3833},{"id":3868,"depth":64,"text":3869,"children":4781},[4782,4783,4784],{"id":3875,"depth":71,"text":3876},{"id":3974,"depth":71,"text":3975},{"id":4023,"depth":71,"text":4024},{"id":4104,"depth":64,"text":4105},{"id":4365,"depth":64,"text":4366},{"id":4460,"depth":64,"text":4461},{"id":4579,"depth":64,"text":4580},{"id":4653,"depth":64,"text":4654},"Web Fundamentals","/blog-covers/how-the-web-works.png","2026-04-27","A deep technical walkthrough of what happens between typing a URL and seeing a webpage, covering DNS, TCP/IP, TLS, HTTP, servers, and browser rendering with diagrams and code examples.","md",{},"/posts/how-the-web-works","30 min read",{"title":7,"description":4793},{"loc":4796,"lastmod":4792},"posts/how-the-web-works",[4802,1335,4803,4804,4805,4806,4807,4808,217],"web","dns","tcp-ip","browser","server","networking","tls","z9VggsdjZNcaw-RA7TdXWt8DmRrK0_U5okoGL1lWuhw",{"id":4811,"title":4812,"author":8,"body":4813,"category":4790,"cover":13296,"date":4792,"description":13297,"extension":4794,"featured":67,"meta":13298,"navigation":67,"path":13299,"published":67,"readingTime":13300,"seo":13301,"sitemap":13302,"stem":13303,"tags":13304,"updated":4792,"__hash__":13311},"posts/posts/progressive-web-apps-deep-dive.md","Progressive Web Apps: A Deep Dive",{"type":10,"value":4814,"toc":13236},[4815,4819,4822,4829,4832,4839,4842,4844,4848,4851,4857,4863,4866,4869,4880,4883,4894,4896,4900,4903,4906,4909,4918,4920,4924,4930,4934,5490,5496,5524,5528,5536,5561,5567,5578,5586,5594,5606,5610,5613,5619,5622,5624,5628,5635,5639,5642,5648,5651,5668,5672,5675,5681,5684,5687,5691,5694,5950,5969,5973,5980,6126,6129,6131,6135,6138,6142,6174,6178,6367,6380,6384,6633,6637,6640,6798,6802,6808,7019,7029,7033,7044,7265,7271,7275,7278,8381,8383,8387,8394,8749,8751,8755,8758,8765,8771,9112,9115,9119,9122,9194,9198,9204,9295,9297,9301,9304,9308,9311,9536,9540,9543,9906,9910,9917,10363,10367,10374,10599,10602,10608,10611,10613,10617,10620,10623,10627,10734,10738,10992,10999,11001,11005,11011,11156,11355,11362,11364,11368,11375,11379,11887,11902,11904,11908,11915,11918,11921,12005,12008,12074,12076,12080,12083,12147,12160,12164,12167,12338,12341,12409,12411,12415,12419,12425,12512,12516,12519,12565,12569,12575,12632,12636,12651,12655,12658,12817,12878,12880,12884,12887,12966,12969,12972,12974,12978,12981,13181,13183,13187,13190,13228,13231,13233],[13,4816,4818],{"id":4817},"what-is-a-progressive-web-app","What is a Progressive Web App?",[18,4820,4821],{},"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.",[18,4823,4824,4825,4828],{},"A ",[518,4826,4827],{},"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.",[18,4830,4831],{},"PWAs are not a single technology. They are a combination of three core ingredients working together:",[18,4833,4834],{},[4835,4836],"img",{"alt":4837,"src":4838},"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",[18,4840,4841],{},"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.",[36,4843],{},[13,4845,4847],{"id":4846},"why-pwas-exist-the-problem-they-solve","Why PWAs Exist: The Problem They Solve",[18,4849,4850],{},"Before diving in, it helps to understand what gap PWAs fill.",[18,4852,4853,4856],{},[518,4854,4855],{},"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.",[18,4858,4859,4862],{},[518,4860,4861],{},"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.",[18,4864,4865],{},"PWAs close that gap. They let you write once (HTML, CSS, JavaScript) and get most of the benefits of both worlds.",[18,4867,4868],{},"The business case is clear. When Pinterest built their PWA:",[1396,4870,4871,4874,4877],{},[1399,4872,4873],{},"Time spent on site increased 40%",[1399,4875,4876],{},"User-generated ad revenue increased 44%",[1399,4878,4879],{},"Core engagements increased 60%",[18,4881,4882],{},"When Twitter built Twitter Lite (their PWA):",[1396,4884,4885,4888,4891],{},[1399,4886,4887],{},"65% increase in pages per session",[1399,4889,4890],{},"75% increase in tweets sent",[1399,4892,4893],{},"20% decrease in bounce rate",[36,4895],{},[13,4897,4899],{"id":4898},"pillar-1-https-the-non-negotiable-foundation","Pillar 1: HTTPS (The Non-Negotiable Foundation)",[18,4901,4902],{},"Before anything else: PWAs require HTTPS. Not optional.",[18,4904,4905],{},"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.",[18,4907,4908],{},"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.",[300,4910,4911],{},[18,4912,4913,4914,4917],{},"During development, ",[22,4915,4916],{},"localhost"," is treated as a secure origin. You can test all PWA features locally without a certificate.",[36,4919],{},[13,4921,4923],{"id":4922},"pillar-2-the-web-app-manifest","Pillar 2: The Web App Manifest",[18,4925,4542,4926,4929],{},[518,4927,4928],{},"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.",[343,4931,4933],{"id":4932},"a-complete-manifest-file","A Complete Manifest File",[46,4935,4939],{"className":4936,"code":4937,"language":4938,"meta":51,"style":51},"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",[22,4940,4941,4946,4958,4970,4982,4994,5005,5017,5029,5041,5053,5065,5077,5085,5090,5102,5114,5126,5136,5141,5145,5156,5167,5177,5185,5189,5193,5204,5215,5225,5233,5237,5242,5249,5253,5264,5275,5285,5295,5299,5303,5314,5325,5335,5344,5348,5352,5359,5363,5375,5388,5401,5414,5444,5449,5454,5474,5485],{"__ignoreMap":51},[55,4942,4943],{"class":57,"line":58},[55,4944,4945],{"class":2053},"{\n",[55,4947,4948,4951,4953,4956],{"class":57,"line":64},[55,4949,4950],{"class":2069},"  \"name\"",[55,4952,3323],{"class":2053},[55,4954,4955],{"class":721},"\"Clarity App\"",[55,4957,2250],{"class":2053},[55,4959,4960,4963,4965,4968],{"class":57,"line":71},[55,4961,4962],{"class":2069},"  \"short_name\"",[55,4964,3323],{"class":2053},[55,4966,4967],{"class":721},"\"Clarity\"",[55,4969,2250],{"class":2053},[55,4971,4972,4975,4977,4980],{"class":57,"line":77},[55,4973,4974],{"class":2069},"  \"description\"",[55,4976,3323],{"class":2053},[55,4978,4979],{"class":721},"\"Design, development, and marketing that converts.\"",[55,4981,2250],{"class":2053},[55,4983,4984,4987,4989,4992],{"class":57,"line":83},[55,4985,4986],{"class":2069},"  \"start_url\"",[55,4988,3323],{"class":2053},[55,4990,4991],{"class":721},"\"/\"",[55,4993,2250],{"class":2053},[55,4995,4996,4999,5001,5003],{"class":57,"line":89},[55,4997,4998],{"class":2069},"  \"scope\"",[55,5000,3323],{"class":2053},[55,5002,4991],{"class":721},[55,5004,2250],{"class":2053},[55,5006,5007,5010,5012,5015],{"class":57,"line":95},[55,5008,5009],{"class":2069},"  \"display\"",[55,5011,3323],{"class":2053},[55,5013,5014],{"class":721},"\"standalone\"",[55,5016,2250],{"class":2053},[55,5018,5019,5022,5024,5027],{"class":57,"line":101},[55,5020,5021],{"class":2069},"  \"orientation\"",[55,5023,3323],{"class":2053},[55,5025,5026],{"class":721},"\"portrait-primary\"",[55,5028,2250],{"class":2053},[55,5030,5031,5034,5036,5039],{"class":57,"line":107},[55,5032,5033],{"class":2069},"  \"background_color\"",[55,5035,3323],{"class":2053},[55,5037,5038],{"class":721},"\"#ffffff\"",[55,5040,2250],{"class":2053},[55,5042,5043,5046,5048,5051],{"class":57,"line":113},[55,5044,5045],{"class":2069},"  \"theme_color\"",[55,5047,3323],{"class":2053},[55,5049,5050],{"class":721},"\"#1e3a5f\"",[55,5052,2250],{"class":2053},[55,5054,5055,5058,5060,5063],{"class":57,"line":119},[55,5056,5057],{"class":2069},"  \"lang\"",[55,5059,3323],{"class":2053},[55,5061,5062],{"class":721},"\"en-US\"",[55,5064,2250],{"class":2053},[55,5066,5067,5070,5072,5075],{"class":57,"line":125},[55,5068,5069],{"class":2069},"  \"dir\"",[55,5071,3323],{"class":2053},[55,5073,5074],{"class":721},"\"ltr\"",[55,5076,2250],{"class":2053},[55,5078,5079,5082],{"class":57,"line":131},[55,5080,5081],{"class":2069},"  \"icons\"",[55,5083,5084],{"class":2053},": [\n",[55,5086,5087],{"class":57,"line":137},[55,5088,5089],{"class":2053},"    {\n",[55,5091,5092,5095,5097,5100],{"class":57,"line":143},[55,5093,5094],{"class":2069},"      \"src\"",[55,5096,3323],{"class":2053},[55,5098,5099],{"class":721},"\"/icons/icon-72x72.png\"",[55,5101,2250],{"class":2053},[55,5103,5104,5107,5109,5112],{"class":57,"line":149},[55,5105,5106],{"class":2069},"      \"sizes\"",[55,5108,3323],{"class":2053},[55,5110,5111],{"class":721},"\"72x72\"",[55,5113,2250],{"class":2053},[55,5115,5116,5119,5121,5124],{"class":57,"line":441},[55,5117,5118],{"class":2069},"      \"type\"",[55,5120,3323],{"class":2053},[55,5122,5123],{"class":721},"\"image/png\"",[55,5125,2250],{"class":2053},[55,5127,5128,5131,5133],{"class":57,"line":447},[55,5129,5130],{"class":2069},"      \"purpose\"",[55,5132,3323],{"class":2053},[55,5134,5135],{"class":721},"\"maskable any\"\n",[55,5137,5138],{"class":57,"line":453},[55,5139,5140],{"class":2053},"    },\n",[55,5142,5143],{"class":57,"line":459},[55,5144,5089],{"class":2053},[55,5146,5147,5149,5151,5154],{"class":57,"line":464},[55,5148,5094],{"class":2069},[55,5150,3323],{"class":2053},[55,5152,5153],{"class":721},"\"/icons/icon-192x192.png\"",[55,5155,2250],{"class":2053},[55,5157,5158,5160,5162,5165],{"class":57,"line":470},[55,5159,5106],{"class":2069},[55,5161,3323],{"class":2053},[55,5163,5164],{"class":721},"\"192x192\"",[55,5166,2250],{"class":2053},[55,5168,5169,5171,5173,5175],{"class":57,"line":475},[55,5170,5118],{"class":2069},[55,5172,3323],{"class":2053},[55,5174,5123],{"class":721},[55,5176,2250],{"class":2053},[55,5178,5179,5181,5183],{"class":57,"line":481},[55,5180,5130],{"class":2069},[55,5182,3323],{"class":2053},[55,5184,5135],{"class":721},[55,5186,5187],{"class":57,"line":486},[55,5188,5140],{"class":2053},[55,5190,5191],{"class":57,"line":492},[55,5192,5089],{"class":2053},[55,5194,5195,5197,5199,5202],{"class":57,"line":497},[55,5196,5094],{"class":2069},[55,5198,3323],{"class":2053},[55,5200,5201],{"class":721},"\"/icons/icon-512x512.png\"",[55,5203,2250],{"class":2053},[55,5205,5206,5208,5210,5213],{"class":57,"line":503},[55,5207,5106],{"class":2069},[55,5209,3323],{"class":2053},[55,5211,5212],{"class":721},"\"512x512\"",[55,5214,2250],{"class":2053},[55,5216,5217,5219,5221,5223],{"class":57,"line":508},[55,5218,5118],{"class":2069},[55,5220,3323],{"class":2053},[55,5222,5123],{"class":721},[55,5224,2250],{"class":2053},[55,5226,5227,5229,5231],{"class":57,"line":2347},[55,5228,5130],{"class":2069},[55,5230,3323],{"class":2053},[55,5232,5135],{"class":721},[55,5234,5235],{"class":57,"line":2353},[55,5236,1962],{"class":2053},[55,5238,5239],{"class":57,"line":2368},[55,5240,5241],{"class":2053},"  ],\n",[55,5243,5244,5247],{"class":57,"line":3094},[55,5245,5246],{"class":2069},"  \"screenshots\"",[55,5248,5084],{"class":2053},[55,5250,5251],{"class":57,"line":3099},[55,5252,5089],{"class":2053},[55,5254,5255,5257,5259,5262],{"class":57,"line":3104},[55,5256,5094],{"class":2069},[55,5258,3323],{"class":2053},[55,5260,5261],{"class":721},"\"/screenshots/desktop.png\"",[55,5263,2250],{"class":2053},[55,5265,5266,5268,5270,5273],{"class":57,"line":4281},[55,5267,5106],{"class":2069},[55,5269,3323],{"class":2053},[55,5271,5272],{"class":721},"\"1280x720\"",[55,5274,2250],{"class":2053},[55,5276,5277,5279,5281,5283],{"class":57,"line":4287},[55,5278,5118],{"class":2069},[55,5280,3323],{"class":2053},[55,5282,5123],{"class":721},[55,5284,2250],{"class":2053},[55,5286,5287,5290,5292],{"class":57,"line":4293},[55,5288,5289],{"class":2069},"      \"form_factor\"",[55,5291,3323],{"class":2053},[55,5293,5294],{"class":721},"\"wide\"\n",[55,5296,5297],{"class":57,"line":4298},[55,5298,5140],{"class":2053},[55,5300,5301],{"class":57,"line":4304},[55,5302,5089],{"class":2053},[55,5304,5305,5307,5309,5312],{"class":57,"line":4309},[55,5306,5094],{"class":2069},[55,5308,3323],{"class":2053},[55,5310,5311],{"class":721},"\"/screenshots/mobile.png\"",[55,5313,2250],{"class":2053},[55,5315,5316,5318,5320,5323],{"class":57,"line":4315},[55,5317,5106],{"class":2069},[55,5319,3323],{"class":2053},[55,5321,5322],{"class":721},"\"390x844\"",[55,5324,2250],{"class":2053},[55,5326,5327,5329,5331,5333],{"class":57,"line":4321},[55,5328,5118],{"class":2069},[55,5330,3323],{"class":2053},[55,5332,5123],{"class":721},[55,5334,2250],{"class":2053},[55,5336,5337,5339,5341],{"class":57,"line":4326},[55,5338,5289],{"class":2069},[55,5340,3323],{"class":2053},[55,5342,5343],{"class":721},"\"narrow\"\n",[55,5345,5346],{"class":57,"line":4332},[55,5347,1962],{"class":2053},[55,5349,5350],{"class":57,"line":4337},[55,5351,5241],{"class":2053},[55,5353,5354,5357],{"class":57,"line":4343},[55,5355,5356],{"class":2069},"  \"shortcuts\"",[55,5358,5084],{"class":2053},[55,5360,5361],{"class":57,"line":4348},[55,5362,5089],{"class":2053},[55,5364,5365,5368,5370,5373],{"class":57,"line":4354},[55,5366,5367],{"class":2069},"      \"name\"",[55,5369,3323],{"class":2053},[55,5371,5372],{"class":721},"\"New Project\"",[55,5374,2250],{"class":2053},[55,5376,5378,5381,5383,5386],{"class":57,"line":5377},50,[55,5379,5380],{"class":2069},"      \"short_name\"",[55,5382,3323],{"class":2053},[55,5384,5385],{"class":721},"\"New\"",[55,5387,2250],{"class":2053},[55,5389,5391,5394,5396,5399],{"class":57,"line":5390},51,[55,5392,5393],{"class":2069},"      \"description\"",[55,5395,3323],{"class":2053},[55,5397,5398],{"class":721},"\"Start a new project\"",[55,5400,2250],{"class":2053},[55,5402,5404,5407,5409,5412],{"class":57,"line":5403},52,[55,5405,5406],{"class":2069},"      \"url\"",[55,5408,3323],{"class":2053},[55,5410,5411],{"class":721},"\"/projects/new\"",[55,5413,2250],{"class":2053},[55,5415,5417,5420,5423,5426,5428,5431,5433,5436,5438,5441],{"class":57,"line":5416},53,[55,5418,5419],{"class":2069},"      \"icons\"",[55,5421,5422],{"class":2053},": [{ ",[55,5424,5425],{"class":2069},"\"src\"",[55,5427,3323],{"class":2053},[55,5429,5430],{"class":721},"\"/icons/new-project.png\"",[55,5432,525],{"class":2053},[55,5434,5435],{"class":2069},"\"sizes\"",[55,5437,3323],{"class":2053},[55,5439,5440],{"class":721},"\"96x96\"",[55,5442,5443],{"class":2053}," }]\n",[55,5445,5447],{"class":57,"line":5446},54,[55,5448,1962],{"class":2053},[55,5450,5452],{"class":57,"line":5451},55,[55,5453,5241],{"class":2053},[55,5455,5457,5460,5463,5466,5468,5471],{"class":57,"line":5456},56,[55,5458,5459],{"class":2069},"  \"categories\"",[55,5461,5462],{"class":2053},": [",[55,5464,5465],{"class":721},"\"business\"",[55,5467,525],{"class":2053},[55,5469,5470],{"class":721},"\"productivity\"",[55,5472,5473],{"class":2053},"],\n",[55,5475,5477,5480,5482],{"class":57,"line":5476},57,[55,5478,5479],{"class":2069},"  \"prefer_related_applications\"",[55,5481,3323],{"class":2053},[55,5483,5484],{"class":2069},"false\n",[55,5486,5488],{"class":57,"line":5487},58,[55,5489,2005],{"class":2053},[18,5491,5492,5493,5495],{},"Link it from every HTML page in the ",[22,5494,3433],{},":",[46,5497,5499],{"className":3121,"code":5498,"language":3123,"meta":51,"style":51},"\u003Clink rel=\"manifest\" href=\"/manifest.json\" />\n",[22,5500,5501],{"__ignoreMap":51},[55,5502,5503,5505,5507,5509,5511,5514,5516,5518,5521],{"class":57,"line":58},[55,5504,3145],{"class":2053},[55,5506,4044],{"class":3133},[55,5508,4047],{"class":717},[55,5510,3475],{"class":2053},[55,5512,5513],{"class":721},"\"manifest\"",[55,5515,4055],{"class":717},[55,5517,3475],{"class":2053},[55,5519,5520],{"class":721},"\"/manifest.json\"",[55,5522,5523],{"class":2053}," />\n",[343,5525,5527],{"id":5526},"key-manifest-fields-explained","Key Manifest Fields Explained",[18,5529,5530,5535],{},[518,5531,5532],{},[22,5533,5534],{},"display"," controls how the app opens when launched from the home screen:",[46,5537,5539],{"className":48,"code":5538,"language":50,"meta":51,"style":51},"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",[22,5540,5541,5546,5551,5556],{"__ignoreMap":51},[55,5542,5543],{"class":57,"line":58},[55,5544,5545],{},"display: \"browser\"     - opens in a regular browser tab (no PWA feel)\n",[55,5547,5548],{"class":57,"line":64},[55,5549,5550],{},"display: \"minimal-ui\"  - browser chrome, but minimal (back button, URL)\n",[55,5552,5553],{"class":57,"line":71},[55,5554,5555],{},"display: \"standalone\"  - looks like a native app (no URL bar)\n",[55,5557,5558],{"class":57,"line":77},[55,5559,5560],{},"display: \"fullscreen\"  - covers entire screen (games, media players)\n",[18,5562,5563,5564,5566],{},"For most apps, ",[22,5565,5014],{}," is the right choice. It removes the browser chrome so the experience is indistinguishable from a native app.",[18,5568,5569,5574,5575,532],{},[518,5570,5571],{},[22,5572,5573],{},"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: ",[22,5576,5577],{},"\"start_url\": \"/?source=pwa\"",[18,5579,5580,5585],{},[518,5581,5582],{},[22,5583,5584],{},"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.",[18,5587,5588,5593],{},[518,5589,5590],{},[22,5591,5592],{},"theme_color"," sets the color of the browser address bar and the system status bar on Android. It should match your primary brand color.",[18,5595,5596,5601,5602,5605],{},[518,5597,5598],{},[22,5599,5600],{},"icons"," need at minimum 192x192 and 512x512 PNG icons. The ",[22,5603,5604],{},"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.",[343,5607,5609],{"id":5608},"the-maskable-icon-safe-zone","The Maskable Icon Safe Zone",[18,5611,5612],{},"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:",[18,5614,5615],{},[4835,5616],{"alt":5617,"src":5618},"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",[18,5620,5621],{},"Keep your logo and important content within the safe zone. Use maskable.app to preview your icon against all Android mask shapes before shipping.",[36,5623],{},[13,5625,5627],{"id":5626},"pillar-3-the-service-worker","Pillar 3: The Service Worker",[18,5629,5630,5631,5634],{},"This is the heart of everything. A ",[518,5632,5633],{},"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.",[343,5636,5638],{"id":5637},"the-service-worker-mental-model","The Service Worker Mental Model",[18,5640,5641],{},"Think of a service worker as a programmable network proxy sitting between your web page and the internet:",[18,5643,5644],{},[4835,5645],{"alt":5646,"src":5647},"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",[18,5649,5650],{},"The service worker can:",[1396,5652,5653,5656,5659,5662,5665],{},[1399,5654,5655],{},"Serve responses from a local cache (offline support)",[1399,5657,5658],{},"Fetch from the network and update the cache in the background",[1399,5660,5661],{},"Intercept and modify requests before they hit the network",[1399,5663,5664],{},"Receive push notifications from a server (even when the page is closed)",[1399,5666,5667],{},"Run background sync tasks when connectivity is restored",[343,5669,5671],{"id":5670},"service-worker-lifecycle","Service Worker Lifecycle",[18,5673,5674],{},"Understanding the lifecycle is critical. Many PWA bugs come from misunderstanding when a service worker takes effect.",[18,5676,5677],{},[4835,5678],{"alt":5679,"src":5680},"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",[18,5682,5683],{},"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.",[18,5685,5686],{},"During development, use Chrome DevTools > Application > Service Workers > \"Update on reload\" to bypass this.",[343,5688,5690],{"id":5689},"registering-a-service-worker","Registering a Service Worker",[18,5692,5693],{},"Registration happens from your main JavaScript file, not from the service worker itself:",[46,5695,5697],{"className":2025,"code":5696,"language":2027,"meta":51,"style":51},"// 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",[22,5698,5699,5704,5708,5720,5740,5755,5762,5767,5771,5779,5804,5812,5817,5821,5842,5855,5870,5874,5878,5892,5903,5918,5922,5926,5930,5935],{"__ignoreMap":51},[55,5700,5701],{"class":57,"line":58},[55,5702,5703],{"class":711},"// main.js (runs in the page, not in the service worker)\n",[55,5705,5706],{"class":57,"line":64},[55,5707,68],{"emptyLinePlaceholder":67},[55,5709,5710,5712,5714,5717],{"class":57,"line":71},[55,5711,2044],{"class":2043},[55,5713,2047],{"class":2043},[55,5715,5716],{"class":717}," registerServiceWorker",[55,5718,5719],{"class":2053},"() {\n",[55,5721,5722,5725,5727,5729,5731,5734,5737],{"class":57,"line":77},[55,5723,5724],{"class":2043},"  if",[55,5726,2093],{"class":2053},[55,5728,2096],{"class":2043},[55,5730,2054],{"class":2053},[55,5732,5733],{"class":721},"'serviceWorker'",[55,5735,5736],{"class":2043}," in",[55,5738,5739],{"class":2053}," navigator)) {\n",[55,5741,5742,5745,5748,5750,5753],{"class":57,"line":83},[55,5743,5744],{"class":2053},"    console.",[55,5746,5747],{"class":717},"log",[55,5749,2054],{"class":2053},[55,5751,5752],{"class":721},"'Service workers not supported'",[55,5754,2178],{"class":2053},[55,5756,5757,5759],{"class":57,"line":89},[55,5758,2356],{"class":2043},[55,5760,5761],{"class":2053},";\n",[55,5763,5764],{"class":57,"line":95},[55,5765,5766],{"class":2053},"  }\n",[55,5768,5769],{"class":57,"line":101},[55,5770,68],{"emptyLinePlaceholder":67},[55,5772,5773,5776],{"class":57,"line":107},[55,5774,5775],{"class":2043},"  try",[55,5777,5778],{"class":2053}," {\n",[55,5780,5781,5783,5786,5788,5790,5793,5796,5798,5801],{"class":57,"line":113},[55,5782,2066],{"class":2043},[55,5784,5785],{"class":2069}," registration",[55,5787,2073],{"class":2043},[55,5789,2158],{"class":2043},[55,5791,5792],{"class":2053}," navigator.serviceWorker.",[55,5794,5795],{"class":717},"register",[55,5797,2054],{"class":2053},[55,5799,5800],{"class":721},"'/sw.js'",[55,5802,5803],{"class":2053},", {\n",[55,5805,5806,5809],{"class":57,"line":119},[55,5807,5808],{"class":2053},"      scope: ",[55,5810,5811],{"class":721},"'/'\n",[55,5813,5814],{"class":57,"line":125},[55,5815,5816],{"class":2053},"    });\n",[55,5818,5819],{"class":57,"line":131},[55,5820,68],{"emptyLinePlaceholder":67},[55,5822,5823,5826,5829,5831,5834,5837,5840],{"class":57,"line":137},[55,5824,5825],{"class":2053},"    registration.",[55,5827,5828],{"class":717},"addEventListener",[55,5830,2054],{"class":2053},[55,5832,5833],{"class":721},"'updatefound'",[55,5835,5836],{"class":2053},", () ",[55,5838,5839],{"class":2043},"=>",[55,5841,5778],{"class":2053},[55,5843,5844,5847,5850,5852],{"class":57,"line":143},[55,5845,5846],{"class":2043},"      const",[55,5848,5849],{"class":2069}," newWorker",[55,5851,2073],{"class":2043},[55,5853,5854],{"class":2053}," registration.installing;\n",[55,5856,5857,5860,5862,5864,5867],{"class":57,"line":149},[55,5858,5859],{"class":2053},"      console.",[55,5861,5747],{"class":717},[55,5863,2054],{"class":2053},[55,5865,5866],{"class":721},"'New service worker installing:'",[55,5868,5869],{"class":2053},", newWorker);\n",[55,5871,5872],{"class":57,"line":441},[55,5873,5816],{"class":2053},[55,5875,5876],{"class":57,"line":447},[55,5877,68],{"emptyLinePlaceholder":67},[55,5879,5880,5882,5884,5886,5889],{"class":57,"line":453},[55,5881,5744],{"class":2053},[55,5883,5747],{"class":717},[55,5885,2054],{"class":2053},[55,5887,5888],{"class":721},"'Service worker registered, scope:'",[55,5890,5891],{"class":2053},", registration.scope);\n",[55,5893,5894,5897,5900],{"class":57,"line":459},[55,5895,5896],{"class":2053},"  } ",[55,5898,5899],{"class":2043},"catch",[55,5901,5902],{"class":2053}," (error) {\n",[55,5904,5905,5907,5910,5912,5915],{"class":57,"line":464},[55,5906,5744],{"class":2053},[55,5908,5909],{"class":717},"error",[55,5911,2054],{"class":2053},[55,5913,5914],{"class":721},"'Service worker registration failed:'",[55,5916,5917],{"class":2053},", error);\n",[55,5919,5920],{"class":57,"line":470},[55,5921,5766],{"class":2053},[55,5923,5924],{"class":57,"line":475},[55,5925,2005],{"class":2053},[55,5927,5928],{"class":57,"line":481},[55,5929,68],{"emptyLinePlaceholder":67},[55,5931,5932],{"class":57,"line":486},[55,5933,5934],{"class":711},"// Register after the page loads\n",[55,5936,5937,5940,5942,5944,5947],{"class":57,"line":492},[55,5938,5939],{"class":2053},"window.",[55,5941,5828],{"class":717},[55,5943,2054],{"class":2053},[55,5945,5946],{"class":721},"'load'",[55,5948,5949],{"class":2053},", registerServiceWorker);\n",[18,5951,4542,5952,5954,5955,5958,5959,5961,5962,5965,5966,532],{},[22,5953,5584],{}," defaults to the directory containing the service worker file. A service worker at ",[22,5956,5957],{},"/sw.js"," controls all pages at ",[22,5960,3857],{},". A service worker at ",[22,5963,5964],{},"/blog/sw.js"," only controls pages under ",[22,5967,5968],{},"/blog/",[343,5970,5972],{"id":5971},"the-fetch-event-intercepting-requests","The Fetch Event: Intercepting Requests",[18,5974,5975,5976,5979],{},"The most important event in a service worker is ",[22,5977,5978],{},"fetch",". Every network request from your page flows through it:",[46,5981,5983],{"className":2025,"code":5982,"language":2027,"meta":51,"style":51},"// 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",[22,5984,5985,5990,5994,6019,6024,6029,6033,6043,6048,6074,6082,6092,6097,6111,6116,6121],{"__ignoreMap":51},[55,5986,5987],{"class":57,"line":58},[55,5988,5989],{"class":711},"// sw.js\n",[55,5991,5992],{"class":57,"line":64},[55,5993,68],{"emptyLinePlaceholder":67},[55,5995,5996,5999,6001,6003,6006,6009,6012,6015,6017],{"class":57,"line":71},[55,5997,5998],{"class":2053},"self.",[55,6000,5828],{"class":717},[55,6002,2054],{"class":2053},[55,6004,6005],{"class":721},"'fetch'",[55,6007,6008],{"class":2053},", (",[55,6010,6011],{"class":2057},"event",[55,6013,6014],{"class":2053},") ",[55,6016,5839],{"class":2043},[55,6018,5778],{"class":2053},[55,6020,6021],{"class":57,"line":77},[55,6022,6023],{"class":711},"  // event.request is the original Request object\n",[55,6025,6026],{"class":57,"line":83},[55,6027,6028],{"class":711},"  // event.respondWith() lets you override the response\n",[55,6030,6031],{"class":57,"line":89},[55,6032,68],{"emptyLinePlaceholder":67},[55,6034,6035,6038,6041],{"class":57,"line":95},[55,6036,6037],{"class":2053},"  event.",[55,6039,6040],{"class":717},"respondWith",[55,6042,2242],{"class":2053},[55,6044,6045],{"class":57,"line":101},[55,6046,6047],{"class":711},"    // Try the cache first, fall back to network\n",[55,6049,6050,6053,6056,6059,6062,6065,6068,6070,6072],{"class":57,"line":107},[55,6051,6052],{"class":2053},"    caches.",[55,6054,6055],{"class":717},"match",[55,6057,6058],{"class":2053},"(event.request).",[55,6060,6061],{"class":717},"then",[55,6063,6064],{"class":2053},"((",[55,6066,6067],{"class":2057},"cachedResponse",[55,6069,6014],{"class":2053},[55,6071,5839],{"class":2043},[55,6073,5778],{"class":2053},[55,6075,6076,6079],{"class":57,"line":113},[55,6077,6078],{"class":2043},"      if",[55,6080,6081],{"class":2053}," (cachedResponse) {\n",[55,6083,6084,6086,6089],{"class":57,"line":119},[55,6085,2116],{"class":2043},[55,6087,6088],{"class":2053}," cachedResponse; ",[55,6090,6091],{"class":711},"// serve from cache\n",[55,6093,6094],{"class":57,"line":125},[55,6095,6096],{"class":2053},"      }\n",[55,6098,6099,6102,6105,6108],{"class":57,"line":131},[55,6100,6101],{"class":2043},"      return",[55,6103,6104],{"class":717}," fetch",[55,6106,6107],{"class":2053},"(event.request); ",[55,6109,6110],{"class":711},"// fall back to network\n",[55,6112,6113],{"class":57,"line":137},[55,6114,6115],{"class":2053},"    })\n",[55,6117,6118],{"class":57,"line":143},[55,6119,6120],{"class":2053},"  );\n",[55,6122,6123],{"class":57,"line":149},[55,6124,6125],{"class":2053},"});\n",[18,6127,6128],{},"This is the simplest possible strategy: cache first, network fallback. But different resources need different strategies.",[36,6130],{},[13,6132,6134],{"id":6133},"caching-strategies","Caching Strategies",[18,6136,6137],{},"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.",[343,6139,6141],{"id":6140},"the-five-core-strategies","The Five Core Strategies",[2527,6143,6144,6150,6156,6162,6168],{},[1399,6145,6146,6149],{},[518,6147,6148],{},"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).",[1399,6151,6152,6155],{},[518,6153,6154],{},"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.",[1399,6157,6158,6161],{},[518,6159,6160],{},"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).",[1399,6163,6164,6167],{},[518,6165,6166],{},"Network Only:"," Always fetch from the network. Never cache. Best for analytics pings, POST requests, and payments.",[1399,6169,6170,6173],{},[518,6171,6172],{},"Cache Only:"," Serve exclusively from cache. Never touch the network. Best for assets pre-cached at install time as part of the app shell.",[343,6175,6177],{"id":6176},"strategy-1-cache-first","Strategy 1: Cache First",[46,6179,6181],{"className":2025,"code":6180,"language":2027,"meta":51,"style":51},"// 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",[22,6182,6183,6187,6202,6206,6226,6234,6266,6284,6297,6301,6315,6320,6327,6344,6348,6355,6359,6363],{"__ignoreMap":51},[55,6184,6185],{"class":57,"line":58},[55,6186,5989],{"class":711},[55,6188,6189,6192,6195,6197,6200],{"class":57,"line":64},[55,6190,6191],{"class":2043},"const",[55,6193,6194],{"class":2069}," STATIC_CACHE",[55,6196,2073],{"class":2043},[55,6198,6199],{"class":721}," 'static-v1'",[55,6201,5761],{"class":2053},[55,6203,6204],{"class":57,"line":71},[55,6205,68],{"emptyLinePlaceholder":67},[55,6207,6208,6210,6212,6214,6216,6218,6220,6222,6224],{"class":57,"line":77},[55,6209,5998],{"class":2053},[55,6211,5828],{"class":717},[55,6213,2054],{"class":2053},[55,6215,6005],{"class":721},[55,6217,6008],{"class":2053},[55,6219,6011],{"class":2057},[55,6221,6014],{"class":2053},[55,6223,5839],{"class":2043},[55,6225,5778],{"class":2053},[55,6227,6228,6230,6232],{"class":57,"line":83},[55,6229,6037],{"class":2053},[55,6231,6040],{"class":717},[55,6233,2242],{"class":2053},[55,6235,6236,6238,6241,6243,6246,6249,6251,6253,6255,6257,6260,6262,6264],{"class":57,"line":89},[55,6237,6052],{"class":2053},[55,6239,6240],{"class":717},"open",[55,6242,2054],{"class":2053},[55,6244,6245],{"class":2069},"STATIC_CACHE",[55,6247,6248],{"class":2053},").",[55,6250,6061],{"class":717},[55,6252,2054],{"class":2053},[55,6254,2044],{"class":2043},[55,6256,2093],{"class":2053},[55,6258,6259],{"class":2057},"cache",[55,6261,6014],{"class":2053},[55,6263,5839],{"class":2043},[55,6265,5778],{"class":2053},[55,6267,6268,6270,6272,6274,6276,6279,6281],{"class":57,"line":95},[55,6269,5846],{"class":2043},[55,6271,2153],{"class":2069},[55,6273,2073],{"class":2043},[55,6275,2158],{"class":2043},[55,6277,6278],{"class":2053}," cache.",[55,6280,6055],{"class":717},[55,6282,6283],{"class":2053},"(event.request);\n",[55,6285,6286,6288,6291,6294],{"class":57,"line":101},[55,6287,6078],{"class":2043},[55,6289,6290],{"class":2053}," (cached) ",[55,6292,6293],{"class":2043},"return",[55,6295,6296],{"class":2053}," cached;\n",[55,6298,6299],{"class":57,"line":107},[55,6300,68],{"emptyLinePlaceholder":67},[55,6302,6303,6305,6307,6309,6311,6313],{"class":57,"line":113},[55,6304,5846],{"class":2043},[55,6306,2119],{"class":2069},[55,6308,2073],{"class":2043},[55,6310,2158],{"class":2043},[55,6312,6104],{"class":717},[55,6314,6283],{"class":2053},[55,6316,6317],{"class":57,"line":119},[55,6318,6319],{"class":711},"      // Only cache successful responses\n",[55,6321,6322,6324],{"class":57,"line":125},[55,6323,6078],{"class":2043},[55,6325,6326],{"class":2053}," (response.ok) {\n",[55,6328,6329,6332,6335,6338,6341],{"class":57,"line":131},[55,6330,6331],{"class":2053},"        cache.",[55,6333,6334],{"class":717},"put",[55,6336,6337],{"class":2053},"(event.request, response.",[55,6339,6340],{"class":717},"clone",[55,6342,6343],{"class":2053},"());\n",[55,6345,6346],{"class":57,"line":137},[55,6347,6096],{"class":2053},[55,6349,6350,6352],{"class":57,"line":143},[55,6351,6101],{"class":2043},[55,6353,6354],{"class":2053}," response;\n",[55,6356,6357],{"class":57,"line":149},[55,6358,6115],{"class":2053},[55,6360,6361],{"class":57,"line":441},[55,6362,6120],{"class":2053},[55,6364,6365],{"class":57,"line":447},[55,6366,6125],{"class":2053},[18,6368,2008,6369,6372,6373,6376,6377,6379],{},[22,6370,6371],{},"response.clone()",". A Response body can only be consumed once (it is a stream). If you ",[22,6374,6375],{},"cache.put()"," and then ",[22,6378,6293],{}," the same response, the body will already be consumed and the page gets an empty response. Always clone before caching.",[343,6381,6383],{"id":6382},"strategy-2-network-first","Strategy 2: Network First",[46,6385,6387],{"className":2025,"code":6386,"language":2027,"meta":51,"style":51},"// 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",[22,6388,6389,6393,6407,6411,6426,6432,6447,6453,6476,6490,6494,6500,6508,6513,6529,6539,6543,6548,6563,6567,6571,6575,6595,6610,6625,6629],{"__ignoreMap":51},[55,6390,6391],{"class":57,"line":58},[55,6392,5989],{"class":711},[55,6394,6395,6397,6400,6402,6405],{"class":57,"line":64},[55,6396,6191],{"class":2043},[55,6398,6399],{"class":2069}," DYNAMIC_CACHE",[55,6401,2073],{"class":2043},[55,6403,6404],{"class":721}," 'dynamic-v1'",[55,6406,5761],{"class":2053},[55,6408,6409],{"class":57,"line":71},[55,6410,68],{"emptyLinePlaceholder":67},[55,6412,6413,6415,6417,6420,6422,6424],{"class":57,"line":77},[55,6414,2044],{"class":2043},[55,6416,2047],{"class":2043},[55,6418,6419],{"class":717}," networkFirst",[55,6421,2054],{"class":2053},[55,6423,2058],{"class":2057},[55,6425,2061],{"class":2053},[55,6427,6428,6430],{"class":57,"line":83},[55,6429,5775],{"class":2043},[55,6431,5778],{"class":2053},[55,6433,6434,6436,6438,6440,6442,6444],{"class":57,"line":89},[55,6435,2066],{"class":2043},[55,6437,2119],{"class":2069},[55,6439,2073],{"class":2043},[55,6441,2158],{"class":2043},[55,6443,6104],{"class":717},[55,6445,6446],{"class":2053},"(request);\n",[55,6448,6449,6451],{"class":57,"line":95},[55,6450,2090],{"class":2043},[55,6452,6326],{"class":2053},[55,6454,6455,6457,6460,6462,6464,6467,6469,6471,6474],{"class":57,"line":101},[55,6456,5846],{"class":2043},[55,6458,6459],{"class":2069}," cache",[55,6461,2073],{"class":2043},[55,6463,2158],{"class":2043},[55,6465,6466],{"class":2053}," caches.",[55,6468,6240],{"class":717},[55,6470,2054],{"class":2053},[55,6472,6473],{"class":2069},"DYNAMIC_CACHE",[55,6475,2178],{"class":2053},[55,6477,6478,6481,6483,6486,6488],{"class":57,"line":107},[55,6479,6480],{"class":2053},"      cache.",[55,6482,6334],{"class":717},[55,6484,6485],{"class":2053},"(request, response.",[55,6487,6340],{"class":717},[55,6489,6343],{"class":2053},[55,6491,6492],{"class":57,"line":113},[55,6493,1962],{"class":2053},[55,6495,6496,6498],{"class":57,"line":119},[55,6497,2356],{"class":2043},[55,6499,6354],{"class":2053},[55,6501,6502,6504,6506],{"class":57,"line":125},[55,6503,5896],{"class":2053},[55,6505,5899],{"class":2043},[55,6507,5902],{"class":2053},[55,6509,6510],{"class":57,"line":131},[55,6511,6512],{"class":711},"    // Network failed: try cache\n",[55,6514,6515,6517,6519,6521,6523,6525,6527],{"class":57,"line":137},[55,6516,2066],{"class":2043},[55,6518,2153],{"class":2069},[55,6520,2073],{"class":2043},[55,6522,2158],{"class":2043},[55,6524,6466],{"class":2053},[55,6526,6055],{"class":717},[55,6528,6446],{"class":2053},[55,6530,6531,6533,6535,6537],{"class":57,"line":143},[55,6532,2090],{"class":2043},[55,6534,6290],{"class":2053},[55,6536,6293],{"class":2043},[55,6538,6296],{"class":2053},[55,6540,6541],{"class":57,"line":149},[55,6542,68],{"emptyLinePlaceholder":67},[55,6544,6545],{"class":57,"line":441},[55,6546,6547],{"class":711},"    // Nothing in cache either: return offline fallback\n",[55,6549,6550,6552,6554,6556,6558,6561],{"class":57,"line":447},[55,6551,2356],{"class":2043},[55,6553,6466],{"class":2053},[55,6555,6055],{"class":717},[55,6557,2054],{"class":2053},[55,6559,6560],{"class":721},"'/offline.html'",[55,6562,2178],{"class":2053},[55,6564,6565],{"class":57,"line":453},[55,6566,5766],{"class":2053},[55,6568,6569],{"class":57,"line":459},[55,6570,2005],{"class":2053},[55,6572,6573],{"class":57,"line":464},[55,6574,68],{"emptyLinePlaceholder":67},[55,6576,6577,6579,6581,6583,6585,6587,6589,6591,6593],{"class":57,"line":470},[55,6578,5998],{"class":2053},[55,6580,5828],{"class":717},[55,6582,2054],{"class":2053},[55,6584,6005],{"class":721},[55,6586,6008],{"class":2053},[55,6588,6011],{"class":2057},[55,6590,6014],{"class":2053},[55,6592,5839],{"class":2043},[55,6594,5778],{"class":2053},[55,6596,6597,6599,6602,6605,6608],{"class":57,"line":475},[55,6598,5724],{"class":2043},[55,6600,6601],{"class":2053}," (event.request.mode ",[55,6603,6604],{"class":2043},"===",[55,6606,6607],{"class":721}," 'navigate'",[55,6609,2061],{"class":2053},[55,6611,6612,6615,6617,6619,6622],{"class":57,"line":481},[55,6613,6614],{"class":2053},"    event.",[55,6616,6040],{"class":717},[55,6618,2054],{"class":2053},[55,6620,6621],{"class":717},"networkFirst",[55,6623,6624],{"class":2053},"(event.request));\n",[55,6626,6627],{"class":57,"line":486},[55,6628,5766],{"class":2053},[55,6630,6631],{"class":57,"line":492},[55,6632,6125],{"class":2053},[343,6634,6636],{"id":6635},"strategy-3-stale-while-revalidate","Strategy 3: Stale While Revalidate",[18,6638,6639],{},"This strategy gives you the best of both worlds for most content: instant response from cache AND fresh data for next time.",[46,6641,6643],{"className":2025,"code":6642,"language":2027,"meta":51,"style":51},"// 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",[22,6644,6645,6649,6664,6686,6702,6706,6711,6738,6744,6756,6760,6766,6771,6775,6780,6794],{"__ignoreMap":51},[55,6646,6647],{"class":57,"line":58},[55,6648,5989],{"class":711},[55,6650,6651,6653,6655,6658,6660,6662],{"class":57,"line":64},[55,6652,2044],{"class":2043},[55,6654,2047],{"class":2043},[55,6656,6657],{"class":717}," staleWhileRevalidate",[55,6659,2054],{"class":2053},[55,6661,2058],{"class":2057},[55,6663,2061],{"class":2053},[55,6665,6666,6669,6671,6673,6675,6677,6679,6681,6684],{"class":57,"line":71},[55,6667,6668],{"class":2043},"  const",[55,6670,6459],{"class":2069},[55,6672,2073],{"class":2043},[55,6674,2158],{"class":2043},[55,6676,6466],{"class":2053},[55,6678,6240],{"class":717},[55,6680,2054],{"class":2053},[55,6682,6683],{"class":721},"'swr-cache-v1'",[55,6685,2178],{"class":2053},[55,6687,6688,6690,6692,6694,6696,6698,6700],{"class":57,"line":77},[55,6689,6668],{"class":2043},[55,6691,2153],{"class":2069},[55,6693,2073],{"class":2043},[55,6695,2158],{"class":2043},[55,6697,6278],{"class":2053},[55,6699,6055],{"class":717},[55,6701,6446],{"class":2053},[55,6703,6704],{"class":57,"line":83},[55,6705,68],{"emptyLinePlaceholder":67},[55,6707,6708],{"class":57,"line":89},[55,6709,6710],{"class":711},"  // Kick off a network fetch in the background regardless\n",[55,6712,6713,6715,6718,6720,6722,6725,6727,6729,6732,6734,6736],{"class":57,"line":95},[55,6714,6668],{"class":2043},[55,6716,6717],{"class":2069}," networkFetch",[55,6719,2073],{"class":2043},[55,6721,6104],{"class":717},[55,6723,6724],{"class":2053},"(request).",[55,6726,6061],{"class":717},[55,6728,6064],{"class":2053},[55,6730,6731],{"class":2057},"response",[55,6733,6014],{"class":2053},[55,6735,5839],{"class":2043},[55,6737,5778],{"class":2053},[55,6739,6740,6742],{"class":57,"line":101},[55,6741,2090],{"class":2043},[55,6743,6326],{"class":2053},[55,6745,6746,6748,6750,6752,6754],{"class":57,"line":107},[55,6747,6480],{"class":2053},[55,6749,6334],{"class":717},[55,6751,6485],{"class":2053},[55,6753,6340],{"class":717},[55,6755,6343],{"class":2053},[55,6757,6758],{"class":57,"line":113},[55,6759,1962],{"class":2053},[55,6761,6762,6764],{"class":57,"line":119},[55,6763,2356],{"class":2043},[55,6765,6354],{"class":2053},[55,6767,6768],{"class":57,"line":125},[55,6769,6770],{"class":2053},"  });\n",[55,6772,6773],{"class":57,"line":131},[55,6774,68],{"emptyLinePlaceholder":67},[55,6776,6777],{"class":57,"line":137},[55,6778,6779],{"class":711},"  // Return cached immediately if available, otherwise wait for network\n",[55,6781,6782,6785,6788,6791],{"class":57,"line":143},[55,6783,6784],{"class":2043},"  return",[55,6786,6787],{"class":2053}," cached ",[55,6789,6790],{"class":2043},"??",[55,6792,6793],{"class":2053}," networkFetch;\n",[55,6795,6796],{"class":57,"line":149},[55,6797,2005],{"class":2053},[343,6799,6801],{"id":6800},"pre-caching-the-app-shell","Pre-caching: The App Shell",[18,6803,4542,6804,6807],{},[518,6805,6806],{},"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.",[46,6809,6811],{"className":2025,"code":6810,"language":2027,"meta":51,"style":51},"// 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",[22,6812,6813,6817,6831,6835,6840,6852,6859,6866,6873,6880,6887,6894,6899,6903,6924,6933,6958,6971,6987,6991,6995,6999,7004,7015],{"__ignoreMap":51},[55,6814,6815],{"class":57,"line":58},[55,6816,5989],{"class":711},[55,6818,6819,6821,6824,6826,6829],{"class":57,"line":64},[55,6820,6191],{"class":2043},[55,6822,6823],{"class":2069}," APP_SHELL_CACHE",[55,6825,2073],{"class":2043},[55,6827,6828],{"class":721}," 'app-shell-v1'",[55,6830,5761],{"class":2053},[55,6832,6833],{"class":57,"line":71},[55,6834,68],{"emptyLinePlaceholder":67},[55,6836,6837],{"class":57,"line":77},[55,6838,6839],{"class":711},"// These files are pre-cached when the service worker installs\n",[55,6841,6842,6844,6847,6849],{"class":57,"line":83},[55,6843,6191],{"class":2043},[55,6845,6846],{"class":2069}," APP_SHELL_FILES",[55,6848,2073],{"class":2043},[55,6850,6851],{"class":2053}," [\n",[55,6853,6854,6857],{"class":57,"line":89},[55,6855,6856],{"class":721},"  '/'",[55,6858,2250],{"class":2053},[55,6860,6861,6864],{"class":57,"line":95},[55,6862,6863],{"class":721},"  '/offline.html'",[55,6865,2250],{"class":2053},[55,6867,6868,6871],{"class":57,"line":101},[55,6869,6870],{"class":721},"  '/styles.a3f9b2.css'",[55,6872,2250],{"class":2053},[55,6874,6875,6878],{"class":57,"line":107},[55,6876,6877],{"class":721},"  '/app.8d7c3e.js'",[55,6879,2250],{"class":2053},[55,6881,6882,6885],{"class":57,"line":113},[55,6883,6884],{"class":721},"  '/icons/icon-192x192.png'",[55,6886,2250],{"class":2053},[55,6888,6889,6892],{"class":57,"line":119},[55,6890,6891],{"class":721},"  '/fonts/fira-sans.woff2'",[55,6893,2250],{"class":2053},[55,6895,6896],{"class":57,"line":125},[55,6897,6898],{"class":2053},"];\n",[55,6900,6901],{"class":57,"line":131},[55,6902,68],{"emptyLinePlaceholder":67},[55,6904,6905,6907,6909,6911,6914,6916,6918,6920,6922],{"class":57,"line":137},[55,6906,5998],{"class":2053},[55,6908,5828],{"class":717},[55,6910,2054],{"class":2053},[55,6912,6913],{"class":721},"'install'",[55,6915,6008],{"class":2053},[55,6917,6011],{"class":2057},[55,6919,6014],{"class":2053},[55,6921,5839],{"class":2043},[55,6923,5778],{"class":2053},[55,6925,6926,6928,6931],{"class":57,"line":143},[55,6927,6037],{"class":2053},[55,6929,6930],{"class":717},"waitUntil",[55,6932,2242],{"class":2053},[55,6934,6935,6937,6939,6941,6944,6946,6948,6950,6952,6954,6956],{"class":57,"line":149},[55,6936,6052],{"class":2053},[55,6938,6240],{"class":717},[55,6940,2054],{"class":2053},[55,6942,6943],{"class":2069},"APP_SHELL_CACHE",[55,6945,6248],{"class":2053},[55,6947,6061],{"class":717},[55,6949,6064],{"class":2053},[55,6951,6259],{"class":2057},[55,6953,6014],{"class":2053},[55,6955,5839],{"class":2043},[55,6957,5778],{"class":2053},[55,6959,6960,6962,6964,6966,6969],{"class":57,"line":441},[55,6961,5859],{"class":2053},[55,6963,5747],{"class":717},[55,6965,2054],{"class":2053},[55,6967,6968],{"class":721},"'Pre-caching app shell'",[55,6970,2178],{"class":2053},[55,6972,6973,6975,6977,6980,6982,6985],{"class":57,"line":447},[55,6974,6101],{"class":2043},[55,6976,6278],{"class":2053},[55,6978,6979],{"class":717},"addAll",[55,6981,2054],{"class":2053},[55,6983,6984],{"class":2069},"APP_SHELL_FILES",[55,6986,2178],{"class":2053},[55,6988,6989],{"class":57,"line":453},[55,6990,6115],{"class":2053},[55,6992,6993],{"class":57,"line":459},[55,6994,6120],{"class":2053},[55,6996,6997],{"class":57,"line":464},[55,6998,68],{"emptyLinePlaceholder":67},[55,7000,7001],{"class":57,"line":470},[55,7002,7003],{"class":711},"  // Skip waiting so this SW activates immediately\n",[55,7005,7006,7009,7012],{"class":57,"line":475},[55,7007,7008],{"class":2053},"  self.",[55,7010,7011],{"class":717},"skipWaiting",[55,7013,7014],{"class":2053},"();\n",[55,7016,7017],{"class":57,"line":481},[55,7018,6125],{"class":2053},[18,7020,7021,7024,7025,7028],{},[22,7022,7023],{},"event.waitUntil()"," keeps the service worker in the installing state until the promise resolves. If any file in ",[22,7026,7027],{},"addAll()"," fails to download, the entire installation fails and the service worker does not activate. Be conservative with which files you include here.",[343,7030,7032],{"id":7031},"cleaning-up-old-caches","Cleaning Up Old Caches",[18,7034,7035,7036,7039,7040,7043],{},"When you deploy a new version (new cache name ",[22,7037,7038],{},"app-shell-v2","), old caches from previous versions sit on the user's device eating storage. The ",[22,7041,7042],{},"activate"," event is the right place to clean them up:",[46,7045,7047],{"className":2025,"code":7046,"language":2027,"meta":51,"style":51},"// 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",[22,7048,7049,7053,7075,7079,7100,7108,7131,7145,7150,7179,7196,7211,7224,7229,7234,7238,7242,7246,7251,7261],{"__ignoreMap":51},[55,7050,7051],{"class":57,"line":58},[55,7052,5989],{"class":711},[55,7054,7055,7057,7060,7062,7065,7068,7070,7073],{"class":57,"line":64},[55,7056,6191],{"class":2043},[55,7058,7059],{"class":2069}," CURRENT_CACHES",[55,7061,2073],{"class":2043},[55,7063,7064],{"class":2053}," [",[55,7066,7067],{"class":721},"'app-shell-v2'",[55,7069,525],{"class":2053},[55,7071,7072],{"class":721},"'dynamic-v1'",[55,7074,6898],{"class":2053},[55,7076,7077],{"class":57,"line":71},[55,7078,68],{"emptyLinePlaceholder":67},[55,7080,7081,7083,7085,7087,7090,7092,7094,7096,7098],{"class":57,"line":77},[55,7082,5998],{"class":2053},[55,7084,5828],{"class":717},[55,7086,2054],{"class":2053},[55,7088,7089],{"class":721},"'activate'",[55,7091,6008],{"class":2053},[55,7093,6011],{"class":2057},[55,7095,6014],{"class":2053},[55,7097,5839],{"class":2043},[55,7099,5778],{"class":2053},[55,7101,7102,7104,7106],{"class":57,"line":83},[55,7103,6037],{"class":2053},[55,7105,6930],{"class":717},[55,7107,2242],{"class":2053},[55,7109,7110,7112,7115,7118,7120,7122,7125,7127,7129],{"class":57,"line":89},[55,7111,6052],{"class":2053},[55,7113,7114],{"class":717},"keys",[55,7116,7117],{"class":2053},"().",[55,7119,6061],{"class":717},[55,7121,6064],{"class":2053},[55,7123,7124],{"class":2057},"cacheNames",[55,7126,6014],{"class":2053},[55,7128,5839],{"class":2043},[55,7130,5778],{"class":2053},[55,7132,7133,7135,7138,7140,7143],{"class":57,"line":95},[55,7134,6101],{"class":2043},[55,7136,7137],{"class":2069}," Promise",[55,7139,532],{"class":2053},[55,7141,7142],{"class":717},"all",[55,7144,2242],{"class":2053},[55,7146,7147],{"class":57,"line":101},[55,7148,7149],{"class":2053},"        cacheNames\n",[55,7151,7152,7155,7158,7160,7162,7164,7166,7168,7171,7173,7176],{"class":57,"line":107},[55,7153,7154],{"class":2053},"          .",[55,7156,7157],{"class":717},"filter",[55,7159,6064],{"class":2053},[55,7161,2447],{"class":2057},[55,7163,6014],{"class":2053},[55,7165,5839],{"class":2043},[55,7167,2105],{"class":2043},[55,7169,7170],{"class":2069},"CURRENT_CACHES",[55,7172,532],{"class":2053},[55,7174,7175],{"class":717},"includes",[55,7177,7178],{"class":2053},"(name))\n",[55,7180,7181,7183,7186,7188,7190,7192,7194],{"class":57,"line":113},[55,7182,7154],{"class":2053},[55,7184,7185],{"class":717},"map",[55,7187,6064],{"class":2053},[55,7189,2447],{"class":2057},[55,7191,6014],{"class":2053},[55,7193,5839],{"class":2043},[55,7195,5778],{"class":2053},[55,7197,7198,7201,7203,7205,7208],{"class":57,"line":119},[55,7199,7200],{"class":2053},"            console.",[55,7202,5747],{"class":717},[55,7204,2054],{"class":2053},[55,7206,7207],{"class":721},"'Deleting old cache:'",[55,7209,7210],{"class":2053},", name);\n",[55,7212,7213,7216,7218,7221],{"class":57,"line":125},[55,7214,7215],{"class":2043},"            return",[55,7217,6466],{"class":2053},[55,7219,7220],{"class":717},"delete",[55,7222,7223],{"class":2053},"(name);\n",[55,7225,7226],{"class":57,"line":131},[55,7227,7228],{"class":2053},"          })\n",[55,7230,7231],{"class":57,"line":137},[55,7232,7233],{"class":2053},"      );\n",[55,7235,7236],{"class":57,"line":143},[55,7237,6115],{"class":2053},[55,7239,7240],{"class":57,"line":149},[55,7241,6120],{"class":2053},[55,7243,7244],{"class":57,"line":441},[55,7245,68],{"emptyLinePlaceholder":67},[55,7247,7248],{"class":57,"line":447},[55,7249,7250],{"class":711},"  // Claim all clients immediately (don't wait for tab reopen)\n",[55,7252,7253,7256,7259],{"class":57,"line":453},[55,7254,7255],{"class":2053},"  self.clients.",[55,7257,7258],{"class":717},"claim",[55,7260,7014],{"class":2053},[55,7262,7263],{"class":57,"line":459},[55,7264,6125],{"class":2053},[18,7266,7267,7270],{},[22,7268,7269],{},"self.clients.claim()"," makes the activated service worker take control of all open tabs immediately, without waiting for a page reload.",[343,7272,7274],{"id":7273},"a-complete-production-ready-service-worker","A Complete, Production-Ready Service Worker",[18,7276,7277],{},"Here is a service worker that combines everything above, routing different request types to the appropriate strategy:",[46,7279,7281],{"className":2025,"code":7280,"language":2027,"meta":51,"style":51},"// 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",[22,7282,7283,7287,7291,7304,7316,7334,7338,7348,7354,7360,7367,7374,7378,7382,7387,7407,7415,7448,7452,7460,7464,7468,7473,7493,7501,7521,7532,7537,7561,7581,7586,7591,7595,7603,7607,7611,7616,7636,7652,7670,7674,7679,7707,7711,7716,7734,7747,7753,7757,7761,7766,7823,7836,7842,7846,7850,7855,7868,7881,7888,7893,7898,7904,7917,7922,7927,7933,7938,7954,7971,7982,7997,8004,8025,8039,8044,8051,8056,8061,8076,8097,8104,8119,8135,8142,8151,8168,8199,8216,8221,8226,8231,8236,8251,8272,8289,8314,8329,8336,8360,8365,8376],{"__ignoreMap":51},[55,7284,7285],{"class":57,"line":58},[55,7286,5989],{"class":711},[55,7288,7289],{"class":57,"line":64},[55,7290,68],{"emptyLinePlaceholder":67},[55,7292,7293,7295,7297,7299,7302],{"class":57,"line":71},[55,7294,6191],{"class":2043},[55,7296,6823],{"class":2069},[55,7298,2073],{"class":2043},[55,7300,7301],{"class":721}," 'app-shell-v2'",[55,7303,5761],{"class":2053},[55,7305,7306,7308,7310,7312,7314],{"class":57,"line":77},[55,7307,6191],{"class":2043},[55,7309,6399],{"class":2069},[55,7311,2073],{"class":2043},[55,7313,6404],{"class":721},[55,7315,5761],{"class":2053},[55,7317,7318,7320,7322,7324,7326,7328,7330,7332],{"class":57,"line":83},[55,7319,6191],{"class":2043},[55,7321,7059],{"class":2069},[55,7323,2073],{"class":2043},[55,7325,7064],{"class":2053},[55,7327,6943],{"class":2069},[55,7329,525],{"class":2053},[55,7331,6473],{"class":2069},[55,7333,6898],{"class":2053},[55,7335,7336],{"class":57,"line":89},[55,7337,68],{"emptyLinePlaceholder":67},[55,7339,7340,7342,7344,7346],{"class":57,"line":95},[55,7341,6191],{"class":2043},[55,7343,6846],{"class":2069},[55,7345,2073],{"class":2043},[55,7347,6851],{"class":2053},[55,7349,7350,7352],{"class":57,"line":101},[55,7351,6856],{"class":721},[55,7353,2250],{"class":2053},[55,7355,7356,7358],{"class":57,"line":107},[55,7357,6863],{"class":721},[55,7359,2250],{"class":2053},[55,7361,7362,7365],{"class":57,"line":113},[55,7363,7364],{"class":721},"  '/styles.abc123.css'",[55,7366,2250],{"class":2053},[55,7368,7369,7372],{"class":57,"line":119},[55,7370,7371],{"class":721},"  '/app.def456.js'",[55,7373,2250],{"class":2053},[55,7375,7376],{"class":57,"line":125},[55,7377,6898],{"class":2053},[55,7379,7380],{"class":57,"line":131},[55,7381,68],{"emptyLinePlaceholder":67},[55,7383,7384],{"class":57,"line":137},[55,7385,7386],{"class":711},"// ---------- INSTALL ----------\n",[55,7388,7389,7391,7393,7395,7397,7399,7401,7403,7405],{"class":57,"line":143},[55,7390,5998],{"class":2053},[55,7392,5828],{"class":717},[55,7394,2054],{"class":2053},[55,7396,6913],{"class":721},[55,7398,6008],{"class":2053},[55,7400,6011],{"class":2057},[55,7402,6014],{"class":2053},[55,7404,5839],{"class":2043},[55,7406,5778],{"class":2053},[55,7408,7409,7411,7413],{"class":57,"line":149},[55,7410,6037],{"class":2053},[55,7412,6930],{"class":717},[55,7414,2242],{"class":2053},[55,7416,7417,7419,7421,7423,7425,7427,7429,7431,7433,7435,7437,7439,7441,7443,7445],{"class":57,"line":441},[55,7418,6052],{"class":2053},[55,7420,6240],{"class":717},[55,7422,2054],{"class":2053},[55,7424,6943],{"class":2069},[55,7426,6248],{"class":2053},[55,7428,6061],{"class":717},[55,7430,6064],{"class":2053},[55,7432,6259],{"class":2057},[55,7434,6014],{"class":2053},[55,7436,5839],{"class":2043},[55,7438,6278],{"class":2053},[55,7440,6979],{"class":717},[55,7442,2054],{"class":2053},[55,7444,6984],{"class":2069},[55,7446,7447],{"class":2053},"))\n",[55,7449,7450],{"class":57,"line":447},[55,7451,6120],{"class":2053},[55,7453,7454,7456,7458],{"class":57,"line":453},[55,7455,7008],{"class":2053},[55,7457,7011],{"class":717},[55,7459,7014],{"class":2053},[55,7461,7462],{"class":57,"line":459},[55,7463,6125],{"class":2053},[55,7465,7466],{"class":57,"line":464},[55,7467,68],{"emptyLinePlaceholder":67},[55,7469,7470],{"class":57,"line":470},[55,7471,7472],{"class":711},"// ---------- ACTIVATE ----------\n",[55,7474,7475,7477,7479,7481,7483,7485,7487,7489,7491],{"class":57,"line":475},[55,7476,5998],{"class":2053},[55,7478,5828],{"class":717},[55,7480,2054],{"class":2053},[55,7482,7089],{"class":721},[55,7484,6008],{"class":2053},[55,7486,6011],{"class":2057},[55,7488,6014],{"class":2053},[55,7490,5839],{"class":2043},[55,7492,5778],{"class":2053},[55,7494,7495,7497,7499],{"class":57,"line":481},[55,7496,6037],{"class":2053},[55,7498,6930],{"class":717},[55,7500,2242],{"class":2053},[55,7502,7503,7505,7507,7509,7511,7513,7516,7518],{"class":57,"line":486},[55,7504,6052],{"class":2053},[55,7506,7114],{"class":717},[55,7508,7117],{"class":2053},[55,7510,6061],{"class":717},[55,7512,6064],{"class":2053},[55,7514,7515],{"class":2057},"names",[55,7517,6014],{"class":2053},[55,7519,7520],{"class":2043},"=>\n",[55,7522,7523,7526,7528,7530],{"class":57,"line":492},[55,7524,7525],{"class":2069},"      Promise",[55,7527,532],{"class":2053},[55,7529,7142],{"class":717},[55,7531,2242],{"class":2053},[55,7533,7534],{"class":57,"line":497},[55,7535,7536],{"class":2053},"        names\n",[55,7538,7539,7541,7543,7545,7547,7549,7551,7553,7555,7557,7559],{"class":57,"line":503},[55,7540,7154],{"class":2053},[55,7542,7157],{"class":717},[55,7544,6064],{"class":2053},[55,7546,2447],{"class":2057},[55,7548,6014],{"class":2053},[55,7550,5839],{"class":2043},[55,7552,2105],{"class":2043},[55,7554,7170],{"class":2069},[55,7556,532],{"class":2053},[55,7558,7175],{"class":717},[55,7560,7178],{"class":2053},[55,7562,7563,7565,7567,7569,7571,7573,7575,7577,7579],{"class":57,"line":508},[55,7564,7154],{"class":2053},[55,7566,7185],{"class":717},[55,7568,6064],{"class":2053},[55,7570,2447],{"class":2057},[55,7572,6014],{"class":2053},[55,7574,5839],{"class":2043},[55,7576,6466],{"class":2053},[55,7578,7220],{"class":717},[55,7580,7178],{"class":2053},[55,7582,7583],{"class":57,"line":2347},[55,7584,7585],{"class":2053},"      )\n",[55,7587,7588],{"class":57,"line":2353},[55,7589,7590],{"class":2053},"    )\n",[55,7592,7593],{"class":57,"line":2368},[55,7594,6120],{"class":2053},[55,7596,7597,7599,7601],{"class":57,"line":3094},[55,7598,7255],{"class":2053},[55,7600,7258],{"class":717},[55,7602,7014],{"class":2053},[55,7604,7605],{"class":57,"line":3099},[55,7606,6125],{"class":2053},[55,7608,7609],{"class":57,"line":3104},[55,7610,68],{"emptyLinePlaceholder":67},[55,7612,7613],{"class":57,"line":4281},[55,7614,7615],{"class":711},"// ---------- FETCH ----------\n",[55,7617,7618,7620,7622,7624,7626,7628,7630,7632,7634],{"class":57,"line":4287},[55,7619,5998],{"class":2053},[55,7621,5828],{"class":717},[55,7623,2054],{"class":2053},[55,7625,6005],{"class":721},[55,7627,6008],{"class":2053},[55,7629,6011],{"class":2057},[55,7631,6014],{"class":2053},[55,7633,5839],{"class":2043},[55,7635,5778],{"class":2053},[55,7637,7638,7640,7642,7644,7647,7649],{"class":57,"line":4293},[55,7639,6668],{"class":2043},[55,7641,3317],{"class":2053},[55,7643,2058],{"class":2069},[55,7645,7646],{"class":2053}," } ",[55,7648,3475],{"class":2043},[55,7650,7651],{"class":2053}," event;\n",[55,7653,7654,7656,7659,7661,7664,7667],{"class":57,"line":4298},[55,7655,6668],{"class":2043},[55,7657,7658],{"class":2069}," url",[55,7660,2073],{"class":2043},[55,7662,7663],{"class":2043}," new",[55,7665,7666],{"class":717}," URL",[55,7668,7669],{"class":2053},"(request.url);\n",[55,7671,7672],{"class":57,"line":4304},[55,7673,68],{"emptyLinePlaceholder":67},[55,7675,7676],{"class":57,"line":4309},[55,7677,7678],{"class":711},"  // 1. Skip non-GET requests and cross-origin requests\n",[55,7680,7681,7683,7686,7689,7692,7695,7698,7700,7703,7705],{"class":57,"line":4315},[55,7682,5724],{"class":2043},[55,7684,7685],{"class":2053}," (request.method ",[55,7687,7688],{"class":2043},"!==",[55,7690,7691],{"class":721}," 'GET'",[55,7693,7694],{"class":2043}," ||",[55,7696,7697],{"class":2053}," url.origin ",[55,7699,7688],{"class":2043},[55,7701,7702],{"class":2053}," location.origin) ",[55,7704,6293],{"class":2043},[55,7706,5761],{"class":2053},[55,7708,7709],{"class":57,"line":4321},[55,7710,68],{"emptyLinePlaceholder":67},[55,7712,7713],{"class":57,"line":4326},[55,7714,7715],{"class":711},"  // 2. API calls: network first\n",[55,7717,7718,7720,7723,7726,7728,7731],{"class":57,"line":4332},[55,7719,5724],{"class":2043},[55,7721,7722],{"class":2053}," (url.pathname.",[55,7724,7725],{"class":717},"startsWith",[55,7727,2054],{"class":2053},[55,7729,7730],{"class":721},"'/api/'",[55,7732,7733],{"class":2053},")) {\n",[55,7735,7736,7738,7740,7742,7744],{"class":57,"line":4337},[55,7737,6614],{"class":2053},[55,7739,6040],{"class":717},[55,7741,2054],{"class":2053},[55,7743,6621],{"class":717},[55,7745,7746],{"class":2053},"(request));\n",[55,7748,7749,7751],{"class":57,"line":4343},[55,7750,2356],{"class":2043},[55,7752,5761],{"class":2053},[55,7754,7755],{"class":57,"line":4348},[55,7756,5766],{"class":2053},[55,7758,7759],{"class":57,"line":4354},[55,7760,68],{"emptyLinePlaceholder":67},[55,7762,7763],{"class":57,"line":5377},[55,7764,7765],{"class":711},"  // 3. Static assets with content hash in filename: cache first\n",[55,7767,7768,7770,7772,7774,7778,7781,7784,7786,7789,7792,7795,7797,7800,7802,7805,7807,7810,7813,7815,7817,7820],{"class":57,"line":5390},[55,7769,5724],{"class":2043},[55,7771,2093],{"class":2053},[55,7773,3857],{"class":721},[55,7775,7777],{"class":7776},"s691h","\\.",[55,7779,7780],{"class":2069},"[a-f0-9]",[55,7782,7783],{"class":2043},"{6,}",[55,7785,7777],{"class":7776},[55,7787,7788],{"class":721},"(css",[55,7790,7791],{"class":2043},"|",[55,7793,7794],{"class":721},"js",[55,7796,7791],{"class":2043},[55,7798,7799],{"class":721},"woff2",[55,7801,7791],{"class":2043},[55,7803,7804],{"class":721},"png",[55,7806,7791],{"class":2043},[55,7808,7809],{"class":721},"jpg)",[55,7811,7812],{"class":2043},"$",[55,7814,3857],{"class":721},[55,7816,532],{"class":2053},[55,7818,7819],{"class":717},"test",[55,7821,7822],{"class":2053},"(url.pathname)) {\n",[55,7824,7825,7827,7829,7831,7834],{"class":57,"line":5403},[55,7826,6614],{"class":2053},[55,7828,6040],{"class":717},[55,7830,2054],{"class":2053},[55,7832,7833],{"class":717},"cacheFirst",[55,7835,7746],{"class":2053},[55,7837,7838,7840],{"class":57,"line":5416},[55,7839,2356],{"class":2043},[55,7841,5761],{"class":2053},[55,7843,7844],{"class":57,"line":5446},[55,7845,5766],{"class":2053},[55,7847,7848],{"class":57,"line":5451},[55,7849,68],{"emptyLinePlaceholder":67},[55,7851,7852],{"class":57,"line":5456},[55,7853,7854],{"class":711},"  // 4. HTML navigation: stale while revalidate\n",[55,7856,7857,7859,7862,7864,7866],{"class":57,"line":5476},[55,7858,5724],{"class":2043},[55,7860,7861],{"class":2053}," (request.mode ",[55,7863,6604],{"class":2043},[55,7865,6607],{"class":721},[55,7867,2061],{"class":2053},[55,7869,7870,7872,7874,7876,7879],{"class":57,"line":5487},[55,7871,6614],{"class":2053},[55,7873,6040],{"class":717},[55,7875,2054],{"class":2053},[55,7877,7878],{"class":717},"staleWhileRevalidate",[55,7880,7746],{"class":2053},[55,7882,7884,7886],{"class":57,"line":7883},59,[55,7885,2356],{"class":2043},[55,7887,5761],{"class":2053},[55,7889,7891],{"class":57,"line":7890},60,[55,7892,5766],{"class":2053},[55,7894,7896],{"class":57,"line":7895},61,[55,7897,68],{"emptyLinePlaceholder":67},[55,7899,7901],{"class":57,"line":7900},62,[55,7902,7903],{"class":711},"  // 5. Everything else: stale while revalidate\n",[55,7905,7907,7909,7911,7913,7915],{"class":57,"line":7906},63,[55,7908,6037],{"class":2053},[55,7910,6040],{"class":717},[55,7912,2054],{"class":2053},[55,7914,7878],{"class":717},[55,7916,7746],{"class":2053},[55,7918,7920],{"class":57,"line":7919},64,[55,7921,6125],{"class":2053},[55,7923,7925],{"class":57,"line":7924},65,[55,7926,68],{"emptyLinePlaceholder":67},[55,7928,7930],{"class":57,"line":7929},66,[55,7931,7932],{"class":711},"// ---------- STRATEGIES ----------\n",[55,7934,7936],{"class":57,"line":7935},67,[55,7937,68],{"emptyLinePlaceholder":67},[55,7939,7941,7943,7945,7948,7950,7952],{"class":57,"line":7940},68,[55,7942,2044],{"class":2043},[55,7944,2047],{"class":2043},[55,7946,7947],{"class":717}," cacheFirst",[55,7949,2054],{"class":2053},[55,7951,2058],{"class":2057},[55,7953,2061],{"class":2053},[55,7955,7957,7959,7961,7963,7965,7967,7969],{"class":57,"line":7956},69,[55,7958,6668],{"class":2043},[55,7960,2153],{"class":2069},[55,7962,2073],{"class":2043},[55,7964,2158],{"class":2043},[55,7966,6466],{"class":2053},[55,7968,6055],{"class":717},[55,7970,6446],{"class":2053},[55,7972,7974,7976,7978,7980],{"class":57,"line":7973},70,[55,7975,5724],{"class":2043},[55,7977,6290],{"class":2053},[55,7979,6293],{"class":2043},[55,7981,6296],{"class":2053},[55,7983,7985,7987,7989,7991,7993,7995],{"class":57,"line":7984},71,[55,7986,6668],{"class":2043},[55,7988,2119],{"class":2069},[55,7990,2073],{"class":2043},[55,7992,2158],{"class":2043},[55,7994,6104],{"class":717},[55,7996,6446],{"class":2053},[55,7998,8000,8002],{"class":57,"line":7999},72,[55,8001,5724],{"class":2043},[55,8003,6326],{"class":2053},[55,8005,8007,8009,8011,8013,8015,8017,8019,8021,8023],{"class":57,"line":8006},73,[55,8008,2066],{"class":2043},[55,8010,6459],{"class":2069},[55,8012,2073],{"class":2043},[55,8014,2158],{"class":2043},[55,8016,6466],{"class":2053},[55,8018,6240],{"class":717},[55,8020,2054],{"class":2053},[55,8022,6473],{"class":2069},[55,8024,2178],{"class":2053},[55,8026,8028,8031,8033,8035,8037],{"class":57,"line":8027},74,[55,8029,8030],{"class":2053},"    cache.",[55,8032,6334],{"class":717},[55,8034,6485],{"class":2053},[55,8036,6340],{"class":717},[55,8038,6343],{"class":2053},[55,8040,8042],{"class":57,"line":8041},75,[55,8043,5766],{"class":2053},[55,8045,8047,8049],{"class":57,"line":8046},76,[55,8048,6784],{"class":2043},[55,8050,6354],{"class":2053},[55,8052,8054],{"class":57,"line":8053},77,[55,8055,2005],{"class":2053},[55,8057,8059],{"class":57,"line":8058},78,[55,8060,68],{"emptyLinePlaceholder":67},[55,8062,8064,8066,8068,8070,8072,8074],{"class":57,"line":8063},79,[55,8065,2044],{"class":2043},[55,8067,2047],{"class":2043},[55,8069,6419],{"class":717},[55,8071,2054],{"class":2053},[55,8073,2058],{"class":2057},[55,8075,2061],{"class":2053},[55,8077,8079,8081,8083,8085,8087,8089,8091,8093,8095],{"class":57,"line":8078},80,[55,8080,6668],{"class":2043},[55,8082,6459],{"class":2069},[55,8084,2073],{"class":2043},[55,8086,2158],{"class":2043},[55,8088,6466],{"class":2053},[55,8090,6240],{"class":717},[55,8092,2054],{"class":2053},[55,8094,6473],{"class":2069},[55,8096,2178],{"class":2053},[55,8098,8100,8102],{"class":57,"line":8099},81,[55,8101,5775],{"class":2043},[55,8103,5778],{"class":2053},[55,8105,8107,8109,8111,8113,8115,8117],{"class":57,"line":8106},82,[55,8108,2066],{"class":2043},[55,8110,2119],{"class":2069},[55,8112,2073],{"class":2043},[55,8114,2158],{"class":2043},[55,8116,6104],{"class":717},[55,8118,6446],{"class":2053},[55,8120,8122,8124,8127,8129,8131,8133],{"class":57,"line":8121},83,[55,8123,2090],{"class":2043},[55,8125,8126],{"class":2053}," (response.ok) cache.",[55,8128,6334],{"class":717},[55,8130,6485],{"class":2053},[55,8132,6340],{"class":717},[55,8134,6343],{"class":2053},[55,8136,8138,8140],{"class":57,"line":8137},84,[55,8139,2356],{"class":2043},[55,8141,6354],{"class":2053},[55,8143,8145,8147,8149],{"class":57,"line":8144},85,[55,8146,5896],{"class":2053},[55,8148,5899],{"class":2043},[55,8150,5778],{"class":2053},[55,8152,8154,8156,8158,8160,8162,8164,8166],{"class":57,"line":8153},86,[55,8155,2066],{"class":2043},[55,8157,2153],{"class":2069},[55,8159,2073],{"class":2043},[55,8161,2158],{"class":2043},[55,8163,6278],{"class":2053},[55,8165,6055],{"class":717},[55,8167,6446],{"class":2053},[55,8169,8171,8173,8175,8177,8179,8182,8184,8186,8188,8190,8193,8196],{"class":57,"line":8170},87,[55,8172,2356],{"class":2043},[55,8174,6787],{"class":2053},[55,8176,6790],{"class":2043},[55,8178,7663],{"class":2043},[55,8180,8181],{"class":717}," Response",[55,8183,2054],{"class":2053},[55,8185,2201],{"class":2069},[55,8187,532],{"class":2053},[55,8189,2337],{"class":717},[55,8191,8192],{"class":2053},"({ error: ",[55,8194,8195],{"class":721},"'Offline'",[55,8197,8198],{"class":2053}," }), {\n",[55,8200,8202,8205,8208,8210,8213],{"class":57,"line":8201},88,[55,8203,8204],{"class":2053},"      headers: { ",[55,8206,8207],{"class":721},"'Content-Type'",[55,8209,3323],{"class":2053},[55,8211,8212],{"class":721},"'application/json'",[55,8214,8215],{"class":2053}," },\n",[55,8217,8219],{"class":57,"line":8218},89,[55,8220,5816],{"class":2053},[55,8222,8224],{"class":57,"line":8223},90,[55,8225,5766],{"class":2053},[55,8227,8229],{"class":57,"line":8228},91,[55,8230,2005],{"class":2053},[55,8232,8234],{"class":57,"line":8233},92,[55,8235,68],{"emptyLinePlaceholder":67},[55,8237,8239,8241,8243,8245,8247,8249],{"class":57,"line":8238},93,[55,8240,2044],{"class":2043},[55,8242,2047],{"class":2043},[55,8244,6657],{"class":717},[55,8246,2054],{"class":2053},[55,8248,2058],{"class":2057},[55,8250,2061],{"class":2053},[55,8252,8254,8256,8258,8260,8262,8264,8266,8268,8270],{"class":57,"line":8253},94,[55,8255,6668],{"class":2043},[55,8257,6459],{"class":2069},[55,8259,2073],{"class":2043},[55,8261,2158],{"class":2043},[55,8263,6466],{"class":2053},[55,8265,6240],{"class":717},[55,8267,2054],{"class":2053},[55,8269,6473],{"class":2069},[55,8271,2178],{"class":2053},[55,8273,8275,8277,8279,8281,8283,8285,8287],{"class":57,"line":8274},95,[55,8276,6668],{"class":2043},[55,8278,2153],{"class":2069},[55,8280,2073],{"class":2043},[55,8282,2158],{"class":2043},[55,8284,6278],{"class":2053},[55,8286,6055],{"class":717},[55,8288,6446],{"class":2053},[55,8290,8292,8294,8296,8298,8300,8302,8304,8306,8308,8310,8312],{"class":57,"line":8291},96,[55,8293,6668],{"class":2043},[55,8295,6717],{"class":2069},[55,8297,2073],{"class":2043},[55,8299,6104],{"class":717},[55,8301,6724],{"class":2053},[55,8303,6061],{"class":717},[55,8305,6064],{"class":2053},[55,8307,6731],{"class":2057},[55,8309,6014],{"class":2053},[55,8311,5839],{"class":2043},[55,8313,5778],{"class":2053},[55,8315,8317,8319,8321,8323,8325,8327],{"class":57,"line":8316},97,[55,8318,2090],{"class":2043},[55,8320,8126],{"class":2053},[55,8322,6334],{"class":717},[55,8324,6485],{"class":2053},[55,8326,6340],{"class":717},[55,8328,6343],{"class":2053},[55,8330,8332,8334],{"class":57,"line":8331},98,[55,8333,2356],{"class":2043},[55,8335,6354],{"class":2053},[55,8337,8339,8342,8344,8347,8349,8351,8353,8355,8357],{"class":57,"line":8338},99,[55,8340,8341],{"class":2053},"  }).",[55,8343,5899],{"class":717},[55,8345,8346],{"class":2053},"(() ",[55,8348,5839],{"class":2043},[55,8350,6466],{"class":2053},[55,8352,6055],{"class":717},[55,8354,2054],{"class":2053},[55,8356,6560],{"class":721},[55,8358,8359],{"class":2053},"));\n",[55,8361,8363],{"class":57,"line":8362},100,[55,8364,68],{"emptyLinePlaceholder":67},[55,8366,8368,8370,8372,8374],{"class":57,"line":8367},101,[55,8369,6784],{"class":2043},[55,8371,6787],{"class":2053},[55,8373,6790],{"class":2043},[55,8375,6793],{"class":2053},[55,8377,8379],{"class":57,"line":8378},102,[55,8380,2005],{"class":2053},[36,8382],{},[13,8384,8386],{"id":8385},"the-offline-page","The Offline Page",[18,8388,8389,8390,8393],{},"Every PWA should have a fallback ",[22,8391,8392],{},"/offline.html"," page to show when a navigation fails and there is no cached version. Keep it simple and informative:",[46,8395,8397],{"className":3121,"code":8396,"language":3123,"meta":51,"style":51},"\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",[22,8398,8399,8409,8425,8433,8450,8474,8487,8495,8502,8513,8525,8537,8549,8560,8575,8587,8599,8611,8615,8634,8651,8659,8667,8675,8687,8700,8733,8741],{"__ignoreMap":51},[55,8400,8401,8403,8405,8407],{"class":57,"line":58},[55,8402,3130],{"class":2053},[55,8404,3134],{"class":3133},[55,8406,3137],{"class":717},[55,8408,3140],{"class":2053},[55,8410,8411,8413,8415,8418,8420,8423],{"class":57,"line":64},[55,8412,3145],{"class":2053},[55,8414,3123],{"class":3133},[55,8416,8417],{"class":717}," lang",[55,8419,3475],{"class":2053},[55,8421,8422],{"class":721},"\"en\"",[55,8424,3140],{"class":2053},[55,8426,8427,8429,8431],{"class":57,"line":71},[55,8428,3145],{"class":2053},[55,8430,3157],{"class":3133},[55,8432,3140],{"class":2053},[55,8434,8435,8437,8440,8443,8445,8448],{"class":57,"line":77},[55,8436,3154],{"class":2053},[55,8438,8439],{"class":3133},"meta",[55,8441,8442],{"class":717}," charset",[55,8444,3475],{"class":2053},[55,8446,8447],{"class":721},"\"UTF-8\"",[55,8449,5523],{"class":2053},[55,8451,8452,8454,8456,8459,8461,8464,8467,8469,8472],{"class":57,"line":83},[55,8453,3154],{"class":2053},[55,8455,8439],{"class":3133},[55,8457,8458],{"class":717}," name",[55,8460,3475],{"class":2053},[55,8462,8463],{"class":721},"\"viewport\"",[55,8465,8466],{"class":717}," content",[55,8468,3475],{"class":2053},[55,8470,8471],{"class":721},"\"width=device-width, initial-scale=1.0\"",[55,8473,5523],{"class":2053},[55,8475,8476,8478,8480,8483,8485],{"class":57,"line":89},[55,8477,3154],{"class":2053},[55,8479,2413],{"class":3133},[55,8481,8482],{"class":2053},">You are offline\u003C/",[55,8484,2413],{"class":3133},[55,8486,3140],{"class":2053},[55,8488,8489,8491,8493],{"class":57,"line":95},[55,8490,3154],{"class":2053},[55,8492,4725],{"class":3133},[55,8494,3140],{"class":2053},[55,8496,8497,8500],{"class":57,"line":101},[55,8498,8499],{"class":3133},"    body",[55,8501,5778],{"class":2053},[55,8503,8504,8507,8509,8511],{"class":57,"line":107},[55,8505,8506],{"class":2069},"      font-family",[55,8508,3323],{"class":2053},[55,8510,3326],{"class":2069},[55,8512,5761],{"class":2053},[55,8514,8515,8518,8520,8523],{"class":57,"line":113},[55,8516,8517],{"class":2069},"      display",[55,8519,3323],{"class":2053},[55,8521,8522],{"class":2069},"flex",[55,8524,5761],{"class":2053},[55,8526,8527,8530,8532,8535],{"class":57,"line":119},[55,8528,8529],{"class":2069},"      flex-direction",[55,8531,3323],{"class":2053},[55,8533,8534],{"class":2069},"column",[55,8536,5761],{"class":2053},[55,8538,8539,8542,8544,8547],{"class":57,"line":125},[55,8540,8541],{"class":2069},"      align-items",[55,8543,3323],{"class":2053},[55,8545,8546],{"class":2069},"center",[55,8548,5761],{"class":2053},[55,8550,8551,8554,8556,8558],{"class":57,"line":131},[55,8552,8553],{"class":2069},"      justify-content",[55,8555,3323],{"class":2053},[55,8557,8546],{"class":2069},[55,8559,5761],{"class":2053},[55,8561,8562,8565,8567,8570,8573],{"class":57,"line":137},[55,8563,8564],{"class":2069},"      min-height",[55,8566,3323],{"class":2053},[55,8568,8569],{"class":2069},"100",[55,8571,8572],{"class":2043},"vh",[55,8574,5761],{"class":2053},[55,8576,8577,8580,8582,8585],{"class":57,"line":143},[55,8578,8579],{"class":2069},"      margin",[55,8581,3323],{"class":2053},[55,8583,8584],{"class":2069},"0",[55,8586,5761],{"class":2053},[55,8588,8589,8592,8594,8597],{"class":57,"line":149},[55,8590,8591],{"class":2069},"      background",[55,8593,3323],{"class":2053},[55,8595,8596],{"class":2069},"#f9fafb",[55,8598,5761],{"class":2053},[55,8600,8601,8604,8606,8609],{"class":57,"line":441},[55,8602,8603],{"class":2069},"      color",[55,8605,3323],{"class":2053},[55,8607,8608],{"class":2069},"#111",[55,8610,5761],{"class":2053},[55,8612,8613],{"class":57,"line":447},[55,8614,1962],{"class":2053},[55,8616,8617,8620,8622,8624,8626,8629,8632],{"class":57,"line":453},[55,8618,8619],{"class":3133},"    h1",[55,8621,3317],{"class":2053},[55,8623,3350],{"class":2069},[55,8625,3323],{"class":2053},[55,8627,8628],{"class":2069},"1.5",[55,8630,8631],{"class":2043},"rem",[55,8633,3329],{"class":2053},[55,8635,8636,8639,8642,8644,8646,8649],{"class":57,"line":459},[55,8637,8638],{"class":3133},"    p",[55,8640,8641],{"class":2053},"  { ",[55,8643,3339],{"class":2069},[55,8645,3323],{"class":2053},[55,8647,8648],{"class":2069},"#6b7280",[55,8650,3329],{"class":2053},[55,8652,8653,8655,8657],{"class":57,"line":464},[55,8654,3178],{"class":2053},[55,8656,4725],{"class":3133},[55,8658,3140],{"class":2053},[55,8660,8661,8663,8665],{"class":57,"line":470},[55,8662,3230],{"class":2053},[55,8664,3157],{"class":3133},[55,8666,3140],{"class":2053},[55,8668,8669,8671,8673],{"class":57,"line":475},[55,8670,3145],{"class":2053},[55,8672,2424],{"class":3133},[55,8674,3140],{"class":2053},[55,8676,8677,8679,8681,8683,8685],{"class":57,"line":481},[55,8678,3154],{"class":2053},[55,8680,3197],{"class":3133},[55,8682,8482],{"class":2053},[55,8684,3197],{"class":3133},[55,8686,3140],{"class":2053},[55,8688,8689,8691,8693,8696,8698],{"class":57,"line":486},[55,8690,3154],{"class":2053},[55,8692,18],{"class":3133},[55,8694,8695],{"class":2053},">Check your internet connection and try again.\u003C/",[55,8697,18],{"class":3133},[55,8699,3140],{"class":2053},[55,8701,8702,8704,8707,8710,8712,8715,8718,8720,8723,8726,8729,8731],{"class":57,"line":492},[55,8703,3154],{"class":2053},[55,8705,8706],{"class":3133},"button",[55,8708,8709],{"class":717}," onclick",[55,8711,3475],{"class":2053},[55,8713,8714],{"class":721},"\"",[55,8716,8717],{"class":2053},"location",[55,8719,532],{"class":721},[55,8721,8722],{"class":717},"reload",[55,8724,8725],{"class":721},"()\"",[55,8727,8728],{"class":2053},">Try again\u003C/",[55,8730,8706],{"class":3133},[55,8732,3140],{"class":2053},[55,8734,8735,8737,8739],{"class":57,"line":497},[55,8736,3230],{"class":2053},[55,8738,2424],{"class":3133},[55,8740,3140],{"class":2053},[55,8742,8743,8745,8747],{"class":57,"line":503},[55,8744,3230],{"class":2053},[55,8746,3123],{"class":3133},[55,8748,3140],{"class":2053},[36,8750],{},[13,8752,8754],{"id":8753},"pwa-installation-the-add-to-home-screen-flow","PWA Installation: The \"Add to Home Screen\" Flow",[18,8756,8757],{},"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.",[343,8759,4542,8761,8764],{"id":8760},"the-beforeinstallprompt-event",[22,8762,8763],{},"beforeinstallprompt"," Event",[18,8766,8767,8768,8770],{},"The browser fires ",[22,8769,8763],{}," when it is ready to show the install prompt. You can capture it and show your own UI at a better moment:",[46,8772,8774],{"className":2025,"code":8773,"language":2027,"meta":51,"style":51},"// 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",[22,8775,8776,8781,8785,8800,8822,8826,8847,8852,8861,8865,8870,8879,8883,8888,8900,8904,8908,8931,8946,8950,8955,8965,8969,8974,8992,8996,9030,9034,9039,9049,9060,9064,9068,9073,9090,9103,9108],{"__ignoreMap":51},[55,8777,8778],{"class":57,"line":58},[55,8779,8780],{"class":711},"// main.js\n",[55,8782,8783],{"class":57,"line":64},[55,8784,68],{"emptyLinePlaceholder":67},[55,8786,8787,8790,8793,8795,8798],{"class":57,"line":71},[55,8788,8789],{"class":2043},"let",[55,8791,8792],{"class":2053}," deferredPrompt ",[55,8794,3475],{"class":2043},[55,8796,8797],{"class":2069}," null",[55,8799,5761],{"class":2053},[55,8801,8802,8804,8807,8809,8812,8815,8817,8820],{"class":57,"line":77},[55,8803,6191],{"class":2043},[55,8805,8806],{"class":2069}," installButton",[55,8808,2073],{"class":2043},[55,8810,8811],{"class":2053}," document.",[55,8813,8814],{"class":717},"querySelector",[55,8816,2054],{"class":2053},[55,8818,8819],{"class":721},"'#install-btn'",[55,8821,2178],{"class":2053},[55,8823,8824],{"class":57,"line":83},[55,8825,68],{"emptyLinePlaceholder":67},[55,8827,8828,8830,8832,8834,8837,8839,8841,8843,8845],{"class":57,"line":89},[55,8829,5939],{"class":2053},[55,8831,5828],{"class":717},[55,8833,2054],{"class":2053},[55,8835,8836],{"class":721},"'beforeinstallprompt'",[55,8838,6008],{"class":2053},[55,8840,6011],{"class":2057},[55,8842,6014],{"class":2053},[55,8844,5839],{"class":2043},[55,8846,5778],{"class":2053},[55,8848,8849],{"class":57,"line":95},[55,8850,8851],{"class":711},"  // Prevent the browser's default mini-infobar\n",[55,8853,8854,8856,8859],{"class":57,"line":101},[55,8855,6037],{"class":2053},[55,8857,8858],{"class":717},"preventDefault",[55,8860,7014],{"class":2053},[55,8862,8863],{"class":57,"line":107},[55,8864,68],{"emptyLinePlaceholder":67},[55,8866,8867],{"class":57,"line":113},[55,8868,8869],{"class":711},"  // Save the event for later use\n",[55,8871,8872,8875,8877],{"class":57,"line":119},[55,8873,8874],{"class":2053},"  deferredPrompt ",[55,8876,3475],{"class":2043},[55,8878,7651],{"class":2053},[55,8880,8881],{"class":57,"line":125},[55,8882,68],{"emptyLinePlaceholder":67},[55,8884,8885],{"class":57,"line":131},[55,8886,8887],{"class":711},"  // Show your custom install button\n",[55,8889,8890,8893,8895,8898],{"class":57,"line":137},[55,8891,8892],{"class":2053},"  installButton.style.display ",[55,8894,3475],{"class":2043},[55,8896,8897],{"class":721}," 'block'",[55,8899,5761],{"class":2053},[55,8901,8902],{"class":57,"line":143},[55,8903,6125],{"class":2053},[55,8905,8906],{"class":57,"line":149},[55,8907,68],{"emptyLinePlaceholder":67},[55,8909,8910,8913,8915,8917,8920,8922,8924,8927,8929],{"class":57,"line":441},[55,8911,8912],{"class":2053},"installButton.",[55,8914,5828],{"class":717},[55,8916,2054],{"class":2053},[55,8918,8919],{"class":721},"'click'",[55,8921,525],{"class":2053},[55,8923,2044],{"class":2043},[55,8925,8926],{"class":2053}," () ",[55,8928,5839],{"class":2043},[55,8930,5778],{"class":2053},[55,8932,8933,8935,8937,8939,8942,8944],{"class":57,"line":447},[55,8934,5724],{"class":2043},[55,8936,2093],{"class":2053},[55,8938,2096],{"class":2043},[55,8940,8941],{"class":2053},"deferredPrompt) ",[55,8943,6293],{"class":2043},[55,8945,5761],{"class":2053},[55,8947,8948],{"class":57,"line":453},[55,8949,68],{"emptyLinePlaceholder":67},[55,8951,8952],{"class":57,"line":459},[55,8953,8954],{"class":711},"  // Show the browser's install prompt\n",[55,8956,8957,8960,8963],{"class":57,"line":464},[55,8958,8959],{"class":2053},"  deferredPrompt.",[55,8961,8962],{"class":717},"prompt",[55,8964,7014],{"class":2053},[55,8966,8967],{"class":57,"line":470},[55,8968,68],{"emptyLinePlaceholder":67},[55,8970,8971],{"class":57,"line":475},[55,8972,8973],{"class":711},"  // Wait for the user's choice\n",[55,8975,8976,8978,8980,8983,8985,8987,8989],{"class":57,"line":481},[55,8977,6668],{"class":2043},[55,8979,3317],{"class":2053},[55,8981,8982],{"class":2069},"outcome",[55,8984,7646],{"class":2053},[55,8986,3475],{"class":2043},[55,8988,2158],{"class":2043},[55,8990,8991],{"class":2053}," deferredPrompt.userChoice;\n",[55,8993,8994],{"class":57,"line":486},[55,8995,68],{"emptyLinePlaceholder":67},[55,8997,8998,9001,9003,9005,9008,9010,9013,9016,9019,9022,9025,9028],{"class":57,"line":492},[55,8999,9000],{"class":2053},"  console.",[55,9002,5747],{"class":717},[55,9004,2054],{"class":2053},[55,9006,9007],{"class":721},"`User ${",[55,9009,8982],{"class":2053},[55,9011,9012],{"class":2043}," ===",[55,9014,9015],{"class":721}," 'accepted'",[55,9017,9018],{"class":2043}," ?",[55,9020,9021],{"class":721}," 'installed'",[55,9023,9024],{"class":2043}," :",[55,9026,9027],{"class":721}," 'dismissed'} the app`",[55,9029,2178],{"class":2053},[55,9031,9032],{"class":57,"line":497},[55,9033,68],{"emptyLinePlaceholder":67},[55,9035,9036],{"class":57,"line":503},[55,9037,9038],{"class":711},"  // The prompt can only be used once\n",[55,9040,9041,9043,9045,9047],{"class":57,"line":508},[55,9042,8874],{"class":2053},[55,9044,3475],{"class":2043},[55,9046,8797],{"class":2069},[55,9048,5761],{"class":2053},[55,9050,9051,9053,9055,9058],{"class":57,"line":2347},[55,9052,8892],{"class":2053},[55,9054,3475],{"class":2043},[55,9056,9057],{"class":721}," 'none'",[55,9059,5761],{"class":2053},[55,9061,9062],{"class":57,"line":2353},[55,9063,6125],{"class":2053},[55,9065,9066],{"class":57,"line":2368},[55,9067,68],{"emptyLinePlaceholder":67},[55,9069,9070],{"class":57,"line":3094},[55,9071,9072],{"class":711},"// Track successful installations\n",[55,9074,9075,9077,9079,9081,9084,9086,9088],{"class":57,"line":3099},[55,9076,5939],{"class":2053},[55,9078,5828],{"class":717},[55,9080,2054],{"class":2053},[55,9082,9083],{"class":721},"'appinstalled'",[55,9085,5836],{"class":2053},[55,9087,5839],{"class":2043},[55,9089,5778],{"class":2053},[55,9091,9092,9094,9096,9098,9101],{"class":57,"line":3104},[55,9093,9000],{"class":2053},[55,9095,5747],{"class":717},[55,9097,2054],{"class":2053},[55,9099,9100],{"class":721},"'App installed to home screen'",[55,9102,2178],{"class":2053},[55,9104,9105],{"class":57,"line":4281},[55,9106,9107],{"class":711},"  // Send an analytics event\n",[55,9109,9110],{"class":57,"line":4287},[55,9111,6125],{"class":2053},[18,9113,9114],{},"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.",[343,9116,9118],{"id":9117},"detecting-if-the-app-is-already-installed","Detecting if the App is Already Installed",[18,9120,9121],{},"You can detect whether the user is running your app in standalone mode (installed) vs. in the browser:",[46,9123,9125],{"className":2025,"code":9124,"language":2027,"meta":51,"style":51},"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",[22,9126,9127,9150,9168,9172,9180,9185,9190],{"__ignoreMap":51},[55,9128,9129,9131,9134,9136,9139,9142,9144,9147],{"class":57,"line":58},[55,9130,6191],{"class":2043},[55,9132,9133],{"class":2069}," isStandalone",[55,9135,2073],{"class":2043},[55,9137,9138],{"class":2053}," window.",[55,9140,9141],{"class":717},"matchMedia",[55,9143,2054],{"class":2053},[55,9145,9146],{"class":721},"'(display-mode: standalone)'",[55,9148,9149],{"class":2053},").matches\n",[55,9151,9152,9155,9158,9160,9163,9165],{"class":57,"line":64},[55,9153,9154],{"class":2043},"  ||",[55,9156,9157],{"class":2053}," window.navigator.standalone ",[55,9159,6604],{"class":2043},[55,9161,9162],{"class":2069}," true",[55,9164,3347],{"class":2053},[55,9166,9167],{"class":711},"// iOS Safari\n",[55,9169,9170],{"class":57,"line":71},[55,9171,68],{"emptyLinePlaceholder":67},[55,9173,9174,9177],{"class":57,"line":77},[55,9175,9176],{"class":2043},"if",[55,9178,9179],{"class":2053}," (isStandalone) {\n",[55,9181,9182],{"class":57,"line":83},[55,9183,9184],{"class":711},"  // Running as installed PWA\n",[55,9186,9187],{"class":57,"line":89},[55,9188,9189],{"class":711},"  // Hide the \"install\" UI, show \"open in browser\" if needed\n",[55,9191,9192],{"class":57,"line":95},[55,9193,2005],{"class":2053},[343,9195,9197],{"id":9196},"ios-safari-considerations","iOS Safari Considerations",[18,9199,9200,9201,9203],{},"iOS Safari does not fire ",[22,9202,8763],{},". 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:",[46,9205,9207],{"className":2025,"code":9206,"language":2027,"meta":51,"style":51},"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",[22,9208,9209,9243,9260,9264,9279,9284,9291],{"__ignoreMap":51},[55,9210,9211,9213,9216,9218,9221,9223,9226,9228,9231,9233,9235,9238,9241],{"class":57,"line":58},[55,9212,6191],{"class":2043},[55,9214,9215],{"class":2069}," isIos",[55,9217,2073],{"class":2043},[55,9219,9220],{"class":721}," /iphone",[55,9222,7791],{"class":2043},[55,9224,9225],{"class":721},"ipad",[55,9227,7791],{"class":2043},[55,9229,9230],{"class":721},"ipod/",[55,9232,532],{"class":2053},[55,9234,7819],{"class":717},[55,9236,9237],{"class":2053},"(navigator.userAgent.",[55,9239,9240],{"class":717},"toLowerCase",[55,9242,6343],{"class":2053},[55,9244,9245,9247,9250,9252,9254,9256,9258],{"class":57,"line":64},[55,9246,6191],{"class":2043},[55,9248,9249],{"class":2069}," isInStandaloneMode",[55,9251,2073],{"class":2043},[55,9253,9157],{"class":2053},[55,9255,6604],{"class":2043},[55,9257,9162],{"class":2069},[55,9259,5761],{"class":2053},[55,9261,9262],{"class":57,"line":71},[55,9263,68],{"emptyLinePlaceholder":67},[55,9265,9266,9268,9271,9274,9276],{"class":57,"line":77},[55,9267,9176],{"class":2043},[55,9269,9270],{"class":2053}," (isIos ",[55,9272,9273],{"class":2043},"&&",[55,9275,2105],{"class":2043},[55,9277,9278],{"class":2053},"isInStandaloneMode) {\n",[55,9280,9281],{"class":57,"line":83},[55,9282,9283],{"class":711},"  // Show \"tap Share, then Add to Home Screen\" instruction UI\n",[55,9285,9286,9289],{"class":57,"line":89},[55,9287,9288],{"class":717},"  showIosInstallInstructions",[55,9290,7014],{"class":2053},[55,9292,9293],{"class":57,"line":95},[55,9294,2005],{"class":2053},[36,9296],{},[13,9298,9300],{"id":9299},"push-notifications","Push Notifications",[18,9302,9303],{},"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).",[343,9305,9307],{"id":9306},"the-permission-flow","The Permission Flow",[18,9309,9310],{},"You must ask the user for permission before sending notifications. Ask at a moment when the value is obvious, never on first page load:",[46,9312,9314],{"className":2025,"code":9313,"language":2027,"meta":51,"style":51},"// 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",[22,9315,9316,9320,9324,9335,9353,9366,9372,9376,9380,9400,9419,9423,9442,9449,9453,9457,9462,9492,9506,9519,9528,9532],{"__ignoreMap":51},[55,9317,9318],{"class":57,"line":58},[55,9319,8780],{"class":711},[55,9321,9322],{"class":57,"line":64},[55,9323,68],{"emptyLinePlaceholder":67},[55,9325,9326,9328,9330,9333],{"class":57,"line":71},[55,9327,2044],{"class":2043},[55,9329,2047],{"class":2043},[55,9331,9332],{"class":717}," requestNotificationPermission",[55,9334,5719],{"class":2053},[55,9336,9337,9339,9341,9343,9345,9348,9350],{"class":57,"line":77},[55,9338,5724],{"class":2043},[55,9340,2093],{"class":2053},[55,9342,2096],{"class":2043},[55,9344,2054],{"class":2053},[55,9346,9347],{"class":721},"'Notification'",[55,9349,5736],{"class":2043},[55,9351,9352],{"class":2053}," window)) {\n",[55,9354,9355,9357,9359,9361,9364],{"class":57,"line":83},[55,9356,5744],{"class":2053},[55,9358,5747],{"class":717},[55,9360,2054],{"class":2053},[55,9362,9363],{"class":721},"'Notifications not supported'",[55,9365,2178],{"class":2053},[55,9367,9368,9370],{"class":57,"line":89},[55,9369,2356],{"class":2043},[55,9371,5761],{"class":2053},[55,9373,9374],{"class":57,"line":95},[55,9375,5766],{"class":2053},[55,9377,9378],{"class":57,"line":101},[55,9379,68],{"emptyLinePlaceholder":67},[55,9381,9382,9384,9387,9389,9392,9394,9396,9398],{"class":57,"line":107},[55,9383,5724],{"class":2043},[55,9385,9386],{"class":2053}," (Notification.permission ",[55,9388,6604],{"class":2043},[55,9390,9391],{"class":721}," 'granted'",[55,9393,6014],{"class":2053},[55,9395,6293],{"class":2043},[55,9397,9391],{"class":721},[55,9399,5761],{"class":2053},[55,9401,9402,9404,9406,9408,9411,9413,9415,9417],{"class":57,"line":113},[55,9403,5724],{"class":2043},[55,9405,9386],{"class":2053},[55,9407,6604],{"class":2043},[55,9409,9410],{"class":721}," 'denied'",[55,9412,6014],{"class":2053},[55,9414,6293],{"class":2043},[55,9416,9410],{"class":721},[55,9418,5761],{"class":2053},[55,9420,9421],{"class":57,"line":119},[55,9422,68],{"emptyLinePlaceholder":67},[55,9424,9425,9427,9430,9432,9434,9437,9440],{"class":57,"line":125},[55,9426,6668],{"class":2043},[55,9428,9429],{"class":2069}," permission",[55,9431,2073],{"class":2043},[55,9433,2158],{"class":2043},[55,9435,9436],{"class":2053}," Notification.",[55,9438,9439],{"class":717},"requestPermission",[55,9441,7014],{"class":2053},[55,9443,9444,9446],{"class":57,"line":131},[55,9445,6784],{"class":2043},[55,9447,9448],{"class":2053}," permission;\n",[55,9450,9451],{"class":57,"line":137},[55,9452,2005],{"class":2053},[55,9454,9455],{"class":57,"line":143},[55,9456,68],{"emptyLinePlaceholder":67},[55,9458,9459],{"class":57,"line":149},[55,9460,9461],{"class":711},"// Only ask after a user action, like subscribing to a newsletter\n",[55,9463,9464,9467,9469,9471,9474,9476,9478,9480,9482,9484,9486,9488,9490],{"class":57,"line":441},[55,9465,9466],{"class":2053},"document.",[55,9468,8814],{"class":717},[55,9470,2054],{"class":2053},[55,9472,9473],{"class":721},"'#subscribe-btn'",[55,9475,6248],{"class":2053},[55,9477,5828],{"class":717},[55,9479,2054],{"class":2053},[55,9481,8919],{"class":721},[55,9483,525],{"class":2053},[55,9485,2044],{"class":2043},[55,9487,8926],{"class":2053},[55,9489,5839],{"class":2043},[55,9491,5778],{"class":2053},[55,9493,9494,9496,9498,9500,9502,9504],{"class":57,"line":447},[55,9495,6668],{"class":2043},[55,9497,9429],{"class":2069},[55,9499,2073],{"class":2043},[55,9501,2158],{"class":2043},[55,9503,9332],{"class":717},[55,9505,7014],{"class":2053},[55,9507,9508,9510,9513,9515,9517],{"class":57,"line":453},[55,9509,5724],{"class":2043},[55,9511,9512],{"class":2053}," (permission ",[55,9514,6604],{"class":2043},[55,9516,9391],{"class":721},[55,9518,2061],{"class":2053},[55,9520,9521,9523,9526],{"class":57,"line":459},[55,9522,2311],{"class":2043},[55,9524,9525],{"class":717}," subscribeUserToPush",[55,9527,7014],{"class":2053},[55,9529,9530],{"class":57,"line":464},[55,9531,5766],{"class":2053},[55,9533,9534],{"class":57,"line":470},[55,9535,6125],{"class":2053},[343,9537,9539],{"id":9538},"subscribing-to-push","Subscribing to Push",[18,9541,9542],{},"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:",[46,9544,9546],{"className":2025,"code":9545,"language":2027,"meta":51,"style":51},"// 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",[22,9547,9548,9552,9556,9566,9579,9583,9603,9616,9632,9636,9640,9645,9659,9669,9682,9696,9700,9704,9717,9721,9725,9730,9745,9792,9844,9859,9902],{"__ignoreMap":51},[55,9549,9550],{"class":57,"line":58},[55,9551,8780],{"class":711},[55,9553,9554],{"class":57,"line":64},[55,9555,68],{"emptyLinePlaceholder":67},[55,9557,9558,9560,9562,9564],{"class":57,"line":71},[55,9559,2044],{"class":2043},[55,9561,2047],{"class":2043},[55,9563,9525],{"class":717},[55,9565,5719],{"class":2053},[55,9567,9568,9570,9572,9574,9576],{"class":57,"line":77},[55,9569,6668],{"class":2043},[55,9571,5785],{"class":2069},[55,9573,2073],{"class":2043},[55,9575,2158],{"class":2043},[55,9577,9578],{"class":2053}," navigator.serviceWorker.ready;\n",[55,9580,9581],{"class":57,"line":83},[55,9582,68],{"emptyLinePlaceholder":67},[55,9584,9585,9587,9590,9592,9594,9597,9600],{"class":57,"line":89},[55,9586,6668],{"class":2043},[55,9588,9589],{"class":2069}," subscription",[55,9591,2073],{"class":2043},[55,9593,2158],{"class":2043},[55,9595,9596],{"class":2053}," registration.pushManager.",[55,9598,9599],{"class":717},"subscribe",[55,9601,9602],{"class":2053},"({\n",[55,9604,9605,9608,9611,9613],{"class":57,"line":95},[55,9606,9607],{"class":2053},"    userVisibleOnly: ",[55,9609,9610],{"class":2069},"true",[55,9612,525],{"class":2053},[55,9614,9615],{"class":711},"// required to be true\n",[55,9617,9618,9621,9624,9626,9629],{"class":57,"line":101},[55,9619,9620],{"class":2053},"    applicationServerKey: ",[55,9622,9623],{"class":717},"urlBase64ToUint8Array",[55,9625,2054],{"class":2053},[55,9627,9628],{"class":2069},"YOUR_PUBLIC_VAPID_KEY",[55,9630,9631],{"class":2053},"),\n",[55,9633,9634],{"class":57,"line":107},[55,9635,6770],{"class":2053},[55,9637,9638],{"class":57,"line":113},[55,9639,68],{"emptyLinePlaceholder":67},[55,9641,9642],{"class":57,"line":119},[55,9643,9644],{"class":711},"  // Send the subscription object to your backend\n",[55,9646,9647,9650,9652,9654,9657],{"class":57,"line":125},[55,9648,9649],{"class":2043},"  await",[55,9651,6104],{"class":717},[55,9653,2054],{"class":2053},[55,9655,9656],{"class":721},"'/api/push-subscriptions'",[55,9658,5803],{"class":2053},[55,9660,9661,9664,9667],{"class":57,"line":131},[55,9662,9663],{"class":2053},"    method: ",[55,9665,9666],{"class":721},"'POST'",[55,9668,2250],{"class":2053},[55,9670,9671,9674,9676,9678,9680],{"class":57,"line":137},[55,9672,9673],{"class":2053},"    headers: { ",[55,9675,8207],{"class":721},[55,9677,3323],{"class":2053},[55,9679,8212],{"class":721},[55,9681,8215],{"class":2053},[55,9683,9684,9687,9689,9691,9693],{"class":57,"line":143},[55,9685,9686],{"class":2053},"    body: ",[55,9688,2201],{"class":2069},[55,9690,532],{"class":2053},[55,9692,2337],{"class":717},[55,9694,9695],{"class":2053},"(subscription),\n",[55,9697,9698],{"class":57,"line":149},[55,9699,6770],{"class":2053},[55,9701,9702],{"class":57,"line":441},[55,9703,68],{"emptyLinePlaceholder":67},[55,9705,9706,9708,9710,9712,9715],{"class":57,"line":447},[55,9707,9000],{"class":2053},[55,9709,5747],{"class":717},[55,9711,2054],{"class":2053},[55,9713,9714],{"class":721},"'User subscribed to push notifications'",[55,9716,2178],{"class":2053},[55,9718,9719],{"class":57,"line":453},[55,9720,2005],{"class":2053},[55,9722,9723],{"class":57,"line":459},[55,9724,68],{"emptyLinePlaceholder":67},[55,9726,9727],{"class":57,"line":464},[55,9728,9729],{"class":711},"// Helper to convert VAPID key\n",[55,9731,9732,9735,9738,9740,9743],{"class":57,"line":470},[55,9733,9734],{"class":2043},"function",[55,9736,9737],{"class":717}," urlBase64ToUint8Array",[55,9739,2054],{"class":2053},[55,9741,9742],{"class":2057},"base64String",[55,9744,2061],{"class":2053},[55,9746,9747,9749,9752,9754,9757,9759,9762,9764,9767,9770,9773,9776,9779,9782,9785,9788,9790],{"class":57,"line":475},[55,9748,6668],{"class":2043},[55,9750,9751],{"class":2069}," padding",[55,9753,2073],{"class":2043},[55,9755,9756],{"class":721}," '='",[55,9758,532],{"class":2053},[55,9760,9761],{"class":717},"repeat",[55,9763,6064],{"class":2053},[55,9765,9766],{"class":2069},"4",[55,9768,9769],{"class":2043}," -",[55,9771,9772],{"class":2053}," (base64String.",[55,9774,9775],{"class":2069},"length",[55,9777,9778],{"class":2043}," %",[55,9780,9781],{"class":2069}," 4",[55,9783,9784],{"class":2053},")) ",[55,9786,9787],{"class":2043},"%",[55,9789,9781],{"class":2069},[55,9791,2178],{"class":2053},[55,9793,9794,9796,9799,9801,9804,9807,9810,9813,9815,9818,9821,9823,9826,9828,9830,9832,9835,9837,9839,9842],{"class":57,"line":481},[55,9795,6668],{"class":2043},[55,9797,9798],{"class":2069}," base64",[55,9800,2073],{"class":2043},[55,9802,9803],{"class":2053}," (base64String ",[55,9805,9806],{"class":2043},"+",[55,9808,9809],{"class":2053}," padding).",[55,9811,9812],{"class":717},"replace",[55,9814,2054],{"class":2053},[55,9816,9817],{"class":721},"/-/",[55,9819,9820],{"class":2043},"g",[55,9822,525],{"class":2053},[55,9824,9825],{"class":721},"'+'",[55,9827,6248],{"class":2053},[55,9829,9812],{"class":717},[55,9831,2054],{"class":2053},[55,9833,9834],{"class":721},"/_/",[55,9836,9820],{"class":2043},[55,9838,525],{"class":2053},[55,9840,9841],{"class":721},"'/'",[55,9843,2178],{"class":2053},[55,9845,9846,9848,9851,9853,9856],{"class":57,"line":486},[55,9847,6668],{"class":2043},[55,9849,9850],{"class":2069}," rawData",[55,9852,2073],{"class":2043},[55,9854,9855],{"class":717}," atob",[55,9857,9858],{"class":2053},"(base64);\n",[55,9860,9861,9863,9866,9869,9872,9875,9878,9880,9882,9885,9887,9889,9892,9895,9897,9899],{"class":57,"line":492},[55,9862,6784],{"class":2043},[55,9864,9865],{"class":2053}," Uint8Array.",[55,9867,9868],{"class":717},"from",[55,9870,9871],{"class":2053},"([",[55,9873,9874],{"class":2043},"...",[55,9876,9877],{"class":2053},"rawData].",[55,9879,7185],{"class":717},[55,9881,6064],{"class":2053},[55,9883,9884],{"class":2057},"char",[55,9886,6014],{"class":2053},[55,9888,5839],{"class":2043},[55,9890,9891],{"class":2053}," char.",[55,9893,9894],{"class":717},"charCodeAt",[55,9896,2054],{"class":2053},[55,9898,8584],{"class":2069},[55,9900,9901],{"class":2053},")));\n",[55,9903,9904],{"class":57,"line":497},[55,9905,2005],{"class":2053},[343,9907,9909],{"id":9908},"handling-push-in-the-service-worker","Handling Push in the Service Worker",[18,9911,9912,9913,9916],{},"When your server sends a push message, the service worker receives it via the ",[22,9914,9915],{},"push"," event:",[46,9918,9920],{"className":2025,"code":9919,"language":2027,"meta":51,"style":51},"// 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",[22,9921,9922,9926,9930,9951,9966,9970,9986,9990,10001,10006,10016,10026,10031,10036,10041,10057,10071,10076,10084,10097,10107,10112,10116,10124,10135,10139,10143,10147,10168,10178,10182,10200,10204,10222,10230,10260,10265,10283,10306,10319,10324,10328,10333,10351,10355,10359],{"__ignoreMap":51},[55,9923,9924],{"class":57,"line":58},[55,9925,5989],{"class":711},[55,9927,9928],{"class":57,"line":64},[55,9929,68],{"emptyLinePlaceholder":67},[55,9931,9932,9934,9936,9938,9941,9943,9945,9947,9949],{"class":57,"line":71},[55,9933,5998],{"class":2053},[55,9935,5828],{"class":717},[55,9937,2054],{"class":2053},[55,9939,9940],{"class":721},"'push'",[55,9942,6008],{"class":2053},[55,9944,6011],{"class":2057},[55,9946,6014],{"class":2053},[55,9948,5839],{"class":2043},[55,9950,5778],{"class":2053},[55,9952,9953,9955,9957,9959,9962,9964],{"class":57,"line":77},[55,9954,5724],{"class":2043},[55,9956,2093],{"class":2053},[55,9958,2096],{"class":2043},[55,9960,9961],{"class":2053},"event.data) ",[55,9963,6293],{"class":2043},[55,9965,5761],{"class":2053},[55,9967,9968],{"class":57,"line":83},[55,9969,68],{"emptyLinePlaceholder":67},[55,9971,9972,9974,9977,9979,9982,9984],{"class":57,"line":89},[55,9973,6668],{"class":2043},[55,9975,9976],{"class":2069}," data",[55,9978,2073],{"class":2043},[55,9980,9981],{"class":2053}," event.data.",[55,9983,4938],{"class":717},[55,9985,7014],{"class":2053},[55,9987,9988],{"class":57,"line":95},[55,9989,68],{"emptyLinePlaceholder":67},[55,9991,9992,9994,9997,9999],{"class":57,"line":101},[55,9993,6668],{"class":2043},[55,9995,9996],{"class":2069}," options",[55,9998,2073],{"class":2043},[55,10000,5778],{"class":2053},[55,10002,10003],{"class":57,"line":107},[55,10004,10005],{"class":2053},"    body: data.body,\n",[55,10007,10008,10011,10014],{"class":57,"line":113},[55,10009,10010],{"class":2053},"    icon: ",[55,10012,10013],{"class":721},"'/icons/icon-192x192.png'",[55,10015,2250],{"class":2053},[55,10017,10018,10021,10024],{"class":57,"line":119},[55,10019,10020],{"class":2053},"    badge: ",[55,10022,10023],{"class":721},"'/icons/badge-72x72.png'",[55,10025,2250],{"class":2053},[55,10027,10028],{"class":57,"line":125},[55,10029,10030],{"class":2053},"    image: data.image,\n",[55,10032,10033],{"class":57,"line":131},[55,10034,10035],{"class":2053},"    data: { url: data.url },\n",[55,10037,10038],{"class":57,"line":137},[55,10039,10040],{"class":2053},"    actions: [\n",[55,10042,10043,10046,10049,10052,10055],{"class":57,"line":143},[55,10044,10045],{"class":2053},"      { action: ",[55,10047,10048],{"class":721},"'open'",[55,10050,10051],{"class":2053},", title: ",[55,10053,10054],{"class":721},"'Open'",[55,10056,8215],{"class":2053},[55,10058,10059,10061,10064,10066,10069],{"class":57,"line":149},[55,10060,10045],{"class":2053},[55,10062,10063],{"class":721},"'dismiss'",[55,10065,10051],{"class":2053},[55,10067,10068],{"class":721},"'Dismiss'",[55,10070,8215],{"class":2053},[55,10072,10073],{"class":57,"line":441},[55,10074,10075],{"class":2053},"    ],\n",[55,10077,10078,10081],{"class":57,"line":447},[55,10079,10080],{"class":2053},"    tag: data.tag,          ",[55,10082,10083],{"class":711},"// replaces existing notification with same tag\n",[55,10085,10086,10089,10091,10094],{"class":57,"line":453},[55,10087,10088],{"class":2053},"    renotify: ",[55,10090,9610],{"class":2069},[55,10092,10093],{"class":2053},",         ",[55,10095,10096],{"class":711},"// vibrate even if same tag\n",[55,10098,10099,10102,10105],{"class":57,"line":459},[55,10100,10101],{"class":2053},"    requireInteraction: ",[55,10103,10104],{"class":2069},"false",[55,10106,2250],{"class":2053},[55,10108,10109],{"class":57,"line":464},[55,10110,10111],{"class":2053},"  };\n",[55,10113,10114],{"class":57,"line":470},[55,10115,68],{"emptyLinePlaceholder":67},[55,10117,10118,10120,10122],{"class":57,"line":475},[55,10119,6037],{"class":2053},[55,10121,6930],{"class":717},[55,10123,2242],{"class":2053},[55,10125,10126,10129,10132],{"class":57,"line":481},[55,10127,10128],{"class":2053},"    self.registration.",[55,10130,10131],{"class":717},"showNotification",[55,10133,10134],{"class":2053},"(data.title, options)\n",[55,10136,10137],{"class":57,"line":486},[55,10138,6120],{"class":2053},[55,10140,10141],{"class":57,"line":492},[55,10142,6125],{"class":2053},[55,10144,10145],{"class":57,"line":497},[55,10146,68],{"emptyLinePlaceholder":67},[55,10148,10149,10151,10153,10155,10158,10160,10162,10164,10166],{"class":57,"line":503},[55,10150,5998],{"class":2053},[55,10152,5828],{"class":717},[55,10154,2054],{"class":2053},[55,10156,10157],{"class":721},"'notificationclick'",[55,10159,6008],{"class":2053},[55,10161,6011],{"class":2057},[55,10163,6014],{"class":2053},[55,10165,5839],{"class":2043},[55,10167,5778],{"class":2053},[55,10169,10170,10173,10176],{"class":57,"line":508},[55,10171,10172],{"class":2053},"  event.notification.",[55,10174,10175],{"class":717},"close",[55,10177,7014],{"class":2053},[55,10179,10180],{"class":57,"line":2347},[55,10181,68],{"emptyLinePlaceholder":67},[55,10183,10184,10186,10189,10191,10194,10196,10198],{"class":57,"line":2353},[55,10185,5724],{"class":2043},[55,10187,10188],{"class":2053}," (event.action ",[55,10190,6604],{"class":2043},[55,10192,10193],{"class":721}," 'dismiss'",[55,10195,6014],{"class":2053},[55,10197,6293],{"class":2043},[55,10199,5761],{"class":2053},[55,10201,10202],{"class":57,"line":2368},[55,10203,68],{"emptyLinePlaceholder":67},[55,10205,10206,10208,10210,10212,10215,10217,10220],{"class":57,"line":3094},[55,10207,6668],{"class":2043},[55,10209,7658],{"class":2069},[55,10211,2073],{"class":2043},[55,10213,10214],{"class":2053}," event.notification.data.url ",[55,10216,6790],{"class":2043},[55,10218,10219],{"class":721}," '/'",[55,10221,5761],{"class":2053},[55,10223,10224,10226,10228],{"class":57,"line":3099},[55,10225,6037],{"class":2053},[55,10227,6930],{"class":717},[55,10229,2242],{"class":2053},[55,10231,10232,10235,10238,10241,10244,10247,10249,10251,10254,10256,10258],{"class":57,"line":3104},[55,10233,10234],{"class":2053},"    clients.",[55,10236,10237],{"class":717},"matchAll",[55,10239,10240],{"class":2053},"({ type: ",[55,10242,10243],{"class":721},"'window'",[55,10245,10246],{"class":2053}," }).",[55,10248,6061],{"class":717},[55,10250,6064],{"class":2053},[55,10252,10253],{"class":2057},"windowClients",[55,10255,6014],{"class":2053},[55,10257,5839],{"class":2043},[55,10259,5778],{"class":2053},[55,10261,10262],{"class":57,"line":4281},[55,10263,10264],{"class":711},"      // If a window is already open, focus it\n",[55,10266,10267,10270,10272,10274,10277,10280],{"class":57,"line":4287},[55,10268,10269],{"class":2043},"      for",[55,10271,2093],{"class":2053},[55,10273,6191],{"class":2043},[55,10275,10276],{"class":2069}," client",[55,10278,10279],{"class":2043}," of",[55,10281,10282],{"class":2053}," windowClients) {\n",[55,10284,10285,10288,10291,10293,10296,10298,10301,10303],{"class":57,"line":4293},[55,10286,10287],{"class":2043},"        if",[55,10289,10290],{"class":2053}," (client.url ",[55,10292,6604],{"class":2043},[55,10294,10295],{"class":2053}," url ",[55,10297,9273],{"class":2043},[55,10299,10300],{"class":721}," 'focus'",[55,10302,5736],{"class":2043},[55,10304,10305],{"class":2053}," client) {\n",[55,10307,10308,10311,10314,10317],{"class":57,"line":4298},[55,10309,10310],{"class":2043},"          return",[55,10312,10313],{"class":2053}," client.",[55,10315,10316],{"class":717},"focus",[55,10318,7014],{"class":2053},[55,10320,10321],{"class":57,"line":4304},[55,10322,10323],{"class":2053},"        }\n",[55,10325,10326],{"class":57,"line":4309},[55,10327,6096],{"class":2053},[55,10329,10330],{"class":57,"line":4315},[55,10331,10332],{"class":711},"      // Otherwise open a new window\n",[55,10334,10335,10337,10340,10342,10345,10348],{"class":57,"line":4321},[55,10336,6078],{"class":2043},[55,10338,10339],{"class":2053}," (clients.openWindow) ",[55,10341,6293],{"class":2043},[55,10343,10344],{"class":2053}," clients.",[55,10346,10347],{"class":717},"openWindow",[55,10349,10350],{"class":2053},"(url);\n",[55,10352,10353],{"class":57,"line":4326},[55,10354,6115],{"class":2053},[55,10356,10357],{"class":57,"line":4332},[55,10358,6120],{"class":2053},[55,10360,10361],{"class":57,"line":4337},[55,10362,6125],{"class":2053},[343,10364,10366],{"id":10365},"sending-push-from-your-server","Sending Push from Your Server",[18,10368,10369,10370,10373],{},"On the server side, you use a library like ",[22,10371,10372],{},"web-push"," (Node.js) to send the push message:",[46,10375,10377],{"className":2025,"code":10376,"language":2027,"meta":51,"style":51},"// 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",[22,10378,10379,10384,10399,10403,10413,10420,10430,10437,10441,10445,10466,10472,10484,10489,10500,10510,10520,10530,10540,10545,10549,10557,10571,10576,10587,10591,10595],{"__ignoreMap":51},[55,10380,10381],{"class":57,"line":58},[55,10382,10383],{"class":711},"// server.js (Node.js)\n",[55,10385,10386,10389,10392,10394,10397],{"class":57,"line":64},[55,10387,10388],{"class":2043},"import",[55,10390,10391],{"class":2053}," webpush ",[55,10393,9868],{"class":2043},[55,10395,10396],{"class":721}," 'web-push'",[55,10398,5761],{"class":2053},[55,10400,10401],{"class":57,"line":71},[55,10402,68],{"emptyLinePlaceholder":67},[55,10404,10405,10408,10411],{"class":57,"line":77},[55,10406,10407],{"class":2053},"webpush.",[55,10409,10410],{"class":717},"setVapidDetails",[55,10412,2242],{"class":2053},[55,10414,10415,10418],{"class":57,"line":83},[55,10416,10417],{"class":721},"  'mailto:hello@yourapp.com'",[55,10419,2250],{"class":2053},[55,10421,10422,10425,10428],{"class":57,"line":89},[55,10423,10424],{"class":2053},"  process.env.",[55,10426,10427],{"class":2069},"VAPID_PUBLIC_KEY",[55,10429,2250],{"class":2053},[55,10431,10432,10434],{"class":57,"line":95},[55,10433,10424],{"class":2053},[55,10435,10436],{"class":2069},"VAPID_PRIVATE_KEY\n",[55,10438,10439],{"class":57,"line":101},[55,10440,2178],{"class":2053},[55,10442,10443],{"class":57,"line":107},[55,10444,68],{"emptyLinePlaceholder":67},[55,10446,10447,10449,10451,10454,10456,10459,10461,10464],{"class":57,"line":113},[55,10448,2044],{"class":2043},[55,10450,2047],{"class":2043},[55,10452,10453],{"class":717}," sendPushNotification",[55,10455,2054],{"class":2053},[55,10457,10458],{"class":2057},"subscription",[55,10460,525],{"class":2053},[55,10462,10463],{"class":2057},"payload",[55,10465,2061],{"class":2053},[55,10467,10468,10470],{"class":57,"line":119},[55,10469,5775],{"class":2043},[55,10471,5778],{"class":2053},[55,10473,10474,10476,10479,10482],{"class":57,"line":125},[55,10475,2311],{"class":2043},[55,10477,10478],{"class":2053}," webpush.",[55,10480,10481],{"class":717},"sendNotification",[55,10483,2242],{"class":2053},[55,10485,10486],{"class":57,"line":131},[55,10487,10488],{"class":2053},"      subscription,\n",[55,10490,10491,10494,10496,10498],{"class":57,"line":137},[55,10492,10493],{"class":2069},"      JSON",[55,10495,532],{"class":2053},[55,10497,2337],{"class":717},[55,10499,9602],{"class":2053},[55,10501,10502,10505,10508],{"class":57,"line":143},[55,10503,10504],{"class":2053},"        title: ",[55,10506,10507],{"class":721},"'New message'",[55,10509,2250],{"class":2053},[55,10511,10512,10515,10518],{"class":57,"line":149},[55,10513,10514],{"class":2053},"        body: ",[55,10516,10517],{"class":721},"'You have a new message from Alice'",[55,10519,2250],{"class":2053},[55,10521,10522,10525,10528],{"class":57,"line":441},[55,10523,10524],{"class":2053},"        url: ",[55,10526,10527],{"class":721},"'/messages/123'",[55,10529,2250],{"class":2053},[55,10531,10532,10535,10538],{"class":57,"line":447},[55,10533,10534],{"class":2053},"        tag: ",[55,10536,10537],{"class":721},"'message-123'",[55,10539,2250],{"class":2053},[55,10541,10542],{"class":57,"line":453},[55,10543,10544],{"class":2053},"      })\n",[55,10546,10547],{"class":57,"line":459},[55,10548,2260],{"class":2053},[55,10550,10551,10553,10555],{"class":57,"line":464},[55,10552,5896],{"class":2053},[55,10554,5899],{"class":2043},[55,10556,5902],{"class":2053},[55,10558,10559,10561,10564,10566,10569],{"class":57,"line":470},[55,10560,2090],{"class":2043},[55,10562,10563],{"class":2053}," (error.statusCode ",[55,10565,6604],{"class":2043},[55,10567,10568],{"class":2069}," 410",[55,10570,2061],{"class":2053},[55,10572,10573],{"class":57,"line":475},[55,10574,10575],{"class":711},"      // Subscription expired: remove from database\n",[55,10577,10578,10581,10584],{"class":57,"line":481},[55,10579,10580],{"class":2043},"      await",[55,10582,10583],{"class":717}," removeSubscription",[55,10585,10586],{"class":2053},"(subscription.endpoint);\n",[55,10588,10589],{"class":57,"line":486},[55,10590,1962],{"class":2053},[55,10592,10593],{"class":57,"line":492},[55,10594,5766],{"class":2053},[55,10596,10597],{"class":57,"line":497},[55,10598,2005],{"class":2053},[18,10600,10601],{},"The push message flows like this:",[18,10603,10604],{},[4835,10605],{"alt":10606,"src":10607},"Push notification delivery flow diagram","/blog-post-images/progressive-web-apps-deep-dive/push-notification-delivery-pipeline.png",[18,10609,10610],{},"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.",[36,10612],{},[13,10614,10616],{"id":10615},"background-sync","Background Sync",[18,10618,10619],{},"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.",[18,10621,10622],{},"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.",[343,10624,10626],{"id":10625},"registering-a-sync","Registering a Sync",[46,10628,10630],{"className":2025,"code":10629,"language":2027,"meta":51,"style":51},"// 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",[22,10631,10632,10636,10640,10656,10661,10676,10680,10685,10697,10713,10717,10730],{"__ignoreMap":51},[55,10633,10634],{"class":57,"line":58},[55,10635,8780],{"class":711},[55,10637,10638],{"class":57,"line":64},[55,10639,68],{"emptyLinePlaceholder":67},[55,10641,10642,10644,10646,10649,10651,10654],{"class":57,"line":71},[55,10643,2044],{"class":2043},[55,10645,2047],{"class":2043},[55,10647,10648],{"class":717}," sendMessageWithBackgroundSync",[55,10650,2054],{"class":2053},[55,10652,10653],{"class":2057},"message",[55,10655,2061],{"class":2053},[55,10657,10658],{"class":57,"line":77},[55,10659,10660],{"class":711},"  // Save the message to IndexedDB first\n",[55,10662,10663,10665,10668,10670,10673],{"class":57,"line":83},[55,10664,9649],{"class":2043},[55,10666,10667],{"class":717}," saveToIndexedDB",[55,10669,2054],{"class":2053},[55,10671,10672],{"class":721},"'outbox'",[55,10674,10675],{"class":2053},", message);\n",[55,10677,10678],{"class":57,"line":89},[55,10679,68],{"emptyLinePlaceholder":67},[55,10681,10682],{"class":57,"line":95},[55,10683,10684],{"class":711},"  // Register a sync event\n",[55,10686,10687,10689,10691,10693,10695],{"class":57,"line":101},[55,10688,6668],{"class":2043},[55,10690,5785],{"class":2069},[55,10692,2073],{"class":2043},[55,10694,2158],{"class":2043},[55,10696,9578],{"class":2053},[55,10698,10699,10701,10704,10706,10708,10711],{"class":57,"line":107},[55,10700,9649],{"class":2043},[55,10702,10703],{"class":2053}," registration.sync.",[55,10705,5795],{"class":717},[55,10707,2054],{"class":2053},[55,10709,10710],{"class":721},"'sync-outbox'",[55,10712,2178],{"class":2053},[55,10714,10715],{"class":57,"line":113},[55,10716,68],{"emptyLinePlaceholder":67},[55,10718,10719,10721,10723,10725,10728],{"class":57,"line":119},[55,10720,9000],{"class":2053},[55,10722,5747],{"class":717},[55,10724,2054],{"class":2053},[55,10726,10727],{"class":721},"'Message queued for background sync'",[55,10729,2178],{"class":2053},[55,10731,10732],{"class":57,"line":125},[55,10733,2005],{"class":2053},[343,10735,10737],{"id":10736},"handling-sync-in-the-service-worker","Handling Sync in the Service Worker",[46,10739,10741],{"className":2025,"code":10740,"language":2027,"meta":51,"style":51},"// 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",[22,10742,10743,10747,10751,10772,10786,10799,10803,10807,10811,10822,10842,10846,10863,10870,10889,10898,10911,10924,10929,10933,10939,10954,10958,10967,10972,10980,10984,10988],{"__ignoreMap":51},[55,10744,10745],{"class":57,"line":58},[55,10746,5989],{"class":711},[55,10748,10749],{"class":57,"line":64},[55,10750,68],{"emptyLinePlaceholder":67},[55,10752,10753,10755,10757,10759,10762,10764,10766,10768,10770],{"class":57,"line":71},[55,10754,5998],{"class":2053},[55,10756,5828],{"class":717},[55,10758,2054],{"class":2053},[55,10760,10761],{"class":721},"'sync'",[55,10763,6008],{"class":2053},[55,10765,6011],{"class":2057},[55,10767,6014],{"class":2053},[55,10769,5839],{"class":2043},[55,10771,5778],{"class":2053},[55,10773,10774,10776,10779,10781,10784],{"class":57,"line":77},[55,10775,5724],{"class":2043},[55,10777,10778],{"class":2053}," (event.tag ",[55,10780,6604],{"class":2043},[55,10782,10783],{"class":721}," 'sync-outbox'",[55,10785,2061],{"class":2053},[55,10787,10788,10790,10792,10794,10797],{"class":57,"line":83},[55,10789,6614],{"class":2053},[55,10791,6930],{"class":717},[55,10793,2054],{"class":2053},[55,10795,10796],{"class":717},"flushOutbox",[55,10798,6343],{"class":2053},[55,10800,10801],{"class":57,"line":89},[55,10802,5766],{"class":2053},[55,10804,10805],{"class":57,"line":95},[55,10806,6125],{"class":2053},[55,10808,10809],{"class":57,"line":101},[55,10810,68],{"emptyLinePlaceholder":67},[55,10812,10813,10815,10817,10820],{"class":57,"line":107},[55,10814,2044],{"class":2043},[55,10816,2047],{"class":2043},[55,10818,10819],{"class":717}," flushOutbox",[55,10821,5719],{"class":2053},[55,10823,10824,10826,10829,10831,10833,10836,10838,10840],{"class":57,"line":113},[55,10825,6668],{"class":2043},[55,10827,10828],{"class":2069}," messages",[55,10830,2073],{"class":2043},[55,10832,2158],{"class":2043},[55,10834,10835],{"class":717}," getAllFromIndexedDB",[55,10837,2054],{"class":2053},[55,10839,10672],{"class":721},[55,10841,2178],{"class":2053},[55,10843,10844],{"class":57,"line":119},[55,10845,68],{"emptyLinePlaceholder":67},[55,10847,10848,10851,10853,10855,10858,10860],{"class":57,"line":125},[55,10849,10850],{"class":2043},"  for",[55,10852,2093],{"class":2053},[55,10854,6191],{"class":2043},[55,10856,10857],{"class":2069}," message",[55,10859,10279],{"class":2043},[55,10861,10862],{"class":2053}," messages) {\n",[55,10864,10865,10868],{"class":57,"line":131},[55,10866,10867],{"class":2043},"    try",[55,10869,5778],{"class":2053},[55,10871,10872,10874,10876,10878,10880,10882,10884,10887],{"class":57,"line":137},[55,10873,5846],{"class":2043},[55,10875,2119],{"class":2069},[55,10877,2073],{"class":2043},[55,10879,2158],{"class":2043},[55,10881,6104],{"class":717},[55,10883,2054],{"class":2053},[55,10885,10886],{"class":721},"'/api/messages'",[55,10888,5803],{"class":2053},[55,10890,10891,10894,10896],{"class":57,"line":143},[55,10892,10893],{"class":2053},"        method: ",[55,10895,9666],{"class":721},[55,10897,2250],{"class":2053},[55,10899,10900,10903,10905,10907,10909],{"class":57,"line":149},[55,10901,10902],{"class":2053},"        headers: { ",[55,10904,8207],{"class":721},[55,10906,3323],{"class":2053},[55,10908,8212],{"class":721},[55,10910,8215],{"class":2053},[55,10912,10913,10915,10917,10919,10921],{"class":57,"line":441},[55,10914,10514],{"class":2053},[55,10916,2201],{"class":2069},[55,10918,532],{"class":2053},[55,10920,2337],{"class":717},[55,10922,10923],{"class":2053},"(message),\n",[55,10925,10926],{"class":57,"line":447},[55,10927,10928],{"class":2053},"      });\n",[55,10930,10931],{"class":57,"line":453},[55,10932,68],{"emptyLinePlaceholder":67},[55,10934,10935,10937],{"class":57,"line":459},[55,10936,6078],{"class":2043},[55,10938,6326],{"class":2053},[55,10940,10941,10944,10947,10949,10951],{"class":57,"line":464},[55,10942,10943],{"class":2043},"        await",[55,10945,10946],{"class":717}," removeFromIndexedDB",[55,10948,2054],{"class":2053},[55,10950,10672],{"class":721},[55,10952,10953],{"class":2053},", message.id);\n",[55,10955,10956],{"class":57,"line":470},[55,10957,6096],{"class":2053},[55,10959,10960,10963,10965],{"class":57,"line":475},[55,10961,10962],{"class":2053},"    } ",[55,10964,5899],{"class":2043},[55,10966,5902],{"class":2053},[55,10968,10969],{"class":57,"line":481},[55,10970,10971],{"class":711},"      // Network still down: throw to signal the sync should retry\n",[55,10973,10974,10977],{"class":57,"line":486},[55,10975,10976],{"class":2043},"      throw",[55,10978,10979],{"class":2053}," error;\n",[55,10981,10982],{"class":57,"line":492},[55,10983,1962],{"class":2053},[55,10985,10986],{"class":57,"line":497},[55,10987,5766],{"class":2053},[55,10989,10990],{"class":57,"line":503},[55,10991,2005],{"class":2053},[18,10993,10994,10995,10998],{},"If ",[22,10996,10997],{},"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.",[36,11000],{},[13,11002,11004],{"id":11003},"periodic-background-sync","Periodic Background Sync",[18,11006,11007,11008,11010],{},"A newer API, ",[518,11009,11004],{},", 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.",[46,11012,11014],{"className":2025,"code":11013,"language":2027,"meta":51,"style":51},"// 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",[22,11015,11016,11020,11024,11035,11047,11051,11069,11079,11083,11087,11100,11116,11144,11148,11152],{"__ignoreMap":51},[55,11017,11018],{"class":57,"line":58},[55,11019,8780],{"class":711},[55,11021,11022],{"class":57,"line":64},[55,11023,68],{"emptyLinePlaceholder":67},[55,11025,11026,11028,11030,11033],{"class":57,"line":71},[55,11027,2044],{"class":2043},[55,11029,2047],{"class":2043},[55,11031,11032],{"class":717}," registerPeriodicSync",[55,11034,5719],{"class":2053},[55,11036,11037,11039,11041,11043,11045],{"class":57,"line":77},[55,11038,6668],{"class":2043},[55,11040,5785],{"class":2069},[55,11042,2073],{"class":2043},[55,11044,2158],{"class":2043},[55,11046,9578],{"class":2053},[55,11048,11049],{"class":57,"line":83},[55,11050,68],{"emptyLinePlaceholder":67},[55,11052,11053,11055,11058,11060,11062,11065,11067],{"class":57,"line":89},[55,11054,6668],{"class":2043},[55,11056,11057],{"class":2069}," status",[55,11059,2073],{"class":2043},[55,11061,2158],{"class":2043},[55,11063,11064],{"class":2053}," navigator.permissions.",[55,11066,2239],{"class":717},[55,11068,9602],{"class":2053},[55,11070,11071,11074,11077],{"class":57,"line":95},[55,11072,11073],{"class":2053},"    name: ",[55,11075,11076],{"class":721},"'periodic-background-sync'",[55,11078,2250],{"class":2053},[55,11080,11081],{"class":57,"line":101},[55,11082,6770],{"class":2053},[55,11084,11085],{"class":57,"line":107},[55,11086,68],{"emptyLinePlaceholder":67},[55,11088,11089,11091,11094,11096,11098],{"class":57,"line":113},[55,11090,5724],{"class":2043},[55,11092,11093],{"class":2053}," (status.state ",[55,11095,6604],{"class":2043},[55,11097,9391],{"class":721},[55,11099,2061],{"class":2053},[55,11101,11102,11104,11107,11109,11111,11114],{"class":57,"line":119},[55,11103,2311],{"class":2043},[55,11105,11106],{"class":2053}," registration.periodicSync.",[55,11108,5795],{"class":717},[55,11110,2054],{"class":2053},[55,11112,11113],{"class":721},"'refresh-content'",[55,11115,5803],{"class":2053},[55,11117,11118,11121,11124,11127,11130,11132,11134,11136,11139,11141],{"class":57,"line":125},[55,11119,11120],{"class":2053},"      minInterval: ",[55,11122,11123],{"class":2069},"24",[55,11125,11126],{"class":2043}," *",[55,11128,11129],{"class":2069}," 60",[55,11131,11126],{"class":2043},[55,11133,11129],{"class":2069},[55,11135,11126],{"class":2043},[55,11137,11138],{"class":2069}," 1000",[55,11140,525],{"class":2053},[55,11142,11143],{"class":711},"// once per day at minimum\n",[55,11145,11146],{"class":57,"line":131},[55,11147,5816],{"class":2053},[55,11149,11150],{"class":57,"line":137},[55,11151,5766],{"class":2053},[55,11153,11154],{"class":57,"line":143},[55,11155,2005],{"class":2053},[46,11157,11159],{"className":2025,"code":11158,"language":2027,"meta":51,"style":51},"// 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",[22,11160,11161,11165,11169,11190,11203,11216,11220,11224,11228,11239,11259,11280,11284,11296,11318,11332,11343,11347,11351],{"__ignoreMap":51},[55,11162,11163],{"class":57,"line":58},[55,11164,5989],{"class":711},[55,11166,11167],{"class":57,"line":64},[55,11168,68],{"emptyLinePlaceholder":67},[55,11170,11171,11173,11175,11177,11180,11182,11184,11186,11188],{"class":57,"line":71},[55,11172,5998],{"class":2053},[55,11174,5828],{"class":717},[55,11176,2054],{"class":2053},[55,11178,11179],{"class":721},"'periodicsync'",[55,11181,6008],{"class":2053},[55,11183,6011],{"class":2057},[55,11185,6014],{"class":2053},[55,11187,5839],{"class":2043},[55,11189,5778],{"class":2053},[55,11191,11192,11194,11196,11198,11201],{"class":57,"line":77},[55,11193,5724],{"class":2043},[55,11195,10778],{"class":2053},[55,11197,6604],{"class":2043},[55,11199,11200],{"class":721}," 'refresh-content'",[55,11202,2061],{"class":2053},[55,11204,11205,11207,11209,11211,11214],{"class":57,"line":83},[55,11206,6614],{"class":2053},[55,11208,6930],{"class":717},[55,11210,2054],{"class":2053},[55,11212,11213],{"class":717},"refreshCachedContent",[55,11215,6343],{"class":2053},[55,11217,11218],{"class":57,"line":89},[55,11219,5766],{"class":2053},[55,11221,11222],{"class":57,"line":95},[55,11223,6125],{"class":2053},[55,11225,11226],{"class":57,"line":101},[55,11227,68],{"emptyLinePlaceholder":67},[55,11229,11230,11232,11234,11237],{"class":57,"line":107},[55,11231,2044],{"class":2043},[55,11233,2047],{"class":2043},[55,11235,11236],{"class":717}," refreshCachedContent",[55,11238,5719],{"class":2053},[55,11240,11241,11243,11245,11247,11249,11251,11253,11255,11257],{"class":57,"line":113},[55,11242,6668],{"class":2043},[55,11244,6459],{"class":2069},[55,11246,2073],{"class":2043},[55,11248,2158],{"class":2043},[55,11250,6466],{"class":2053},[55,11252,6240],{"class":717},[55,11254,2054],{"class":2053},[55,11256,7072],{"class":721},[55,11258,2178],{"class":2053},[55,11260,11261,11263,11266,11268,11270,11273,11275,11278],{"class":57,"line":119},[55,11262,6668],{"class":2043},[55,11264,11265],{"class":2069}," urls",[55,11267,2073],{"class":2043},[55,11269,7064],{"class":2053},[55,11271,11272],{"class":721},"'/api/posts'",[55,11274,525],{"class":2053},[55,11276,11277],{"class":721},"'/api/featured'",[55,11279,6898],{"class":2053},[55,11281,11282],{"class":57,"line":125},[55,11283,68],{"emptyLinePlaceholder":67},[55,11285,11286,11288,11290,11292,11294],{"class":57,"line":131},[55,11287,9649],{"class":2043},[55,11289,7137],{"class":2069},[55,11291,532],{"class":2053},[55,11293,7142],{"class":717},[55,11295,2242],{"class":2053},[55,11297,11298,11301,11303,11305,11307,11309,11312,11314,11316],{"class":57,"line":137},[55,11299,11300],{"class":2053},"    urls.",[55,11302,7185],{"class":717},[55,11304,2054],{"class":2053},[55,11306,2044],{"class":2043},[55,11308,2093],{"class":2053},[55,11310,11311],{"class":2057},"url",[55,11313,6014],{"class":2053},[55,11315,5839],{"class":2043},[55,11317,5778],{"class":2053},[55,11319,11320,11322,11324,11326,11328,11330],{"class":57,"line":143},[55,11321,5846],{"class":2043},[55,11323,2119],{"class":2069},[55,11325,2073],{"class":2043},[55,11327,2158],{"class":2043},[55,11329,6104],{"class":717},[55,11331,10350],{"class":2053},[55,11333,11334,11336,11338,11340],{"class":57,"line":149},[55,11335,6078],{"class":2043},[55,11337,8126],{"class":2053},[55,11339,6334],{"class":717},[55,11341,11342],{"class":2053},"(url, response);\n",[55,11344,11345],{"class":57,"line":441},[55,11346,6115],{"class":2053},[55,11348,11349],{"class":57,"line":447},[55,11350,6120],{"class":2053},[55,11352,11353],{"class":57,"line":453},[55,11354,2005],{"class":2053},[18,11356,11357,11358,11361],{},"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 ",[22,11359,11360],{},"minInterval"," is a hint, not a guarantee.",[36,11363],{},[13,11365,11367],{"id":11366},"workbox-the-production-tool","Workbox: The Production Tool",[18,11369,11370,11371,11374],{},"Writing service worker code by hand is educational but error-prone in production. Google's ",[518,11372,11373],{},"Workbox"," library provides battle-tested implementations of all the caching strategies, plus tooling to generate the app shell file list automatically.",[343,11376,11378],{"id":11377},"workbox-with-vite","Workbox with Vite",[46,11380,11382],{"className":2025,"code":11381,"language":2027,"meta":51,"style":51},"// 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",[22,11383,11384,11389,11403,11407,11417,11422,11429,11439,11444,11449,11459,11463,11468,11473,11478,11483,11520,11530,11535,11545,11559,11569,11574,11579,11583,11588,11630,11639,11643,11652,11676,11680,11684,11688,11693,11728,11737,11741,11750,11768,11772,11776,11781,11786,11791,11801,11811,11821,11826,11847,11865,11869,11873,11878,11882],{"__ignoreMap":51},[55,11385,11386],{"class":57,"line":58},[55,11387,11388],{"class":711},"// vite.config.js\n",[55,11390,11391,11393,11396,11398,11401],{"class":57,"line":64},[55,11392,10388],{"class":2043},[55,11394,11395],{"class":2053}," { VitePWA } ",[55,11397,9868],{"class":2043},[55,11399,11400],{"class":721}," 'vite-plugin-pwa'",[55,11402,5761],{"class":2053},[55,11404,11405],{"class":57,"line":71},[55,11406,68],{"emptyLinePlaceholder":67},[55,11408,11409,11412,11415],{"class":57,"line":77},[55,11410,11411],{"class":2043},"export",[55,11413,11414],{"class":2043}," default",[55,11416,5778],{"class":2053},[55,11418,11419],{"class":57,"line":83},[55,11420,11421],{"class":2053},"  plugins: [\n",[55,11423,11424,11427],{"class":57,"line":89},[55,11425,11426],{"class":717},"    VitePWA",[55,11428,9602],{"class":2053},[55,11430,11431,11434,11437],{"class":57,"line":95},[55,11432,11433],{"class":2053},"      registerType: ",[55,11435,11436],{"class":721},"'autoUpdate'",[55,11438,2250],{"class":2053},[55,11440,11441],{"class":57,"line":101},[55,11442,11443],{"class":2053},"      workbox: {\n",[55,11445,11446],{"class":57,"line":107},[55,11447,11448],{"class":711},"        // Pre-cache all build output\n",[55,11450,11451,11454,11457],{"class":57,"line":113},[55,11452,11453],{"class":2053},"        globPatterns: [",[55,11455,11456],{"class":721},"'**/*.{js,css,html,ico,png,svg,woff2}'",[55,11458,5473],{"class":2053},[55,11460,11461],{"class":57,"line":119},[55,11462,68],{"emptyLinePlaceholder":67},[55,11464,11465],{"class":57,"line":125},[55,11466,11467],{"class":711},"        // Runtime caching rules\n",[55,11469,11470],{"class":57,"line":131},[55,11471,11472],{"class":2053},"        runtimeCaching: [\n",[55,11474,11475],{"class":57,"line":137},[55,11476,11477],{"class":2053},"          {\n",[55,11479,11480],{"class":57,"line":143},[55,11481,11482],{"class":711},"            // API calls: network first\n",[55,11484,11485,11488,11491,11494,11497,11500,11503,11505,11508,11511,11514,11516,11518],{"class":57,"line":149},[55,11486,11487],{"class":2053},"            urlPattern:",[55,11489,11490],{"class":721}," /",[55,11492,11493],{"class":2043},"^",[55,11495,11496],{"class":721},"https:",[55,11498,11499],{"class":7776},"\\/\\/",[55,11501,11502],{"class":721},"yourapp",[55,11504,7777],{"class":7776},[55,11506,11507],{"class":721},"com",[55,11509,11510],{"class":7776},"\\/",[55,11512,11513],{"class":721},"api",[55,11515,11510],{"class":7776},[55,11517,3857],{"class":721},[55,11519,2250],{"class":2053},[55,11521,11522,11525,11528],{"class":57,"line":441},[55,11523,11524],{"class":2053},"            handler: ",[55,11526,11527],{"class":721},"'NetworkFirst'",[55,11529,2250],{"class":2053},[55,11531,11532],{"class":57,"line":447},[55,11533,11534],{"class":2053},"            options: {\n",[55,11536,11537,11540,11543],{"class":57,"line":453},[55,11538,11539],{"class":2053},"              cacheName: ",[55,11541,11542],{"class":721},"'api-cache'",[55,11544,2250],{"class":2053},[55,11546,11547,11550,11552,11555,11557],{"class":57,"line":459},[55,11548,11549],{"class":2053},"              expiration: { maxEntries: ",[55,11551,8569],{"class":2069},[55,11553,11554],{"class":2053},", maxAgeSeconds: ",[55,11556,691],{"class":2069},[55,11558,8215],{"class":2053},[55,11560,11561,11564,11567],{"class":57,"line":464},[55,11562,11563],{"class":2053},"              networkTimeoutSeconds: ",[55,11565,11566],{"class":2069},"5",[55,11568,2250],{"class":2053},[55,11570,11571],{"class":57,"line":470},[55,11572,11573],{"class":2053},"            },\n",[55,11575,11576],{"class":57,"line":475},[55,11577,11578],{"class":2053},"          },\n",[55,11580,11581],{"class":57,"line":481},[55,11582,11477],{"class":2053},[55,11584,11585],{"class":57,"line":486},[55,11586,11587],{"class":711},"            // Images: stale while revalidate\n",[55,11589,11590,11592,11594,11596,11599,11601,11604,11606,11609,11611,11614,11616,11619,11621,11624,11626,11628],{"class":57,"line":492},[55,11591,11487],{"class":2053},[55,11593,11490],{"class":721},[55,11595,7777],{"class":7776},[55,11597,11598],{"class":721},"(?:png",[55,11600,7791],{"class":2043},[55,11602,11603],{"class":721},"jpg",[55,11605,7791],{"class":2043},[55,11607,11608],{"class":721},"jpeg",[55,11610,7791],{"class":2043},[55,11612,11613],{"class":721},"svg",[55,11615,7791],{"class":2043},[55,11617,11618],{"class":721},"gif",[55,11620,7791],{"class":2043},[55,11622,11623],{"class":721},"webp)",[55,11625,7812],{"class":2043},[55,11627,3857],{"class":721},[55,11629,2250],{"class":2053},[55,11631,11632,11634,11637],{"class":57,"line":497},[55,11633,11524],{"class":2053},[55,11635,11636],{"class":721},"'StaleWhileRevalidate'",[55,11638,2250],{"class":2053},[55,11640,11641],{"class":57,"line":503},[55,11642,11534],{"class":2053},[55,11644,11645,11647,11650],{"class":57,"line":508},[55,11646,11539],{"class":2053},[55,11648,11649],{"class":721},"'image-cache'",[55,11651,2250],{"class":2053},[55,11653,11654,11656,11659,11661,11664,11666,11669,11671,11674],{"class":57,"line":2347},[55,11655,11549],{"class":2053},[55,11657,11658],{"class":2069},"60",[55,11660,11554],{"class":2053},[55,11662,11663],{"class":2069},"30",[55,11665,11126],{"class":2043},[55,11667,11668],{"class":2069}," 24",[55,11670,11126],{"class":2043},[55,11672,11673],{"class":2069}," 3600",[55,11675,8215],{"class":2053},[55,11677,11678],{"class":57,"line":2353},[55,11679,11573],{"class":2053},[55,11681,11682],{"class":57,"line":2368},[55,11683,11578],{"class":2053},[55,11685,11686],{"class":57,"line":3094},[55,11687,11477],{"class":2053},[55,11689,11690],{"class":57,"line":3099},[55,11691,11692],{"class":711},"            // Google Fonts: cache first (they use content hashes)\n",[55,11694,11695,11697,11699,11701,11703,11705,11708,11710,11713,11715,11718,11720,11722,11724,11726],{"class":57,"line":3104},[55,11696,11487],{"class":2053},[55,11698,11490],{"class":721},[55,11700,11493],{"class":2043},[55,11702,11496],{"class":721},[55,11704,11499],{"class":7776},[55,11706,11707],{"class":721},"fonts",[55,11709,7777],{"class":7776},[55,11711,11712],{"class":721},"(googleapis",[55,11714,7791],{"class":2043},[55,11716,11717],{"class":721},"gstatic)",[55,11719,7777],{"class":7776},[55,11721,11507],{"class":721},[55,11723,11510],{"class":7776},[55,11725,3857],{"class":721},[55,11727,2250],{"class":2053},[55,11729,11730,11732,11735],{"class":57,"line":4281},[55,11731,11524],{"class":2053},[55,11733,11734],{"class":721},"'CacheFirst'",[55,11736,2250],{"class":2053},[55,11738,11739],{"class":57,"line":4287},[55,11740,11534],{"class":2053},[55,11742,11743,11745,11748],{"class":57,"line":4293},[55,11744,11539],{"class":2053},[55,11746,11747],{"class":721},"'font-cache'",[55,11749,2250],{"class":2053},[55,11751,11752,11755,11758,11760,11762,11764,11766],{"class":57,"line":4298},[55,11753,11754],{"class":2053},"              expiration: { maxAgeSeconds: ",[55,11756,11757],{"class":2069},"365",[55,11759,11126],{"class":2043},[55,11761,11668],{"class":2069},[55,11763,11126],{"class":2043},[55,11765,11673],{"class":2069},[55,11767,8215],{"class":2053},[55,11769,11770],{"class":57,"line":4304},[55,11771,11573],{"class":2053},[55,11773,11774],{"class":57,"line":4309},[55,11775,11578],{"class":2053},[55,11777,11778],{"class":57,"line":4315},[55,11779,11780],{"class":2053},"        ],\n",[55,11782,11783],{"class":57,"line":4321},[55,11784,11785],{"class":2053},"      },\n",[55,11787,11788],{"class":57,"line":4326},[55,11789,11790],{"class":2053},"      manifest: {\n",[55,11792,11793,11796,11799],{"class":57,"line":4332},[55,11794,11795],{"class":2053},"        name: ",[55,11797,11798],{"class":721},"'Clarity App'",[55,11800,2250],{"class":2053},[55,11802,11803,11806,11809],{"class":57,"line":4337},[55,11804,11805],{"class":2053},"        short_name: ",[55,11807,11808],{"class":721},"'Clarity'",[55,11810,2250],{"class":2053},[55,11812,11813,11816,11819],{"class":57,"line":4343},[55,11814,11815],{"class":2053},"        theme_color: ",[55,11817,11818],{"class":721},"'#1e3a5f'",[55,11820,2250],{"class":2053},[55,11822,11823],{"class":57,"line":4348},[55,11824,11825],{"class":2053},"        icons: [\n",[55,11827,11828,11831,11833,11836,11839,11842,11845],{"class":57,"line":4354},[55,11829,11830],{"class":2053},"          { src: ",[55,11832,10013],{"class":721},[55,11834,11835],{"class":2053},", sizes: ",[55,11837,11838],{"class":721},"'192x192'",[55,11840,11841],{"class":2053},", type: ",[55,11843,11844],{"class":721},"'image/png'",[55,11846,8215],{"class":2053},[55,11848,11849,11851,11854,11856,11859,11861,11863],{"class":57,"line":5377},[55,11850,11830],{"class":2053},[55,11852,11853],{"class":721},"'/icons/icon-512x512.png'",[55,11855,11835],{"class":2053},[55,11857,11858],{"class":721},"'512x512'",[55,11860,11841],{"class":2053},[55,11862,11844],{"class":721},[55,11864,8215],{"class":2053},[55,11866,11867],{"class":57,"line":5390},[55,11868,11780],{"class":2053},[55,11870,11871],{"class":57,"line":5403},[55,11872,11785],{"class":2053},[55,11874,11875],{"class":57,"line":5416},[55,11876,11877],{"class":2053},"    }),\n",[55,11879,11880],{"class":57,"line":5446},[55,11881,5241],{"class":2053},[55,11883,11884],{"class":57,"line":5451},[55,11885,11886],{"class":2053},"};\n",[18,11888,11889,11890,11893,11894,11897,11898,11901],{},"Workbox also provides ",[22,11891,11892],{},"expiration"," settings to limit cache size and age: ",[22,11895,11896],{},"maxEntries"," caps how many responses are stored, and ",[22,11899,11900],{},"maxAgeSeconds"," evicts responses older than the threshold. Without these limits, your app's cache can grow unbounded on users' devices.",[36,11903],{},[13,11905,11907],{"id":11906},"auditing-your-pwa-with-lighthouse","Auditing Your PWA with Lighthouse",[18,11909,11910,11911,11914],{},"Google's ",[518,11912,11913],{},"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.",[18,11916,11917],{},"Run it from Chrome DevTools > Lighthouse > Generate report.",[18,11919,11920],{},"A fully installable PWA passes these Lighthouse checks:",[46,11922,11924],{"className":48,"code":11923,"language":50,"meta":51,"style":51},"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",[22,11925,11926,11931,11936,11941,11946,11951,11956,11961,11966,11971,11976,11980,11985,11990,11995,12000],{"__ignoreMap":51},[55,11927,11928],{"class":57,"line":58},[55,11929,11930],{},"PWA Installability Checklist:\n",[55,11932,11933],{"class":57,"line":64},[55,11934,11935],{},"  [x] Served over HTTPS\n",[55,11937,11938],{"class":57,"line":71},[55,11939,11940],{},"  [x] Has a registered service worker\n",[55,11942,11943],{"class":57,"line":77},[55,11944,11945],{},"  [x] Has a web app manifest with:\n",[55,11947,11948],{"class":57,"line":83},[55,11949,11950],{},"      [x] name or short_name\n",[55,11952,11953],{"class":57,"line":89},[55,11954,11955],{},"      [x] icons (192px and 512px)\n",[55,11957,11958],{"class":57,"line":95},[55,11959,11960],{},"      [x] start_url\n",[55,11962,11963],{"class":57,"line":101},[55,11964,11965],{},"      [x] display: standalone, fullscreen, or minimal-ui\n",[55,11967,11968],{"class":57,"line":107},[55,11969,11970],{},"  [x] Service worker responds to fetch events\n",[55,11972,11973],{"class":57,"line":113},[55,11974,11975],{},"  [x] Manifest's start_url responds with 200 when offline\n",[55,11977,11978],{"class":57,"line":119},[55,11979,68],{"emptyLinePlaceholder":67},[55,11981,11982],{"class":57,"line":125},[55,11983,11984],{},"PWA Best Practices:\n",[55,11986,11987],{"class":57,"line":131},[55,11988,11989],{},"  [x] Has a \u003Cmeta name=\"theme-color\" /> tag\n",[55,11991,11992],{"class":57,"line":137},[55,11993,11994],{},"  [x] Has a \u003Cmeta name=\"viewport\" /> tag\n",[55,11996,11997],{"class":57,"line":143},[55,11998,11999],{},"  [x] Page loads on slow 3G in under 10 seconds\n",[55,12001,12002],{"class":57,"line":149},[55,12003,12004],{},"  [x] Each page has a unique URL\n",[18,12006,12007],{},"From the command line:",[46,12009,12011],{"className":702,"code":12010,"language":704,"meta":51,"style":51},"# 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",[22,12012,12013,12018,12032,12036,12041,12058,12062,12067],{"__ignoreMap":51},[55,12014,12015],{"class":57,"line":58},[55,12016,12017],{"class":711},"# Install Lighthouse CLI\n",[55,12019,12020,12023,12026,12029],{"class":57,"line":64},[55,12021,12022],{"class":717},"npm",[55,12024,12025],{"class":721}," install",[55,12027,12028],{"class":2069}," -g",[55,12030,12031],{"class":721}," lighthouse\n",[55,12033,12034],{"class":57,"line":71},[55,12035,68],{"emptyLinePlaceholder":67},[55,12037,12038],{"class":57,"line":77},[55,12039,12040],{"class":711},"# Run a PWA audit\n",[55,12042,12043,12046,12049,12052,12055],{"class":57,"line":83},[55,12044,12045],{"class":717},"lighthouse",[55,12047,12048],{"class":721}," https://yourapp.com",[55,12050,12051],{"class":2069}," --only-categories=pwa",[55,12053,12054],{"class":2069}," --output=html",[55,12056,12057],{"class":2069}," --output-path=./report.html\n",[55,12059,12060],{"class":57,"line":89},[55,12061,68],{"emptyLinePlaceholder":67},[55,12063,12064],{"class":57,"line":95},[55,12065,12066],{"class":711},"# Open the report\n",[55,12068,12069,12071],{"class":57,"line":101},[55,12070,6240],{"class":717},[55,12072,12073],{"class":721}," report.html\n",[36,12075],{},[13,12077,12079],{"id":12078},"storage-where-pwas-keep-data","Storage: Where PWAs Keep Data",[18,12081,12082],{},"Service workers have access to several storage APIs for persisting data on the device:",[186,12084,12085,12098],{},[189,12086,12087],{},[192,12088,12089,12092,12095],{},[195,12090,12091],{},"API",[195,12093,12094],{},"Best For",[195,12096,12097],{},"Accessible From",[205,12099,12100,12115,12125,12136],{},[192,12101,12102,12109,12112],{},[210,12103,12104,12105,12108],{},"Cache Storage (",[22,12106,12107],{},"caches.open()",")",[210,12110,12111],{},"Network responses (HTML, CSS, JSON)",[210,12113,12114],{},"SW + Page",[192,12116,12117,12120,12123],{},[210,12118,12119],{},"IndexedDB",[210,12121,12122],{},"Structured app data (user content, outbox queue)",[210,12124,12114],{},[192,12126,12127,12130,12133],{},[210,12128,12129],{},"localStorage",[210,12131,12132],{},"Simple key-value (small settings)",[210,12134,12135],{},"Page only, NOT SW. Synchronous.",[192,12137,12138,12141,12144],{},[210,12139,12140],{},"sessionStorage",[210,12142,12143],{},"Tab-scoped temporary state",[210,12145,12146],{},"Page only, NOT SW",[18,12148,12149,12150,12153,12154,12156,12157,12159],{},"For service worker code, you have two choices: ",[518,12151,12152],{},"Cache Storage"," for network responses and ",[518,12155,12119],{}," for application data. ",[22,12158,12129],{}," is synchronous and is not available in service workers at all.",[343,12161,12163],{"id":12162},"checking-available-storage","Checking Available Storage",[18,12165,12166],{},"The Storage Manager API tells you how much space your app is using and the browser's estimate of how much is available:",[46,12168,12170],{"className":2025,"code":12169,"language":2027,"meta":51,"style":51},"// 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",[22,12171,12172,12176,12180,12191,12215,12243,12275,12305,12330,12334],{"__ignoreMap":51},[55,12173,12174],{"class":57,"line":58},[55,12175,8780],{"class":711},[55,12177,12178],{"class":57,"line":64},[55,12179,68],{"emptyLinePlaceholder":67},[55,12181,12182,12184,12186,12189],{"class":57,"line":71},[55,12183,2044],{"class":2043},[55,12185,2047],{"class":2043},[55,12187,12188],{"class":717}," checkStorageQuota",[55,12190,5719],{"class":2053},[55,12192,12193,12195,12197,12200,12202,12205,12207,12210,12212],{"class":57,"line":77},[55,12194,5724],{"class":2043},[55,12196,2093],{"class":2053},[55,12198,12199],{"class":721},"'storage'",[55,12201,5736],{"class":2043},[55,12203,12204],{"class":2053}," navigator ",[55,12206,9273],{"class":2043},[55,12208,12209],{"class":721}," 'estimate'",[55,12211,5736],{"class":2043},[55,12213,12214],{"class":2053}," navigator.storage) {\n",[55,12216,12217,12219,12221,12224,12226,12229,12231,12233,12235,12238,12241],{"class":57,"line":83},[55,12218,2066],{"class":2043},[55,12220,3317],{"class":2053},[55,12222,12223],{"class":2069},"usage",[55,12225,525],{"class":2053},[55,12227,12228],{"class":2069},"quota",[55,12230,7646],{"class":2053},[55,12232,3475],{"class":2043},[55,12234,2158],{"class":2043},[55,12236,12237],{"class":2053}," navigator.storage.",[55,12239,12240],{"class":717},"estimate",[55,12242,7014],{"class":2053},[55,12244,12245,12247,12250,12252,12255,12257,12260,12262,12264,12266,12269,12271,12273],{"class":57,"line":89},[55,12246,2066],{"class":2043},[55,12248,12249],{"class":2069}," usedMB",[55,12251,2073],{"class":2043},[55,12253,12254],{"class":2053}," (usage ",[55,12256,3857],{"class":2043},[55,12258,12259],{"class":2069}," 1024",[55,12261,11490],{"class":2043},[55,12263,12259],{"class":2069},[55,12265,6248],{"class":2053},[55,12267,12268],{"class":717},"toFixed",[55,12270,2054],{"class":2053},[55,12272,3355],{"class":2069},[55,12274,2178],{"class":2053},[55,12276,12277,12279,12282,12284,12287,12289,12291,12293,12295,12297,12299,12301,12303],{"class":57,"line":95},[55,12278,2066],{"class":2043},[55,12280,12281],{"class":2069}," quotaMB",[55,12283,2073],{"class":2043},[55,12285,12286],{"class":2053}," (quota ",[55,12288,3857],{"class":2043},[55,12290,12259],{"class":2069},[55,12292,11490],{"class":2043},[55,12294,12259],{"class":2069},[55,12296,6248],{"class":2053},[55,12298,12268],{"class":717},[55,12300,2054],{"class":2053},[55,12302,3355],{"class":2069},[55,12304,2178],{"class":2053},[55,12306,12307,12309,12311,12313,12316,12319,12322,12325,12328],{"class":57,"line":101},[55,12308,5744],{"class":2053},[55,12310,5747],{"class":717},[55,12312,2054],{"class":2053},[55,12314,12315],{"class":721},"`Using ${",[55,12317,12318],{"class":2053},"usedMB",[55,12320,12321],{"class":721},"} MB of ${",[55,12323,12324],{"class":2053},"quotaMB",[55,12326,12327],{"class":721},"} MB`",[55,12329,2178],{"class":2053},[55,12331,12332],{"class":57,"line":107},[55,12333,5766],{"class":2053},[55,12335,12336],{"class":57,"line":113},[55,12337,2005],{"class":2053},[18,12339,12340],{},"Browsers can evict cache storage under storage pressure. To opt your app's storage into \"persistent\" mode (not evicted without user approval):",[46,12342,12344],{"className":2025,"code":12343,"language":2027,"meta":51,"style":51},"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",[22,12345,12346,12357,12369,12387,12401,12405],{"__ignoreMap":51},[55,12347,12348,12350,12352,12355],{"class":57,"line":58},[55,12349,2044],{"class":2043},[55,12351,2047],{"class":2043},[55,12353,12354],{"class":717}," requestPersistentStorage",[55,12356,5719],{"class":2053},[55,12358,12359,12361,12364,12366],{"class":57,"line":64},[55,12360,5724],{"class":2043},[55,12362,12363],{"class":2053}," (navigator.storage ",[55,12365,9273],{"class":2043},[55,12367,12368],{"class":2053}," navigator.storage.persist) {\n",[55,12370,12371,12373,12376,12378,12380,12382,12385],{"class":57,"line":71},[55,12372,2066],{"class":2043},[55,12374,12375],{"class":2069}," granted",[55,12377,2073],{"class":2043},[55,12379,2158],{"class":2043},[55,12381,12237],{"class":2053},[55,12383,12384],{"class":717},"persist",[55,12386,7014],{"class":2053},[55,12388,12389,12391,12393,12395,12398],{"class":57,"line":77},[55,12390,5744],{"class":2053},[55,12392,5747],{"class":717},[55,12394,2054],{"class":2053},[55,12396,12397],{"class":721},"'Persistent storage granted:'",[55,12399,12400],{"class":2053},", granted);\n",[55,12402,12403],{"class":57,"line":83},[55,12404,5766],{"class":2053},[55,12406,12407],{"class":57,"line":89},[55,12408,2005],{"class":2053},[36,12410],{},[13,12412,12414],{"id":12413},"common-pwa-pitfalls","Common PWA Pitfalls",[343,12416,12418],{"id":12417},"_1-caching-api-responses-that-contain-user-data","1. Caching API Responses That Contain User Data",[18,12420,12421,12422,12424],{},"Never cache responses that contain user-specific data (JWT tokens, personal data) in a shared cache. Use ",[22,12423,4538],{}," on the server for these responses, and check for it in your service worker:",[46,12426,12428],{"className":2025,"code":12427,"language":2027,"meta":51,"style":51},"// sw.js\nfunction isCacheable(response) {\n  const cacheControl = response.headers.get('Cache-Control') ?? '';\n  return !cacheControl.includes('no-store') && !cacheControl.includes('private');\n}\n",[22,12429,12430,12434,12447,12475,12508],{"__ignoreMap":51},[55,12431,12432],{"class":57,"line":58},[55,12433,5989],{"class":711},[55,12435,12436,12438,12441,12443,12445],{"class":57,"line":64},[55,12437,9734],{"class":2043},[55,12439,12440],{"class":717}," isCacheable",[55,12442,2054],{"class":2053},[55,12444,6731],{"class":2057},[55,12446,2061],{"class":2053},[55,12448,12449,12451,12454,12456,12459,12461,12463,12466,12468,12470,12473],{"class":57,"line":71},[55,12450,6668],{"class":2043},[55,12452,12453],{"class":2069}," cacheControl",[55,12455,2073],{"class":2043},[55,12457,12458],{"class":2053}," response.headers.",[55,12460,2164],{"class":717},[55,12462,2054],{"class":2053},[55,12464,12465],{"class":721},"'Cache-Control'",[55,12467,6014],{"class":2053},[55,12469,6790],{"class":2043},[55,12471,12472],{"class":721}," ''",[55,12474,5761],{"class":2053},[55,12476,12477,12479,12481,12484,12486,12488,12491,12493,12495,12497,12499,12501,12503,12506],{"class":57,"line":77},[55,12478,6784],{"class":2043},[55,12480,2105],{"class":2043},[55,12482,12483],{"class":2053},"cacheControl.",[55,12485,7175],{"class":717},[55,12487,2054],{"class":2053},[55,12489,12490],{"class":721},"'no-store'",[55,12492,6014],{"class":2053},[55,12494,9273],{"class":2043},[55,12496,2105],{"class":2043},[55,12498,12483],{"class":2053},[55,12500,7175],{"class":717},[55,12502,2054],{"class":2053},[55,12504,12505],{"class":721},"'private'",[55,12507,2178],{"class":2053},[55,12509,12510],{"class":57,"line":83},[55,12511,2005],{"class":2053},[343,12513,12515],{"id":12514},"_2-not-versioning-your-cache","2. Not Versioning Your Cache",[18,12517,12518],{},"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:",[46,12520,12522],{"className":2025,"code":12521,"language":2027,"meta":51,"style":51},"// Wrong: cache never changes\nconst CACHE = 'my-cache';\n\n// Right: version matches your deployment\nconst CACHE = 'app-shell-v14';\n",[22,12523,12524,12529,12543,12547,12552],{"__ignoreMap":51},[55,12525,12526],{"class":57,"line":58},[55,12527,12528],{"class":711},"// Wrong: cache never changes\n",[55,12530,12531,12533,12536,12538,12541],{"class":57,"line":64},[55,12532,6191],{"class":2043},[55,12534,12535],{"class":2069}," CACHE",[55,12537,2073],{"class":2043},[55,12539,12540],{"class":721}," 'my-cache'",[55,12542,5761],{"class":2053},[55,12544,12545],{"class":57,"line":71},[55,12546,68],{"emptyLinePlaceholder":67},[55,12548,12549],{"class":57,"line":77},[55,12550,12551],{"class":711},"// Right: version matches your deployment\n",[55,12553,12554,12556,12558,12560,12563],{"class":57,"line":83},[55,12555,6191],{"class":2043},[55,12557,12535],{"class":2069},[55,12559,2073],{"class":2043},[55,12561,12562],{"class":721}," 'app-shell-v14'",[55,12564,5761],{"class":2053},[343,12566,12568],{"id":12567},"_3-the-infinite-loop-trap","3. The Infinite Loop Trap",[18,12570,12571,12572,12574],{},"If your service worker fetches its own URL (e.g., a request to ",[22,12573,5957],{}," goes through the fetch handler), you can create an infinite loop. Always skip service worker requests:",[46,12576,12578],{"className":2025,"code":12577,"language":2027,"meta":51,"style":51},"// sw.js\nself.addEventListener('fetch', (event) => {\n  if (event.request.url.includes('/sw.js')) return;\n  // ... handle other requests\n});\n",[22,12579,12580,12584,12604,12623,12628],{"__ignoreMap":51},[55,12581,12582],{"class":57,"line":58},[55,12583,5989],{"class":711},[55,12585,12586,12588,12590,12592,12594,12596,12598,12600,12602],{"class":57,"line":64},[55,12587,5998],{"class":2053},[55,12589,5828],{"class":717},[55,12591,2054],{"class":2053},[55,12593,6005],{"class":721},[55,12595,6008],{"class":2053},[55,12597,6011],{"class":2057},[55,12599,6014],{"class":2053},[55,12601,5839],{"class":2043},[55,12603,5778],{"class":2053},[55,12605,12606,12608,12611,12613,12615,12617,12619,12621],{"class":57,"line":71},[55,12607,5724],{"class":2043},[55,12609,12610],{"class":2053}," (event.request.url.",[55,12612,7175],{"class":717},[55,12614,2054],{"class":2053},[55,12616,5800],{"class":721},[55,12618,9784],{"class":2053},[55,12620,6293],{"class":2043},[55,12622,5761],{"class":2053},[55,12624,12625],{"class":57,"line":77},[55,12626,12627],{"class":711},"  // ... handle other requests\n",[55,12629,12630],{"class":57,"line":83},[55,12631,6125],{"class":2053},[343,12633,12635],{"id":12634},"_4-serving-stale-html-that-references-missing-assets","4. Serving Stale HTML That References Missing Assets",[18,12637,12638,12639,12642,12643,12646,12647,12650],{},"If you cache an old ",[22,12640,12641],{},"index.html"," that references ",[22,12644,12645],{},"/app.abc123.js",", but the user's app shell cache only has ",[22,12648,12649],{},"/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.",[343,12652,12654],{"id":12653},"_5-not-handling-the-update-flow","5. Not Handling the Update Flow",[18,12656,12657],{},"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:",[46,12659,12661],{"className":2025,"code":12660,"language":2027,"meta":51,"style":51},"// 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",[22,12662,12663,12667,12671,12691,12695,12712,12722,12726,12744,12761,12766,12777,12792,12801,12805,12809,12813],{"__ignoreMap":51},[55,12664,12665],{"class":57,"line":58},[55,12666,8780],{"class":711},[55,12668,12669],{"class":57,"line":64},[55,12670,68],{"emptyLinePlaceholder":67},[55,12672,12673,12675,12677,12679,12681,12683,12685,12687,12689],{"class":57,"line":71},[55,12674,6191],{"class":2043},[55,12676,5785],{"class":2069},[55,12678,2073],{"class":2043},[55,12680,2158],{"class":2043},[55,12682,5792],{"class":2053},[55,12684,5795],{"class":717},[55,12686,2054],{"class":2053},[55,12688,5800],{"class":721},[55,12690,2178],{"class":2053},[55,12692,12693],{"class":57,"line":77},[55,12694,68],{"emptyLinePlaceholder":67},[55,12696,12697,12700,12702,12704,12706,12708,12710],{"class":57,"line":83},[55,12698,12699],{"class":2053},"registration.",[55,12701,5828],{"class":717},[55,12703,2054],{"class":2053},[55,12705,5833],{"class":721},[55,12707,5836],{"class":2053},[55,12709,5839],{"class":2043},[55,12711,5778],{"class":2053},[55,12713,12714,12716,12718,12720],{"class":57,"line":89},[55,12715,6668],{"class":2043},[55,12717,5849],{"class":2069},[55,12719,2073],{"class":2043},[55,12721,5854],{"class":2053},[55,12723,12724],{"class":57,"line":95},[55,12725,68],{"emptyLinePlaceholder":67},[55,12727,12728,12731,12733,12735,12738,12740,12742],{"class":57,"line":101},[55,12729,12730],{"class":2053},"  newWorker.",[55,12732,5828],{"class":717},[55,12734,2054],{"class":2053},[55,12736,12737],{"class":721},"'statechange'",[55,12739,5836],{"class":2053},[55,12741,5839],{"class":2043},[55,12743,5778],{"class":2053},[55,12745,12746,12748,12751,12753,12755,12758],{"class":57,"line":107},[55,12747,2090],{"class":2043},[55,12749,12750],{"class":2053}," (newWorker.state ",[55,12752,6604],{"class":2043},[55,12754,9021],{"class":721},[55,12756,12757],{"class":2043}," &&",[55,12759,12760],{"class":2053}," navigator.serviceWorker.controller) {\n",[55,12762,12763],{"class":57,"line":113},[55,12764,12765],{"class":711},"      // A new version is waiting: show an update banner\n",[55,12767,12768,12771,12773,12775],{"class":57,"line":119},[55,12769,12770],{"class":717},"      showUpdateBanner",[55,12772,8346],{"class":2053},[55,12774,5839],{"class":2043},[55,12776,5778],{"class":2053},[55,12778,12779,12782,12785,12787,12790],{"class":57,"line":125},[55,12780,12781],{"class":2053},"        newWorker.",[55,12783,12784],{"class":717},"postMessage",[55,12786,10240],{"class":2053},[55,12788,12789],{"class":721},"'SKIP_WAITING'",[55,12791,2133],{"class":2053},[55,12793,12794,12797,12799],{"class":57,"line":131},[55,12795,12796],{"class":2053},"        window.location.",[55,12798,8722],{"class":717},[55,12800,7014],{"class":2053},[55,12802,12803],{"class":57,"line":137},[55,12804,10928],{"class":2053},[55,12806,12807],{"class":57,"line":143},[55,12808,1962],{"class":2053},[55,12810,12811],{"class":57,"line":149},[55,12812,6770],{"class":2053},[55,12814,12815],{"class":57,"line":441},[55,12816,6125],{"class":2053},[46,12818,12820],{"className":2025,"code":12819,"language":2027,"meta":51,"style":51},"// sw.js\nself.addEventListener('message', (event) => {\n  if (event.data?.type === 'SKIP_WAITING') {\n    self.skipWaiting();\n  }\n});\n",[22,12821,12822,12826,12847,12861,12870,12874],{"__ignoreMap":51},[55,12823,12824],{"class":57,"line":58},[55,12825,5989],{"class":711},[55,12827,12828,12830,12832,12834,12837,12839,12841,12843,12845],{"class":57,"line":64},[55,12829,5998],{"class":2053},[55,12831,5828],{"class":717},[55,12833,2054],{"class":2053},[55,12835,12836],{"class":721},"'message'",[55,12838,6008],{"class":2053},[55,12840,6011],{"class":2057},[55,12842,6014],{"class":2053},[55,12844,5839],{"class":2043},[55,12846,5778],{"class":2053},[55,12848,12849,12851,12854,12856,12859],{"class":57,"line":71},[55,12850,5724],{"class":2043},[55,12852,12853],{"class":2053}," (event.data?.type ",[55,12855,6604],{"class":2043},[55,12857,12858],{"class":721}," 'SKIP_WAITING'",[55,12860,2061],{"class":2053},[55,12862,12863,12866,12868],{"class":57,"line":77},[55,12864,12865],{"class":2053},"    self.",[55,12867,7011],{"class":717},[55,12869,7014],{"class":2053},[55,12871,12872],{"class":57,"line":83},[55,12873,5766],{"class":2053},[55,12875,12876],{"class":57,"line":89},[55,12877,6125],{"class":2053},[36,12879],{},[13,12881,12883],{"id":12882},"pwa-capabilities-by-platform","PWA Capabilities by Platform",[18,12885,12886],{},"Browser and OS support for PWA APIs is not uniform. Here is the current state:",[46,12888,12890],{"className":48,"code":12889,"language":50,"meta":51,"style":51},"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",[22,12891,12892,12897,12902,12907,12912,12917,12922,12927,12932,12937,12942,12947,12952,12957,12961],{"__ignoreMap":51},[55,12893,12894],{"class":57,"line":58},[55,12895,12896],{},"Feature                     Chrome  Firefox  Safari  Edge\n",[55,12898,12899],{"class":57,"line":64},[55,12900,12901],{},"                            (Android/Desktop)       (iOS 16.4+)\n",[55,12903,12904],{"class":57,"line":71},[55,12905,12906],{},"---------------------------------------------------------------\n",[55,12908,12909],{"class":57,"line":77},[55,12910,12911],{},"Service Workers             Yes     Yes      Yes     Yes\n",[55,12913,12914],{"class":57,"line":83},[55,12915,12916],{},"Cache API                   Yes     Yes      Yes     Yes\n",[55,12918,12919],{"class":57,"line":89},[55,12920,12921],{},"Web App Manifest            Yes     Yes      Yes     Yes\n",[55,12923,12924],{"class":57,"line":95},[55,12925,12926],{},"Install prompt              Yes     No       No      Yes\n",[55,12928,12929],{"class":57,"line":101},[55,12930,12931],{},"Push Notifications          Yes     Yes      Yes*    Yes\n",[55,12933,12934],{"class":57,"line":107},[55,12935,12936],{},"Background Sync             Yes     No       No      Yes\n",[55,12938,12939],{"class":57,"line":113},[55,12940,12941],{},"Periodic Background Sync    Yes     No       No      Yes\n",[55,12943,12944],{"class":57,"line":119},[55,12945,12946],{},"Badging API                 Yes     No       No      Yes\n",[55,12948,12949],{"class":57,"line":125},[55,12950,12951],{},"File System Access API      Yes     No       No      Yes\n",[55,12953,12954],{"class":57,"line":131},[55,12955,12956],{},"Web Share API               Yes     No       Yes     Yes\n",[55,12958,12959],{"class":57,"line":137},[55,12960,68],{"emptyLinePlaceholder":67},[55,12962,12963],{"class":57,"line":143},[55,12964,12965],{},"* iOS Push Notifications require iOS 16.4+ and the app must be installed\n",[18,12967,12968],{},"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.",[18,12970,12971],{},"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.",[36,12973],{},[13,12975,12977],{"id":12976},"summary-building-a-pwa-checklist","Summary: Building a PWA Checklist",[18,12979,12980],{},"Use this checklist when shipping a PWA to production:",[46,12982,12984],{"className":48,"code":12983,"language":50,"meta":51,"style":51},"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",[22,12985,12986,12991,12996,13001,13006,13011,13015,13020,13025,13030,13035,13040,13045,13050,13054,13059,13064,13069,13074,13079,13084,13089,13093,13098,13103,13108,13113,13118,13122,13127,13132,13137,13142,13147,13152,13156,13161,13166,13171,13176],{"__ignoreMap":51},[55,12987,12988],{"class":57,"line":58},[55,12989,12990],{},"FOUNDATION\n",[55,12992,12993],{"class":57,"line":64},[55,12994,12995],{},"  [ ] Site is served over HTTPS\n",[55,12997,12998],{"class":57,"line":71},[55,12999,13000],{},"  [ ] manifest.json is linked in every HTML page\n",[55,13002,13003],{"class":57,"line":77},[55,13004,13005],{},"  [ ] Service worker is registered\n",[55,13007,13008],{"class":57,"line":83},[55,13009,13010],{},"  [ ] An /offline.html fallback page exists\n",[55,13012,13013],{"class":57,"line":89},[55,13014,68],{"emptyLinePlaceholder":67},[55,13016,13017],{"class":57,"line":95},[55,13018,13019],{},"MANIFEST\n",[55,13021,13022],{"class":57,"line":101},[55,13023,13024],{},"  [ ] name and short_name set\n",[55,13026,13027],{"class":57,"line":107},[55,13028,13029],{},"  [ ] start_url includes analytics parameter (?source=pwa)\n",[55,13031,13032],{"class":57,"line":113},[55,13033,13034],{},"  [ ] display: \"standalone\"\n",[55,13036,13037],{"class":57,"line":119},[55,13038,13039],{},"  [ ] theme_color and background_color set\n",[55,13041,13042],{"class":57,"line":125},[55,13043,13044],{},"  [ ] 192x192 and 512x512 icons provided\n",[55,13046,13047],{"class":57,"line":131},[55,13048,13049],{},"  [ ] Maskable icon variants provided\n",[55,13051,13052],{"class":57,"line":137},[55,13053,68],{"emptyLinePlaceholder":67},[55,13055,13056],{"class":57,"line":143},[55,13057,13058],{},"SERVICE WORKER\n",[55,13060,13061],{"class":57,"line":149},[55,13062,13063],{},"  [ ] App shell pre-cached at install time\n",[55,13065,13066],{"class":57,"line":441},[55,13067,13068],{},"  [ ] Cache versioned and old caches cleaned up on activate\n",[55,13070,13071],{"class":57,"line":447},[55,13072,13073],{},"  [ ] Caching strategy chosen per resource type\n",[55,13075,13076],{"class":57,"line":453},[55,13077,13078],{},"  [ ] POST requests and cross-origin requests skipped\n",[55,13080,13081],{"class":57,"line":459},[55,13082,13083],{},"  [ ] Cache size limited with expiration rules\n",[55,13085,13086],{"class":57,"line":464},[55,13087,13088],{},"  [ ] Update flow handled (update banner + skipWaiting)\n",[55,13090,13091],{"class":57,"line":470},[55,13092,68],{"emptyLinePlaceholder":67},[55,13094,13095],{"class":57,"line":475},[55,13096,13097],{},"INSTALLATION\n",[55,13099,13100],{"class":57,"line":481},[55,13101,13102],{},"  [ ] beforeinstallprompt captured and deferred\n",[55,13104,13105],{"class":57,"line":486},[55,13106,13107],{},"  [ ] Install UI shown at appropriate moment\n",[55,13109,13110],{"class":57,"line":492},[55,13111,13112],{},"  [ ] iOS \"Add to Home Screen\" instructions shown for Safari users\n",[55,13114,13115],{"class":57,"line":497},[55,13116,13117],{},"  [ ] appinstalled event tracked in analytics\n",[55,13119,13120],{"class":57,"line":503},[55,13121,68],{"emptyLinePlaceholder":67},[55,13123,13124],{"class":57,"line":508},[55,13125,13126],{},"PUSH NOTIFICATIONS (if using)\n",[55,13128,13129],{"class":57,"line":2347},[55,13130,13131],{},"  [ ] VAPID keys generated\n",[55,13133,13134],{"class":57,"line":2353},[55,13135,13136],{},"  [ ] Permission requested after user action\n",[55,13138,13139],{"class":57,"line":2368},[55,13140,13141],{},"  [ ] Subscription sent to server\n",[55,13143,13144],{"class":57,"line":3094},[55,13145,13146],{},"  [ ] Notification click opens the right URL\n",[55,13148,13149],{"class":57,"line":3099},[55,13150,13151],{},"  [ ] Subscription expiry (410) handled on server\n",[55,13153,13154],{"class":57,"line":3104},[55,13155,68],{"emptyLinePlaceholder":67},[55,13157,13158],{"class":57,"line":4281},[55,13159,13160],{},"AUDITING\n",[55,13162,13163],{"class":57,"line":4287},[55,13164,13165],{},"  [ ] Lighthouse PWA audit passes\n",[55,13167,13168],{"class":57,"line":4293},[55,13169,13170],{},"  [ ] Tested offline in DevTools Network panel\n",[55,13172,13173],{"class":57,"line":4298},[55,13174,13175],{},"  [ ] Tested install flow on Android Chrome\n",[55,13177,13178],{"class":57,"line":4304},[55,13179,13180],{},"  [ ] Tested on iOS Safari (16.4+)\n",[36,13182],{},[13,13184,13186],{"id":13185},"what-to-explore-next","What to Explore Next",[18,13188,13189],{},"PWAs open the door to a broader set of browser capabilities that are evolving rapidly:",[1396,13191,13192,13198,13204,13210,13216,13222],{},[1399,13193,13194,13197],{},[518,13195,13196],{},"Web Share API:"," let users share content to other apps natively",[1399,13199,13200,13203],{},[518,13201,13202],{},"Web Bluetooth and Web USB:"," connect to hardware devices from the browser",[1399,13205,13206,13209],{},[518,13207,13208],{},"File System Access API:"," read and write files on the user's local filesystem",[1399,13211,13212,13215],{},[518,13213,13214],{},"Screen Wake Lock API:"," prevent the screen from sleeping during active use (maps, recipes, workouts)",[1399,13217,13218,13221],{},[518,13219,13220],{},"Badging API:"," show a notification count on the app icon in the taskbar",[1399,13223,13224,13227],{},[518,13225,13226],{},"Project Fugu:"," Google's initiative to close the remaining gap between web and native capabilities",[18,13229,13230],{},"The web platform is closing the gap with native faster than ever. PWAs are no longer a compromise, they are a genuinely competitive choice.",[18,13232,4723],{},[4725,13234,13235],{},"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":51,"searchDepth":64,"depth":64,"links":13237},[13238,13239,13240,13241,13246,13252,13261,13262,13268,13274,13278,13279,13282,13283,13286,13293,13294,13295],{"id":4817,"depth":64,"text":4818},{"id":4846,"depth":64,"text":4847},{"id":4898,"depth":64,"text":4899},{"id":4922,"depth":64,"text":4923,"children":13242},[13243,13244,13245],{"id":4932,"depth":71,"text":4933},{"id":5526,"depth":71,"text":5527},{"id":5608,"depth":71,"text":5609},{"id":5626,"depth":64,"text":5627,"children":13247},[13248,13249,13250,13251],{"id":5637,"depth":71,"text":5638},{"id":5670,"depth":71,"text":5671},{"id":5689,"depth":71,"text":5690},{"id":5971,"depth":71,"text":5972},{"id":6133,"depth":64,"text":6134,"children":13253},[13254,13255,13256,13257,13258,13259,13260],{"id":6140,"depth":71,"text":6141},{"id":6176,"depth":71,"text":6177},{"id":6382,"depth":71,"text":6383},{"id":6635,"depth":71,"text":6636},{"id":6800,"depth":71,"text":6801},{"id":7031,"depth":71,"text":7032},{"id":7273,"depth":71,"text":7274},{"id":8385,"depth":64,"text":8386},{"id":8753,"depth":64,"text":8754,"children":13263},[13264,13266,13267],{"id":8760,"depth":71,"text":13265},"The beforeinstallprompt Event",{"id":9117,"depth":71,"text":9118},{"id":9196,"depth":71,"text":9197},{"id":9299,"depth":64,"text":9300,"children":13269},[13270,13271,13272,13273],{"id":9306,"depth":71,"text":9307},{"id":9538,"depth":71,"text":9539},{"id":9908,"depth":71,"text":9909},{"id":10365,"depth":71,"text":10366},{"id":10615,"depth":64,"text":10616,"children":13275},[13276,13277],{"id":10625,"depth":71,"text":10626},{"id":10736,"depth":71,"text":10737},{"id":11003,"depth":64,"text":11004},{"id":11366,"depth":64,"text":11367,"children":13280},[13281],{"id":11377,"depth":71,"text":11378},{"id":11906,"depth":64,"text":11907},{"id":12078,"depth":64,"text":12079,"children":13284},[13285],{"id":12162,"depth":71,"text":12163},{"id":12413,"depth":64,"text":12414,"children":13287},[13288,13289,13290,13291,13292],{"id":12417,"depth":71,"text":12418},{"id":12514,"depth":71,"text":12515},{"id":12567,"depth":71,"text":12568},{"id":12634,"depth":71,"text":12635},{"id":12653,"depth":71,"text":12654},{"id":12882,"depth":64,"text":12883},{"id":12976,"depth":64,"text":12977},{"id":13185,"depth":64,"text":13186},"/blog-covers/progressive-web-apps-deep-dive.png","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.",{},"/posts/progressive-web-apps-deep-dive","35 min read",{"title":4812,"description":13297},{"loc":13299,"lastmod":4792},"posts/progressive-web-apps-deep-dive",[13305,13306,13307,13308,13309,9299,4805,13310],"pwa","service-workers","web-app-manifest","caching","offline","frontend","wKwfY1rblz6ng_DOptTJ0yxLluRw95X3kXsZZaE-FQI",{"id":13313,"title":13314,"author":13315,"body":13316,"category":14258,"cover":14259,"date":14260,"description":14261,"extension":4794,"featured":67,"meta":14262,"navigation":67,"path":14263,"published":67,"readingTime":14264,"seo":14265,"sitemap":14266,"stem":14267,"tags":14268,"updated":14260,"__hash__":14275},"posts/posts/claude-opus-4-7-launch-overview.md","Claude Opus 4.7 Is Here: What's New in Anthropic's April 2026 Release","Manash Sonowal",{"type":10,"value":13317,"toc":14247},[13318,13325,13328,13332,13343,13401,13404,13408,13411,13417,13423,13426,13440,13447,13451,13454,13468,13471,13491,13494,13498,13504,13507,13518,13522,13525,13528,13532,13542,13545,13572,13575,13592,13595,13599,13605,14172,14175,14190,14193,14197,14200,14232,14235,14239,14242,14244],[18,13319,13320,13321,13324],{},"Anthropic shipped ",[518,13322,13323],{},"Claude Opus 4.7"," today, April 16, 2026. It's a point release on paper — the pricing didn't move, the API shape didn't move, the context window didn't move — but the numbers under the hood did. Coding scores jumped, vision got a real capacity upgrade, and tool-use reliability took a noticeable step forward.",[18,13326,13327],{},"If you're already shipping on Opus 4.6, this is the kind of release where you change one string in your config and probably notice it in production the same afternoon. Let's walk through what actually changed.",[13,13329,13331],{"id":13330},"the-headline-numbers","The headline numbers",[18,13333,13334,13335,13342],{},"Straight from Anthropic's ",[13336,13337,13341],"a",{"href":13338,"rel":13339},"https://www.anthropic.com/claude/opus",[13340],"nofollow","Opus product page"," and the launch materials:",[1396,13344,13345,13351,13357,13363,13369,13378,13388,13394],{},[1399,13346,13347,13350],{},[518,13348,13349],{},"1M-token context window",", hybrid reasoning with adaptive thinking",[1399,13352,13353,13356],{},[518,13354,13355],{},"+13%"," lift over Opus 4.6 on a 93-task internal coding benchmark",[1399,13358,13359,13362],{},[518,13360,13361],{},"70% → from 58%"," on CursorBench (real repo-scale coding tasks)",[1399,13364,13365,13368],{},[518,13366,13367],{},"~3×"," more production tasks resolved end-to-end vs 4.6",[1399,13370,13371,13374,13375],{},[518,13372,13373],{},"+14%"," on multi-step agentic workflows, with ",[518,13376,13377],{},"~2/3 fewer tool-use errors",[1399,13379,13380,13383,13384,13387],{},[518,13381,13382],{},"98.5%"," on visual-acuity benchmarks; image inputs up to ",[518,13385,13386],{},"2,576 px"," on the long edge (over 3× prior Claude models)",[1399,13389,13390,13393],{},[518,13391,13392],{},"0.715"," on a research-agent efficiency benchmark",[1399,13395,13396,13397,13400],{},"Pricing ",[518,13398,13399],{},"unchanged",": $5 / M input, $25 / M output",[18,13402,13403],{},"The pattern here isn't \"a new paradigm\" — it's \"the same model family, measurably less likely to mess up.\" That's often the more useful kind of release.",[13,13405,13407],{"id":13406},"smarter-coding","Smarter coding",[18,13409,13410],{},"The coding story is the loudest part of this launch. Two numbers to pay attention to:",[18,13412,13413,13416],{},[518,13414,13415],{},"CursorBench went from 58% to 70%."," CursorBench is a repo-scale evaluation — not toy LeetCode, not isolated functions, but realistic multi-file edits where the model has to reason about a codebase it didn't write. A 12-point jump at that level is the difference between a model that sometimes needs hand-holding and one that can be trusted with a ticket.",[18,13418,13419,13422],{},[518,13420,13421],{},"Anthropic reports ~3× more production tasks resolved end-to-end."," That framing matters. \"Resolved end-to-end\" means the output was good enough to ship, not just good enough to compile. If you've ever prompted a model, skimmed the diff, and quietly rewritten half of it — this is the metric targeting that experience.",[18,13424,13425],{},"What this means day-to-day:",[1396,13427,13428,13431,13437],{},[1399,13429,13430],{},"Larger refactors land in fewer turns.",[1399,13432,13433,13436],{},[22,13434,13435],{},"plan → edit → run tests → fix"," loops are shorter because tool errors are rarer (more on that below).",[1399,13438,13439],{},"You can hand it more of a ticket and less of a scaffold.",[18,13441,13442,13443,13446],{},"If you're using Claude Code, the Claude API with tool use, or any third-party agent harness, changing the model string to ",[22,13444,13445],{},"claude-opus-4-7"," is the whole upgrade path.",[13,13448,13450],{"id":13449},"vision-upgrade","Vision upgrade",[18,13452,13453],{},"The multimodal story is the sleeper feature. Two things changed:",[2527,13455,13456,13462],{},[1399,13457,13458,13461],{},[518,13459,13460],{},"Image resolution ceiling is now 2,576 px on the long edge",", which is more than 3× what prior Claude models accepted before downscaling. You can send screenshots, PDFs, and diagrams without the model losing small text or fine-grained lines.",[1399,13463,13464,13467],{},[518,13465,13466],{},"98.5% on visual-acuity benchmarks",", with Anthropic specifically calling out gains on chemical structures and technical diagrams.",[18,13469,13470],{},"Concretely, this unlocks:",[1396,13472,13473,13479,13485],{},[1399,13474,13475,13478],{},[518,13476,13477],{},"UI screenshots",": reading a full 1440p design mock without cropping or tiling.",[1399,13480,13481,13484],{},[518,13482,13483],{},"OCR on documents",": invoices and forms with small-font footnotes where 4.6 would quietly hallucinate.",[1399,13486,13487,13490],{},[518,13488,13489],{},"Diagram understanding",": architecture drawings, ER diagrams, math on a whiteboard — the \"explain this picture to me\" cases that used to feel 80% reliable now feel closer to 95%.",[18,13492,13493],{},"If you previously built a preprocessing pipeline to split images before sending them to Claude, you can likely rip most of it out.",[13,13495,13497],{"id":13496},"agentic-reliability","Agentic reliability",[18,13499,13500,13501,532],{},"The number that jumps out to anyone who builds agent loops: ",[518,13502,13503],{},"tool errors down by about two-thirds",[18,13505,13506],{},"If you've shipped an agent on top of 4.6, you know the failure mode — the model invents a function name, or fills a required argument with the wrong type, or calls the right tool with swapped parameters. Each of those is a retry or a hard failure, and retries compound fast in multi-step workflows.",[18,13508,13509,13510,13513,13514,13517],{},"Cutting that failure rate by 2/3, combined with a ",[518,13511,13512],{},"+14% lift on multi-step workflows"," and a ",[518,13515,13516],{},"0.715 research-agent score"," (Anthropic calls this their strongest efficiency baseline yet), means agent runs finish more often and with fewer wasted tokens. For anyone paying a bill per tool-use round-trip, that's money back.",[13,13519,13521],{"id":13520},"hybrid-reasoning-and-adaptive-thinking","Hybrid reasoning and adaptive thinking",[18,13523,13524],{},"Opus 4.7 keeps the hybrid reasoning mode from 4.6 — one knob, two behaviors. On easy requests it answers fast. On hard requests it spends more compute \"thinking\" before it speaks, and that budget is adaptive rather than a flat toggle.",[18,13526,13527],{},"The practical implication: you don't need to hand-pick between \"fast\" and \"reasoning\" modes per request. Send it the problem and let it decide. If you want a hard cap on latency or cost, you can still set one — but the default behavior tends to land in a sensible place.",[13,13529,13531],{"id":13530},"pricing-and-where-you-can-run-it","Pricing and where you can run it",[18,13533,13534,13535,13537,13538,13541],{},"Pricing is ",[518,13536,13399],{}," from Opus 4.6: ",[518,13539,13540],{},"$5 per million input tokens, $25 per million output tokens",". That's the detail most teams care about because it turns the upgrade decision into a no-brainer.",[18,13543,13544],{},"Cost levers to lean on:",[1396,13546,13547,13557,13566],{},[1399,13548,13549,13552,13553,13556],{},[518,13550,13551],{},"Prompt caching",": up to ",[518,13554,13555],{},"90%"," savings on repeated context. Long system prompts, large file reads, retrieval-augmented prompts — all good candidates.",[1399,13558,13559,3323,13562,13565],{},[518,13560,13561],{},"Batch processing",[518,13563,13564],{},"50%"," off for non-interactive jobs.",[1399,13567,13568,13571],{},[518,13569,13570],{},"US-only inference",": available at 1.1× for teams with data-residency requirements.",[18,13573,13574],{},"Availability on day one:",[1396,13576,13577,13580,13583,13586,13589],{},[1399,13578,13579],{},"Claude Pro, Max, Team, and Enterprise plans",[1399,13581,13582],{},"Claude Platform API",[1399,13584,13585],{},"Amazon Bedrock",[1399,13587,13588],{},"Google Cloud Vertex AI",[1399,13590,13591],{},"Microsoft Foundry",[18,13593,13594],{},"So whichever cloud you're already on, you can get to 4.7 without leaving it.",[13,13596,13598],{"id":13597},"getting-started","Getting started",[18,13600,13601,13602,13604],{},"The model ID is ",[22,13603,13445],{},". Here's a minimal example with prompt caching enabled — the shape you'd want for anything with a non-trivial system prompt. Pick your language:",[13606,13607,13610,13933],"code-tabs",{"ariaLabel":13608,"default":13609},"Claude Opus 4.7 request example","php",[13611,13612,13613],"template",{"v-slot:php":51},[46,13614,13617],{"className":13615,"code":13616,"language":13609,"meta":51,"style":51},"language-php shiki shiki-themes github-light","use Illuminate\\Support\\Facades\\Http;\n\n$response = Http::withHeaders([\n    'x-api-key' => env('ANTHROPIC_API_KEY'),\n    'anthropic-version' => '2023-06-01',\n    'content-type' => 'application/json',\n])->post('https://api.anthropic.com/v1/messages', [\n    'model' => 'claude-opus-4-7',\n    'max_tokens' => 1024,\n    'system' => [\n        [\n            'type' => 'text',\n            'text' => 'You are a senior backend engineer reviewing code for a '\n                .'production PostgreSQL-backed Laravel service. Prioritize '\n                .'correctness, then security, then performance.',\n            // Cache this instruction block — it rarely changes between calls.\n            'cache_control' => ['type' => 'ephemeral'],\n        ],\n    ],\n    'messages' => [\n        [\n            'role' => 'user',\n            'content' => 'Review the diff in the attached PR and flag any N+1 queries.',\n        ],\n    ],\n])->json();\n\necho $response['content'][0]['text'], PHP_EOL;\necho 'cached input tokens: ', $response['usage']['cache_read_input_tokens'], PHP_EOL;\n",[22,13618,13619,13629,13633,13652,13670,13682,13694,13713,13725,13736,13745,13750,13762,13772,13780,13789,13794,13813,13817,13821,13830,13834,13846,13858,13862,13866,13876,13880,13909],{"__ignoreMap":51},[55,13620,13621,13624,13627],{"class":57,"line":58},[55,13622,13623],{"class":2043},"use",[55,13625,13626],{"class":2069}," Illuminate\\Support\\Facades\\Http",[55,13628,5761],{"class":2053},[55,13630,13631],{"class":57,"line":64},[55,13632,68],{"emptyLinePlaceholder":67},[55,13634,13635,13638,13640,13643,13646,13649],{"class":57,"line":71},[55,13636,13637],{"class":2053},"$response ",[55,13639,3475],{"class":2043},[55,13641,13642],{"class":2069}," Http",[55,13644,13645],{"class":2043},"::",[55,13647,13648],{"class":717},"withHeaders",[55,13650,13651],{"class":2053},"([\n",[55,13653,13654,13657,13660,13663,13665,13668],{"class":57,"line":77},[55,13655,13656],{"class":721},"    'x-api-key'",[55,13658,13659],{"class":2043}," =>",[55,13661,13662],{"class":717}," env",[55,13664,2054],{"class":2053},[55,13666,13667],{"class":721},"'ANTHROPIC_API_KEY'",[55,13669,9631],{"class":2053},[55,13671,13672,13675,13677,13680],{"class":57,"line":83},[55,13673,13674],{"class":721},"    'anthropic-version'",[55,13676,13659],{"class":2043},[55,13678,13679],{"class":721}," '2023-06-01'",[55,13681,2250],{"class":2053},[55,13683,13684,13687,13689,13692],{"class":57,"line":89},[55,13685,13686],{"class":721},"    'content-type'",[55,13688,13659],{"class":2043},[55,13690,13691],{"class":721}," 'application/json'",[55,13693,2250],{"class":2053},[55,13695,13696,13699,13702,13705,13707,13710],{"class":57,"line":95},[55,13697,13698],{"class":2053},"])",[55,13700,13701],{"class":2043},"->",[55,13703,13704],{"class":717},"post",[55,13706,2054],{"class":2053},[55,13708,13709],{"class":721},"'https://api.anthropic.com/v1/messages'",[55,13711,13712],{"class":2053},", [\n",[55,13714,13715,13718,13720,13723],{"class":57,"line":101},[55,13716,13717],{"class":721},"    'model'",[55,13719,13659],{"class":2043},[55,13721,13722],{"class":721}," 'claude-opus-4-7'",[55,13724,2250],{"class":2053},[55,13726,13727,13730,13732,13734],{"class":57,"line":107},[55,13728,13729],{"class":721},"    'max_tokens'",[55,13731,13659],{"class":2043},[55,13733,12259],{"class":2069},[55,13735,2250],{"class":2053},[55,13737,13738,13741,13743],{"class":57,"line":113},[55,13739,13740],{"class":721},"    'system'",[55,13742,13659],{"class":2043},[55,13744,6851],{"class":2053},[55,13746,13747],{"class":57,"line":119},[55,13748,13749],{"class":2053},"        [\n",[55,13751,13752,13755,13757,13760],{"class":57,"line":125},[55,13753,13754],{"class":721},"            'type'",[55,13756,13659],{"class":2043},[55,13758,13759],{"class":721}," 'text'",[55,13761,2250],{"class":2053},[55,13763,13764,13767,13769],{"class":57,"line":131},[55,13765,13766],{"class":721},"            'text'",[55,13768,13659],{"class":2043},[55,13770,13771],{"class":721}," 'You are a senior backend engineer reviewing code for a '\n",[55,13773,13774,13777],{"class":57,"line":137},[55,13775,13776],{"class":2043},"                .",[55,13778,13779],{"class":721},"'production PostgreSQL-backed Laravel service. Prioritize '\n",[55,13781,13782,13784,13787],{"class":57,"line":143},[55,13783,13776],{"class":2043},[55,13785,13786],{"class":721},"'correctness, then security, then performance.'",[55,13788,2250],{"class":2053},[55,13790,13791],{"class":57,"line":149},[55,13792,13793],{"class":711},"            // Cache this instruction block — it rarely changes between calls.\n",[55,13795,13796,13799,13801,13803,13806,13808,13811],{"class":57,"line":441},[55,13797,13798],{"class":721},"            'cache_control'",[55,13800,13659],{"class":2043},[55,13802,7064],{"class":2053},[55,13804,13805],{"class":721},"'type'",[55,13807,13659],{"class":2043},[55,13809,13810],{"class":721}," 'ephemeral'",[55,13812,5473],{"class":2053},[55,13814,13815],{"class":57,"line":447},[55,13816,11780],{"class":2053},[55,13818,13819],{"class":57,"line":453},[55,13820,10075],{"class":2053},[55,13822,13823,13826,13828],{"class":57,"line":459},[55,13824,13825],{"class":721},"    'messages'",[55,13827,13659],{"class":2043},[55,13829,6851],{"class":2053},[55,13831,13832],{"class":57,"line":464},[55,13833,13749],{"class":2053},[55,13835,13836,13839,13841,13844],{"class":57,"line":470},[55,13837,13838],{"class":721},"            'role'",[55,13840,13659],{"class":2043},[55,13842,13843],{"class":721}," 'user'",[55,13845,2250],{"class":2053},[55,13847,13848,13851,13853,13856],{"class":57,"line":475},[55,13849,13850],{"class":721},"            'content'",[55,13852,13659],{"class":2043},[55,13854,13855],{"class":721}," 'Review the diff in the attached PR and flag any N+1 queries.'",[55,13857,2250],{"class":2053},[55,13859,13860],{"class":57,"line":481},[55,13861,11780],{"class":2053},[55,13863,13864],{"class":57,"line":486},[55,13865,10075],{"class":2053},[55,13867,13868,13870,13872,13874],{"class":57,"line":492},[55,13869,13698],{"class":2053},[55,13871,13701],{"class":2043},[55,13873,4938],{"class":717},[55,13875,7014],{"class":2053},[55,13877,13878],{"class":57,"line":497},[55,13879,68],{"emptyLinePlaceholder":67},[55,13881,13882,13885,13888,13891,13894,13896,13898,13901,13904,13907],{"class":57,"line":503},[55,13883,13884],{"class":2069},"echo",[55,13886,13887],{"class":2053}," $response[",[55,13889,13890],{"class":721},"'content'",[55,13892,13893],{"class":2053},"][",[55,13895,8584],{"class":2069},[55,13897,13893],{"class":2053},[55,13899,13900],{"class":721},"'text'",[55,13902,13903],{"class":2053},"], ",[55,13905,13906],{"class":2069},"PHP_EOL",[55,13908,5761],{"class":2053},[55,13910,13911,13913,13916,13919,13922,13924,13927,13929,13931],{"class":57,"line":508},[55,13912,13884],{"class":2069},[55,13914,13915],{"class":721}," 'cached input tokens: '",[55,13917,13918],{"class":2053},", $response[",[55,13920,13921],{"class":721},"'usage'",[55,13923,13893],{"class":2053},[55,13925,13926],{"class":721},"'cache_read_input_tokens'",[55,13928,13903],{"class":2053},[55,13930,13906],{"class":2069},[55,13932,5761],{"class":2053},[13611,13934,13935],{"v-slot:python":51},[46,13936,13940],{"className":13937,"code":13938,"language":13939,"meta":51,"style":51},"language-python shiki shiki-themes github-light","from anthropic import Anthropic\n\nclient = Anthropic()\n\nresponse = client.messages.create(\n    model=\"claude-opus-4-7\",\n    max_tokens=1024,\n    system=[\n        {\n            \"type\": \"text\",\n            \"text\": (\n                \"You are a senior backend engineer reviewing code for a \"\n                \"production PostgreSQL-backed Django service. Prioritize \"\n                \"correctness, then security, then performance.\"\n            ),\n            # Cache this instruction block — it rarely changes between calls.\n            \"cache_control\": {\"type\": \"ephemeral\"},\n        }\n    ],\n    messages=[\n        {\n            \"role\": \"user\",\n            \"content\": \"Review the diff in the attached PR and flag any N+1 queries.\",\n        }\n    ],\n)\n\nprint(response.content[0].text)\nprint(\"cached input tokens:\", response.usage.cache_read_input_tokens)\n","python",[22,13941,13942,13954,13958,13968,13972,13982,13994,14006,14016,14021,14033,14041,14046,14051,14056,14061,14066,14085,14089,14093,14102,14106,14118,14130,14134,14138,14143,14147,14160],{"__ignoreMap":51},[55,13943,13944,13946,13949,13951],{"class":57,"line":58},[55,13945,9868],{"class":2043},[55,13947,13948],{"class":2053}," anthropic ",[55,13950,10388],{"class":2043},[55,13952,13953],{"class":2053}," Anthropic\n",[55,13955,13956],{"class":57,"line":64},[55,13957,68],{"emptyLinePlaceholder":67},[55,13959,13960,13963,13965],{"class":57,"line":71},[55,13961,13962],{"class":2053},"client ",[55,13964,3475],{"class":2043},[55,13966,13967],{"class":2053}," Anthropic()\n",[55,13969,13970],{"class":57,"line":77},[55,13971,68],{"emptyLinePlaceholder":67},[55,13973,13974,13977,13979],{"class":57,"line":83},[55,13975,13976],{"class":2053},"response ",[55,13978,3475],{"class":2043},[55,13980,13981],{"class":2053}," client.messages.create(\n",[55,13983,13984,13987,13989,13992],{"class":57,"line":89},[55,13985,13986],{"class":2057},"    model",[55,13988,3475],{"class":2043},[55,13990,13991],{"class":721},"\"claude-opus-4-7\"",[55,13993,2250],{"class":2053},[55,13995,13996,13999,14001,14004],{"class":57,"line":95},[55,13997,13998],{"class":2057},"    max_tokens",[55,14000,3475],{"class":2043},[55,14002,14003],{"class":2069},"1024",[55,14005,2250],{"class":2053},[55,14007,14008,14011,14013],{"class":57,"line":101},[55,14009,14010],{"class":2057},"    system",[55,14012,3475],{"class":2043},[55,14014,14015],{"class":2053},"[\n",[55,14017,14018],{"class":57,"line":107},[55,14019,14020],{"class":2053},"        {\n",[55,14022,14023,14026,14028,14031],{"class":57,"line":113},[55,14024,14025],{"class":721},"            \"type\"",[55,14027,3323],{"class":2053},[55,14029,14030],{"class":721},"\"text\"",[55,14032,2250],{"class":2053},[55,14034,14035,14038],{"class":57,"line":119},[55,14036,14037],{"class":721},"            \"text\"",[55,14039,14040],{"class":2053},": (\n",[55,14042,14043],{"class":57,"line":125},[55,14044,14045],{"class":721},"                \"You are a senior backend engineer reviewing code for a \"\n",[55,14047,14048],{"class":57,"line":131},[55,14049,14050],{"class":721},"                \"production PostgreSQL-backed Django service. Prioritize \"\n",[55,14052,14053],{"class":57,"line":137},[55,14054,14055],{"class":721},"                \"correctness, then security, then performance.\"\n",[55,14057,14058],{"class":57,"line":143},[55,14059,14060],{"class":2053},"            ),\n",[55,14062,14063],{"class":57,"line":149},[55,14064,14065],{"class":711},"            # Cache this instruction block — it rarely changes between calls.\n",[55,14067,14068,14071,14074,14077,14079,14082],{"class":57,"line":441},[55,14069,14070],{"class":721},"            \"cache_control\"",[55,14072,14073],{"class":2053},": {",[55,14075,14076],{"class":721},"\"type\"",[55,14078,3323],{"class":2053},[55,14080,14081],{"class":721},"\"ephemeral\"",[55,14083,14084],{"class":2053},"},\n",[55,14086,14087],{"class":57,"line":447},[55,14088,10323],{"class":2053},[55,14090,14091],{"class":57,"line":453},[55,14092,10075],{"class":2053},[55,14094,14095,14098,14100],{"class":57,"line":459},[55,14096,14097],{"class":2057},"    messages",[55,14099,3475],{"class":2043},[55,14101,14015],{"class":2053},[55,14103,14104],{"class":57,"line":464},[55,14105,14020],{"class":2053},[55,14107,14108,14111,14113,14116],{"class":57,"line":470},[55,14109,14110],{"class":721},"            \"role\"",[55,14112,3323],{"class":2053},[55,14114,14115],{"class":721},"\"user\"",[55,14117,2250],{"class":2053},[55,14119,14120,14123,14125,14128],{"class":57,"line":475},[55,14121,14122],{"class":721},"            \"content\"",[55,14124,3323],{"class":2053},[55,14126,14127],{"class":721},"\"Review the diff in the attached PR and flag any N+1 queries.\"",[55,14129,2250],{"class":2053},[55,14131,14132],{"class":57,"line":481},[55,14133,10323],{"class":2053},[55,14135,14136],{"class":57,"line":486},[55,14137,10075],{"class":2053},[55,14139,14140],{"class":57,"line":492},[55,14141,14142],{"class":2053},")\n",[55,14144,14145],{"class":57,"line":497},[55,14146,68],{"emptyLinePlaceholder":67},[55,14148,14149,14152,14155,14157],{"class":57,"line":503},[55,14150,14151],{"class":2069},"print",[55,14153,14154],{"class":2053},"(response.content[",[55,14156,8584],{"class":2069},[55,14158,14159],{"class":2053},"].text)\n",[55,14161,14162,14164,14166,14169],{"class":57,"line":508},[55,14163,14151],{"class":2069},[55,14165,2054],{"class":2053},[55,14167,14168],{"class":721},"\"cached input tokens:\"",[55,14170,14171],{"class":2053},", response.usage.cache_read_input_tokens)\n",[18,14173,14174],{},"Two things to note:",[1396,14176,14177,14187],{},[1399,14178,14179,14182,14183,14186],{},[22,14180,14181],{},"cache_control"," on the system block is how you opt into prompt caching. On repeat calls with the same system text, ",[22,14184,14185],{},"cache_read_input_tokens"," climbs and your bill drops by up to 90% on that portion.",[1399,14188,14189],{},"No other changes from your 4.6 setup. Same message schema, same tool-use format — whether you're hitting the API directly from PHP/Laravel or going through the official Python SDK.",[18,14191,14192],{},"If you're on Bedrock or Vertex, swap the transport and keep the model string.",[13,14194,14196],{"id":14195},"should-you-upgrade-from-46","Should you upgrade from 4.6?",[18,14198,14199],{},"A short rubric:",[1396,14201,14202,14208,14214,14220,14226],{},[1399,14203,14204,14207],{},[518,14205,14206],{},"You're doing code generation or repo-scale edits."," Yes. The CursorBench jump alone pays for the migration.",[1399,14209,14210,14213],{},[518,14211,14212],{},"You're running agents with tool use."," Yes. The 2/3 reduction in tool errors compounds across every multi-step run.",[1399,14215,14216,14219],{},[518,14217,14218],{},"You're doing vision-heavy work"," (screenshots, documents, diagrams). Yes. The resolution bump removes a whole class of preprocessing hacks.",[1399,14221,14222,14225],{},[518,14223,14224],{},"You're using Claude mostly for short Q&A or summarization."," Probably fine either way. You'll see small quality wins, but 4.6 isn't suddenly broken.",[1399,14227,14228,14231],{},[518,14229,14230],{},"You're cost-sensitive."," Same price as 4.6. Upgrade.",[18,14233,14234],{},"For most production workloads the answer is: flip the string, rerun your evals, ship.",[13,14236,14238],{"id":14237},"closing","Closing",[18,14240,14241],{},"Opus 4.7 is the kind of release that rewards teams who already have a good eval harness — you'll see the gains clearly, and the upgrade cost is approximately zero. If you don't have evals yet, this is a good prompt to finally write them; you'll want to be able to measure the next point release the same way.",[18,14243,4723],{},[4725,14245,14246],{},"html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"title":51,"searchDepth":64,"depth":64,"links":14248},[14249,14250,14251,14252,14253,14254,14255,14256,14257],{"id":13330,"depth":64,"text":13331},{"id":13406,"depth":64,"text":13407},{"id":13449,"depth":64,"text":13450},{"id":13496,"depth":64,"text":13497},{"id":13520,"depth":64,"text":13521},{"id":13530,"depth":64,"text":13531},{"id":13597,"depth":64,"text":13598},{"id":14195,"depth":64,"text":14196},{"id":14237,"depth":64,"text":14238},"AI & Machine Learning","/blog-covers/claude-opus-4-7-launch.png","2026-04-16","A quick developer tour of Claude Opus 4.7 — 1M context, sharper vision, +13% coding gains over 4.6, same $5/$25 pricing, and how to start using it today.",{},"/posts/claude-opus-4-7-launch-overview","9 min read",{"title":13314,"description":14261},{"loc":14263,"lastmod":14260},"posts/claude-opus-4-7-launch-overview",[14269,14270,14271,14272,14273,14274],"claude","anthropic","llm","ai","opus-4-7","announcement","vY3EL88mrUx0u8lR0aOU7RXjm3p2cFm1ZKw1nSlpVFE",{"id":14277,"title":14278,"author":8,"body":14279,"category":16810,"cover":16811,"date":16812,"description":16813,"extension":4794,"featured":16814,"meta":16815,"navigation":67,"path":16816,"published":67,"readingTime":4797,"seo":16817,"sitemap":16818,"stem":16819,"tags":16820,"updated":16812,"__hash__":16825},"posts/posts/laravel-queues-explained-simply.md","Laravel Queues Explained Simply (Laravel 13)",{"type":10,"value":14280,"toc":16756},[14281,14285,14289,14292,14295,14299,14325,14329,14332,14346,14349,14353,14359,14363,14370,14376,14379,14542,14556,14560,14563,14591,14598,14627,14638,14642,14648,14677,14680,14719,14726,14730,14739,14742,14757,14764,14772,14776,14779,14782,14797,14804,14821,14828,14832,14835,14864,14869,14873,14879,14882,14893,14897,14900,14917,14931,14934,15139,15143,15150,15233,15243,15247,15250,15275,15278,15282,15285,15289,15292,15315,15319,15326,15474,15489,15493,15500,15606,15610,15613,15675,15691,15695,15698,15746,15749,15753,15756,15763,15767,15770,15817,15820,15824,15830,15871,15878,15882,15889,15938,15941,15945,15952,15955,15959,15962,15966,15992,15996,15999,16016,16030,16034,16044,16090,16093,16097,16107,16111,16240,16243,16302,16306,16311,16336,16340,16347,16445,16448,16452,16455,16459,16465,16479,16482,16490,16499,16516,16519,16530,16534,16547,16551,16557,16651,16655,16658,16662,16669,16673,16679,16701,16710,16714,16717,16720,16751,16753],[13,14282,14284],{"id":14283},"overview-of-queues","Overview of Queues",[343,14286,14288],{"id":14287},"what-are-queues","What Are Queues?",[18,14290,14291],{},"In web applications, some tasks take too long to complete during a normal HTTP request. For example, processing a large uploaded CSV file, sending hundreds of emails, or generating a PDF report. If you do these tasks right away, the user would have to wait several seconds (or even minutes) before seeing the next page. This creates a poor user experience.",[18,14293,14294],{},"Queues solve this problem by letting you defer time‑consuming tasks to be processed in the background. Your application immediately returns a response to the user, while the heavy work happens later, outside the request cycle.",[343,14296,14298],{"id":14297},"why-are-queues-important","Why Are Queues Important?",[1396,14300,14301,14307,14313,14319],{},[1399,14302,14303,14306],{},[518,14304,14305],{},"Improved user experience"," – pages load faster.",[1399,14308,14309,14312],{},[518,14310,14311],{},"Better scalability"," – you can add more queue workers to handle spikes in traffic.",[1399,14314,14315,14318],{},[518,14316,14317],{},"Reliability"," – if a task fails, the queue can retry it automatically.",[1399,14320,14321,14324],{},[518,14322,14323],{},"Resource management"," – you can prioritise certain jobs (e.g., process high‑priority tasks first).",[343,14326,14328],{"id":14327},"realworld-example","Real‑World Example",[18,14330,14331],{},"Suppose, you run a podcast website. When a user uploads a new episode, you need to:",[2527,14333,14334,14337,14340,14343],{},[1399,14335,14336],{},"Convert the audio file to different formats.",[1399,14338,14339],{},"Generate a thumbnail.",[1399,14341,14342],{},"Update search indexes.",[1399,14344,14345],{},"Send confirmation emails to subscribers.",[18,14347,14348],{},"Doing all this synchronously would make the upload feel very slow. Instead, after the user uploads the file, you dispatch a job to a queue. The user sees “Upload successful” immediately, while behind the scenes a queue worker processes the conversion, thumbnails, etc.",[13,14350,14352],{"id":14351},"fundamental-concepts-of-laravel-queues","Fundamental Concepts of Laravel Queues",[18,14354,14355],{},[4835,14356],{"alt":14357,"src":14358},"Laravel Queues and Jobs Processing","/blog-post-images/laravel-queues-explained-simply/laravel-queues-jobs-processing.png",[343,14360,14362],{"id":14361},"jobs-the-unit-of-work","Jobs – The Unit of Work",[18,14364,14365,14366,14369],{},"A job is a simple PHP class that contains the logic for a task you want to run in the background. For example, a ",[22,14367,14368],{},"ProcessPodcast"," job might contain the code to convert an audio file.",[18,14371,14372,14373],{},"You create a job using Artisan: ",[22,14374,14375],{},"php artisan make:job ProcessPodcast",[18,14377,14378],{},"Here’s what a basic job looks like:",[46,14380,14382],{"className":13615,"code":14381,"language":13609,"meta":51,"style":51},"\u003C?php\n\nnamespace App\\Jobs;\n\nuse App\\Models\\Podcast;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Queue\\Queueable;\n\nclass ProcessPodcast implements ShouldQueue\n{\n    use Queueable;\n\n    public function __construct(public Podcast $podcast)\n    {}\n\n    public function handle(): void\n    {\n        // The actual work happens here\n        // e.g., convert audio, generate thumbnail...\n    }\n}\n",[22,14383,14384,14392,14396,14406,14410,14419,14428,14437,14441,14455,14459,14469,14473,14494,14499,14503,14520,14524,14529,14534,14538],{"__ignoreMap":51},[55,14385,14386,14389],{"class":57,"line":58},[55,14387,14388],{"class":2043},"\u003C?",[55,14390,14391],{"class":2069},"php\n",[55,14393,14394],{"class":57,"line":64},[55,14395,68],{"emptyLinePlaceholder":67},[55,14397,14398,14401,14404],{"class":57,"line":71},[55,14399,14400],{"class":2043},"namespace",[55,14402,14403],{"class":717}," App\\Jobs",[55,14405,5761],{"class":2053},[55,14407,14408],{"class":57,"line":77},[55,14409,68],{"emptyLinePlaceholder":67},[55,14411,14412,14414,14417],{"class":57,"line":83},[55,14413,13623],{"class":2043},[55,14415,14416],{"class":2069}," App\\Models\\Podcast",[55,14418,5761],{"class":2053},[55,14420,14421,14423,14426],{"class":57,"line":89},[55,14422,13623],{"class":2043},[55,14424,14425],{"class":2069}," Illuminate\\Contracts\\Queue\\ShouldQueue",[55,14427,5761],{"class":2053},[55,14429,14430,14432,14435],{"class":57,"line":95},[55,14431,13623],{"class":2043},[55,14433,14434],{"class":2069}," Illuminate\\Foundation\\Queue\\Queueable",[55,14436,5761],{"class":2053},[55,14438,14439],{"class":57,"line":101},[55,14440,68],{"emptyLinePlaceholder":67},[55,14442,14443,14446,14449,14452],{"class":57,"line":107},[55,14444,14445],{"class":2043},"class",[55,14447,14448],{"class":717}," ProcessPodcast",[55,14450,14451],{"class":2043}," implements",[55,14453,14454],{"class":717}," ShouldQueue\n",[55,14456,14457],{"class":57,"line":113},[55,14458,4945],{"class":2053},[55,14460,14461,14464,14467],{"class":57,"line":119},[55,14462,14463],{"class":2043},"    use",[55,14465,14466],{"class":2069}," Queueable",[55,14468,5761],{"class":2053},[55,14470,14471],{"class":57,"line":125},[55,14472,68],{"emptyLinePlaceholder":67},[55,14474,14475,14478,14480,14483,14485,14488,14491],{"class":57,"line":131},[55,14476,14477],{"class":2043},"    public",[55,14479,2047],{"class":2043},[55,14481,14482],{"class":2069}," __construct",[55,14484,2054],{"class":2053},[55,14486,14487],{"class":2043},"public",[55,14489,14490],{"class":2069}," Podcast",[55,14492,14493],{"class":2053}," $podcast)\n",[55,14495,14496],{"class":57,"line":137},[55,14497,14498],{"class":2053},"    {}\n",[55,14500,14501],{"class":57,"line":143},[55,14502,68],{"emptyLinePlaceholder":67},[55,14504,14505,14507,14509,14512,14515,14517],{"class":57,"line":149},[55,14506,14477],{"class":2043},[55,14508,2047],{"class":2043},[55,14510,14511],{"class":717}," handle",[55,14513,14514],{"class":2053},"()",[55,14516,5495],{"class":2043},[55,14518,14519],{"class":2043}," void\n",[55,14521,14522],{"class":57,"line":441},[55,14523,5089],{"class":2053},[55,14525,14526],{"class":57,"line":447},[55,14527,14528],{"class":711},"        // The actual work happens here\n",[55,14530,14531],{"class":57,"line":453},[55,14532,14533],{"class":711},"        // e.g., convert audio, generate thumbnail...\n",[55,14535,14536],{"class":57,"line":459},[55,14537,1962],{"class":2053},[55,14539,14540],{"class":57,"line":464},[55,14541,2005],{"class":2053},[1396,14543,14544,14550],{},[1399,14545,14546,14549],{},[22,14547,14548],{},"ShouldQueue"," tells Laravel to push this job to a queue instead of running it immediately.",[1399,14551,4542,14552,14555],{},[22,14553,14554],{},"handle()"," method is called when the queue worker processes the job.",[343,14557,14559],{"id":14558},"connections-vs-queues","Connections vs. Queues",[18,14561,14562],{},"Think of your application’s queue system like a post office:",[1396,14564,14565,14585],{},[1399,14566,14567,14570,14571,525,14574,14577,14578,14581,14582,532],{},[518,14568,14569],{},"Connection"," = the postal service you choose (e.g., regular mail, express courier, or a local drop‑off). In Laravel, connections are backends like ",[22,14572,14573],{},"database",[22,14575,14576],{},"redis",", or ",[22,14579,14580],{},"sqs",". They are defined in ",[22,14583,14584],{},"config/queue.php",[1399,14586,14587,14590],{},[518,14588,14589],{},"Queue"," = a specific pile or mailbox inside that service. You can have multiple queues per connection, like “emails”, “reports”, “thumbnails”.",[18,14592,14593,14594,14597],{},"By default, every job goes into the ",[22,14595,14596],{},"default"," queue of the default connection. But you can send a job to a specific queue:",[46,14599,14601],{"className":13615,"code":14600,"language":13609,"meta":51,"style":51},"ProcessPodcast::dispatch($podcast)->onQueue('processing');\n",[22,14602,14603],{"__ignoreMap":51},[55,14604,14605,14607,14609,14612,14615,14617,14620,14622,14625],{"class":57,"line":58},[55,14606,14368],{"class":2069},[55,14608,13645],{"class":2043},[55,14610,14611],{"class":717},"dispatch",[55,14613,14614],{"class":2053},"($podcast)",[55,14616,13701],{"class":2043},[55,14618,14619],{"class":717},"onQueue",[55,14621,2054],{"class":2053},[55,14623,14624],{"class":721},"'processing'",[55,14626,2178],{"class":2053},[18,14628,14629,14630,14633,14634,14637],{},"This helps you prioritise work: you can tell a queue worker to process the ",[22,14631,14632],{},"high"," queue before the ",[22,14635,14636],{},"low"," queue.",[343,14639,14641],{"id":14640},"dispatching-jobs-adding-work-to-the-queue","Dispatching Jobs – Adding Work to the Queue",[18,14643,14644,14645,14647],{},"To send a job to the queue, you call the ",[22,14646,14611],{}," method on the job class:",[46,14649,14651],{"className":13615,"code":14650,"language":13609,"meta":51,"style":51},"use App\\Jobs\\ProcessPodcast;\n\nProcessPodcast::dispatch($podcast);\n",[22,14652,14653,14662,14666],{"__ignoreMap":51},[55,14654,14655,14657,14660],{"class":57,"line":58},[55,14656,13623],{"class":2043},[55,14658,14659],{"class":2069}," App\\Jobs\\ProcessPodcast",[55,14661,5761],{"class":2053},[55,14663,14664],{"class":57,"line":64},[55,14665,68],{"emptyLinePlaceholder":67},[55,14667,14668,14670,14672,14674],{"class":57,"line":71},[55,14669,14368],{"class":2069},[55,14671,13645],{"class":2043},[55,14673,14611],{"class":717},[55,14675,14676],{"class":2053},"($podcast);\n",[18,14678,14679],{},"You can also delay the job:",[46,14681,14683],{"className":13615,"code":14682,"language":13609,"meta":51,"style":51},"ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10));\n",[22,14684,14685],{"__ignoreMap":51},[55,14686,14687,14689,14691,14693,14695,14697,14700,14702,14705,14707,14709,14712,14714,14717],{"class":57,"line":58},[55,14688,14368],{"class":2069},[55,14690,13645],{"class":2043},[55,14692,14611],{"class":717},[55,14694,14614],{"class":2053},[55,14696,13701],{"class":2043},[55,14698,14699],{"class":717},"delay",[55,14701,2054],{"class":2053},[55,14703,14704],{"class":717},"now",[55,14706,14514],{"class":2053},[55,14708,13701],{"class":2043},[55,14710,14711],{"class":717},"addMinutes",[55,14713,2054],{"class":2053},[55,14715,14716],{"class":2069},"10",[55,14718,8359],{"class":2053},[18,14720,14721,14722,14725],{},"If you ever need to run a job immediately (in the same request), use ",[22,14723,14724],{},"dispatchSync()"," – useful for testing or very quick tasks.",[343,14727,14729],{"id":14728},"queue-worker-the-engine-that-processes-jobs","Queue Worker – The Engine That Processes Jobs",[18,14731,4824,14732,14735,14736,14738],{},[518,14733,14734],{},"queue worker"," is a long‑running PHP process that constantly checks the queue for new jobs. When it finds one, it executes the job’s ",[22,14737,14554],{}," method.",[18,14740,14741],{},"Start a worker with:",[46,14743,14745],{"className":702,"code":14744,"language":704,"meta":51,"style":51},"php artisan queue:work\n",[22,14746,14747],{"__ignoreMap":51},[55,14748,14749,14751,14754],{"class":57,"line":58},[55,14750,13609],{"class":717},[55,14752,14753],{"class":721}," artisan",[55,14755,14756],{"class":721}," queue:work\n",[18,14758,14759,14760,14763],{},"By default, the worker will run forever. For production, you should use a process monitor like ",[518,14761,14762],{},"Supervisor"," to automatically restart the worker if it crashes.",[300,14765,14766],{},[18,14767,14768,14769],{},"Important: Workers keep your application in memory. If you change your code, you must restart the worker: ",[22,14770,14771],{},"php artisan queue:restart",[343,14773,14775],{"id":14774},"handling-failures-what-if-something-goes-wrong","Handling Failures – What If Something Goes Wrong?",[18,14777,14778],{},"Sometimes a job fails (e.g., an external API is down). Laravel can retry the job a number of times before giving up.",[18,14780,14781],{},"You set the maximum attempts:",[1396,14783,14784,14790],{},[1399,14785,14786,14787],{},"Globally for the worker: ",[22,14788,14789],{},"php artisan queue:work --tries=3",[1399,14791,14792,14793,14796],{},"Or on the job class using the ",[22,14794,14795],{},"#[Tries(3)]"," attribute.",[18,14798,14799,14800,14803],{},"When all attempts fail, Laravel stores the failed job in a ",[22,14801,14802],{},"failed_jobs"," database table. You can then retry failed jobs manually:",[46,14805,14807],{"className":702,"code":14806,"language":704,"meta":51,"style":51},"php artisan queue:retry all\n",[22,14808,14809],{"__ignoreMap":51},[55,14810,14811,14813,14815,14818],{"class":57,"line":58},[55,14812,13609],{"class":717},[55,14814,14753],{"class":721},[55,14816,14817],{"class":721}," queue:retry",[55,14819,14820],{"class":721}," all\n",[18,14822,14823,14824,14827],{},"You can also define a ",[22,14825,14826],{},"failed()"," method inside your job to run custom cleanup or notifications when a job finally fails.",[343,14829,14831],{"id":14830},"putting-it-all-together-a-simple-workflow","Putting It All Together – A Simple Workflow",[18,14833,14834],{},"Let's put the steps together for the real‑world example:",[2527,14836,14837,14843,14846,14849,14855,14858],{},[1399,14838,14839,14840,14842],{},"User uploads a podcast → Controller dispatches a ",[22,14841,14368],{}," job.",[1399,14844,14845],{},"User receives “success” response immediately.",[1399,14847,14848],{},"Queue worker (running separately) picks up the job.",[1399,14850,14851,14852,14854],{},"Worker executes the ",[22,14853,14554],{}," method (converts audio, etc.).",[1399,14856,14857],{},"If the job fails, Laravel retries it up to the configured limit.",[1399,14859,14860,14861,14863],{},"After final failure, the job appears in the ",[22,14862,14802],{}," table for you to inspect and retry.",[300,14865,14866],{},[18,14867,14868],{},"Let's now explore some advanced features of Laravel queues that can help you manage complex workflows and ensure your jobs run smoothly.",[13,14870,14872],{"id":14871},"job-middleware-adding-layers-to-your-jobs","Job Middleware – Adding Layers to Your Jobs",[18,14874,14875,14876,14878],{},"Sometimes you want to run some code before or after your job executes, without cluttering the job’s ",[22,14877,14554],{}," method. Job middleware lets you wrap custom logic around your job’s execution.",[18,14880,14881],{},"Think of it like a “filter” for your job. For example, you might want to:",[1396,14883,14884,14887,14890],{},[1399,14885,14886],{},"Rate‑limit how often a job can run.",[1399,14888,14889],{},"Prevent two identical jobs from running at the same time.",[1399,14891,14892],{},"Skip the job if a certain condition is not met.",[343,14894,14896],{"id":14895},"creating-a-job-middleware","Creating a Job Middleware",[18,14898,14899],{},"You can generate a middleware class with Artisan:",[46,14901,14903],{"className":702,"code":14902,"language":704,"meta":51,"style":51},"php artisan make:job-middleware RateLimited\n",[22,14904,14905],{"__ignoreMap":51},[55,14906,14907,14909,14911,14914],{"class":57,"line":58},[55,14908,13609],{"class":717},[55,14910,14753],{"class":721},[55,14912,14913],{"class":721}," make:job-middleware",[55,14915,14916],{"class":721}," RateLimited\n",[18,14918,14919,14920,14922,14923,14926,14927,14930],{},"A middleware class contains a ",[22,14921,14554],{}," method that receives the job and a closure (",[22,14924,14925],{},"$next","). You perform your checks, and then call ",[22,14928,14929],{},"$next($job)"," to continue processing.",[18,14932,14933],{},"Example – a simple rate limiter middleware:",[46,14935,14937],{"className":13615,"code":14936,"language":13609,"meta":51,"style":51},"\u003C?php\n\nnamespace App\\Jobs\\Middleware;\n\nuse Closure;\nuse Illuminate\\Support\\Facades\\Redis;\n\nclass RateLimited\n{\n    public function handle(object $job, Closure $next): void\n    {\n        Redis::throttle('key')\n            ->allow(1)->every(5)\n            ->then(function () use ($job, $next) {\n                $next($job);   // job can run\n            }, function () use ($job) {\n                $job->release(5); // try again after 5 seconds\n            });\n    }\n}\n",[22,14938,14939,14945,14949,14958,14962,14971,14980,14984,14990,14994,15020,15024,15041,15067,15084,15092,15106,15126,15131,15135],{"__ignoreMap":51},[55,14940,14941,14943],{"class":57,"line":58},[55,14942,14388],{"class":2043},[55,14944,14391],{"class":2069},[55,14946,14947],{"class":57,"line":64},[55,14948,68],{"emptyLinePlaceholder":67},[55,14950,14951,14953,14956],{"class":57,"line":71},[55,14952,14400],{"class":2043},[55,14954,14955],{"class":717}," App\\Jobs\\Middleware",[55,14957,5761],{"class":2053},[55,14959,14960],{"class":57,"line":77},[55,14961,68],{"emptyLinePlaceholder":67},[55,14963,14964,14966,14969],{"class":57,"line":83},[55,14965,13623],{"class":2043},[55,14967,14968],{"class":2069}," Closure",[55,14970,5761],{"class":2053},[55,14972,14973,14975,14978],{"class":57,"line":89},[55,14974,13623],{"class":2043},[55,14976,14977],{"class":2069}," Illuminate\\Support\\Facades\\Redis",[55,14979,5761],{"class":2053},[55,14981,14982],{"class":57,"line":95},[55,14983,68],{"emptyLinePlaceholder":67},[55,14985,14986,14988],{"class":57,"line":101},[55,14987,14445],{"class":2043},[55,14989,14916],{"class":717},[55,14991,14992],{"class":57,"line":107},[55,14993,4945],{"class":2053},[55,14995,14996,14998,15000,15002,15004,15007,15010,15013,15016,15018],{"class":57,"line":113},[55,14997,14477],{"class":2043},[55,14999,2047],{"class":2043},[55,15001,14511],{"class":717},[55,15003,2054],{"class":2053},[55,15005,15006],{"class":2043},"object",[55,15008,15009],{"class":2053}," $job, ",[55,15011,15012],{"class":2069},"Closure",[55,15014,15015],{"class":2053}," $next)",[55,15017,5495],{"class":2043},[55,15019,14519],{"class":2043},[55,15021,15022],{"class":57,"line":119},[55,15023,5089],{"class":2053},[55,15025,15026,15029,15031,15034,15036,15039],{"class":57,"line":125},[55,15027,15028],{"class":2069},"        Redis",[55,15030,13645],{"class":2043},[55,15032,15033],{"class":717},"throttle",[55,15035,2054],{"class":2053},[55,15037,15038],{"class":721},"'key'",[55,15040,14142],{"class":2053},[55,15042,15043,15046,15049,15051,15054,15056,15058,15061,15063,15065],{"class":57,"line":131},[55,15044,15045],{"class":2043},"            ->",[55,15047,15048],{"class":717},"allow",[55,15050,2054],{"class":2053},[55,15052,15053],{"class":2069},"1",[55,15055,12108],{"class":2053},[55,15057,13701],{"class":2043},[55,15059,15060],{"class":717},"every",[55,15062,2054],{"class":2053},[55,15064,11566],{"class":2069},[55,15066,14142],{"class":2053},[55,15068,15069,15071,15073,15075,15077,15079,15081],{"class":57,"line":137},[55,15070,15045],{"class":2043},[55,15072,6061],{"class":717},[55,15074,2054],{"class":2053},[55,15076,9734],{"class":2043},[55,15078,8926],{"class":2053},[55,15080,13623],{"class":2043},[55,15082,15083],{"class":2053}," ($job, $next) {\n",[55,15085,15086,15089],{"class":57,"line":143},[55,15087,15088],{"class":2053},"                $next($job);   ",[55,15090,15091],{"class":711},"// job can run\n",[55,15093,15094,15097,15099,15101,15103],{"class":57,"line":149},[55,15095,15096],{"class":2053},"            }, ",[55,15098,9734],{"class":2043},[55,15100,8926],{"class":2053},[55,15102,13623],{"class":2043},[55,15104,15105],{"class":2053}," ($job) {\n",[55,15107,15108,15111,15113,15116,15118,15120,15123],{"class":57,"line":441},[55,15109,15110],{"class":2053},"                $job",[55,15112,13701],{"class":2043},[55,15114,15115],{"class":717},"release",[55,15117,2054],{"class":2053},[55,15119,11566],{"class":2069},[55,15121,15122],{"class":2053},"); ",[55,15124,15125],{"class":711},"// try again after 5 seconds\n",[55,15127,15128],{"class":57,"line":447},[55,15129,15130],{"class":2053},"            });\n",[55,15132,15133],{"class":57,"line":453},[55,15134,1962],{"class":2053},[55,15136,15137],{"class":57,"line":459},[55,15138,2005],{"class":2053},[343,15140,15142],{"id":15141},"attaching-middleware-to-a-job","Attaching Middleware to a Job",[18,15144,15145,15146,15149],{},"Inside your job class, add a ",[22,15147,15148],{},"middleware()"," method that returns an array of middleware objects:",[46,15151,15153],{"className":13615,"code":15152,"language":13609,"meta":51,"style":51},"use App\\Jobs\\Middleware\\RateLimited;\n\nclass ProcessPodcast implements ShouldQueue\n{\n    // ...\n\n    public function middleware(): array\n    {\n        return [new RateLimited];\n    }\n}\n",[22,15154,15155,15164,15168,15178,15182,15187,15191,15207,15211,15225,15229],{"__ignoreMap":51},[55,15156,15157,15159,15162],{"class":57,"line":58},[55,15158,13623],{"class":2043},[55,15160,15161],{"class":2069}," App\\Jobs\\Middleware\\RateLimited",[55,15163,5761],{"class":2053},[55,15165,15166],{"class":57,"line":64},[55,15167,68],{"emptyLinePlaceholder":67},[55,15169,15170,15172,15174,15176],{"class":57,"line":71},[55,15171,14445],{"class":2043},[55,15173,14448],{"class":717},[55,15175,14451],{"class":2043},[55,15177,14454],{"class":717},[55,15179,15180],{"class":57,"line":77},[55,15181,4945],{"class":2053},[55,15183,15184],{"class":57,"line":83},[55,15185,15186],{"class":711},"    // ...\n",[55,15188,15189],{"class":57,"line":89},[55,15190,68],{"emptyLinePlaceholder":67},[55,15192,15193,15195,15197,15200,15202,15204],{"class":57,"line":95},[55,15194,14477],{"class":2043},[55,15196,2047],{"class":2043},[55,15198,15199],{"class":717}," middleware",[55,15201,14514],{"class":2053},[55,15203,5495],{"class":2043},[55,15205,15206],{"class":2043}," array\n",[55,15208,15209],{"class":57,"line":101},[55,15210,5089],{"class":2053},[55,15212,15213,15215,15217,15220,15223],{"class":57,"line":107},[55,15214,2116],{"class":2043},[55,15216,7064],{"class":2053},[55,15218,15219],{"class":2043},"new",[55,15221,15222],{"class":2069}," RateLimited",[55,15224,6898],{"class":2053},[55,15226,15227],{"class":57,"line":113},[55,15228,1962],{"class":2053},[55,15230,15231],{"class":57,"line":119},[55,15232,2005],{"class":2053},[18,15234,15235,15236,15238,15239,15242],{},"Now every time ",[22,15237,14368],{}," is processed, the ",[22,15240,15241],{},"RateLimited"," middleware will be applied.",[343,15244,15246],{"id":15245},"builtin-middleware","Built‑in Middleware",[18,15248,15249],{},"Laravel provides several helpful middleware out of the box:",[1396,15251,15252,15257,15263,15269],{},[1399,15253,15254,15256],{},[22,15255,15241],{}," – limit how often a job runs based on a rate limiter you define.",[1399,15258,15259,15262],{},[22,15260,15261],{},"WithoutOverlapping"," – prevent two jobs with the same “key” from running simultaneously.",[1399,15264,15265,15268],{},[22,15266,15267],{},"ThrottlesExceptions"," – if a job fails repeatedly, stop trying for a while.",[1399,15270,15271,15274],{},[22,15272,15273],{},"SkipIfBatchCancelled"," – for batch jobs, skip processing if the batch was cancelled.",[18,15276,15277],{},"Using middleware keeps your jobs clean and focused on their primary task.",[13,15279,15281],{"id":15280},"job-batching-managing-groups-of-jobs","Job Batching – Managing Groups of Jobs",[18,15283,15284],{},"Sometimes you need to run a set of jobs in parallel and then do something when they all finish. Job batching is perfect for this. You can think of it as a “parent” that tracks a group of jobs and notifies you when the group is complete.",[343,15286,15288],{"id":15287},"setting-up-batches","Setting Up Batches",[18,15290,15291],{},"First, create a table to store batch information:",[46,15293,15295],{"className":702,"code":15294,"language":704,"meta":51,"style":51},"php artisan make:queue-batches-table\nphp artisan migrate\n",[22,15296,15297,15306],{"__ignoreMap":51},[55,15298,15299,15301,15303],{"class":57,"line":58},[55,15300,13609],{"class":717},[55,15302,14753],{"class":721},[55,15304,15305],{"class":721}," make:queue-batches-table\n",[55,15307,15308,15310,15312],{"class":57,"line":64},[55,15309,13609],{"class":717},[55,15311,14753],{"class":721},[55,15313,15314],{"class":721}," migrate\n",[343,15316,15318],{"id":15317},"creating-a-batch","Creating a Batch",[18,15320,15321,15322,15325],{},"Use the ",[22,15323,15324],{},"Bus"," facade to define a batch of jobs:",[46,15327,15329],{"className":13615,"code":15328,"language":13609,"meta":51,"style":51},"use App\\Jobs\\ImportCsv;\nuse Illuminate\\Support\\Facades\\Bus;\n\n$batch = Bus::batch([\n    new ImportCsv(1, 100),\n    new ImportCsv(101, 200),\n    new ImportCsv(201, 300),\n])->then(function ($batch) {\n    // All jobs completed successfully\n})->catch(function ($batch, $e) {\n    // A job failed\n})->dispatch();\n",[22,15330,15331,15340,15349,15353,15370,15388,15405,15423,15438,15443,15459,15464],{"__ignoreMap":51},[55,15332,15333,15335,15338],{"class":57,"line":58},[55,15334,13623],{"class":2043},[55,15336,15337],{"class":2069}," App\\Jobs\\ImportCsv",[55,15339,5761],{"class":2053},[55,15341,15342,15344,15347],{"class":57,"line":64},[55,15343,13623],{"class":2043},[55,15345,15346],{"class":2069}," Illuminate\\Support\\Facades\\Bus",[55,15348,5761],{"class":2053},[55,15350,15351],{"class":57,"line":71},[55,15352,68],{"emptyLinePlaceholder":67},[55,15354,15355,15358,15360,15363,15365,15368],{"class":57,"line":77},[55,15356,15357],{"class":2053},"$batch ",[55,15359,3475],{"class":2043},[55,15361,15362],{"class":2069}," Bus",[55,15364,13645],{"class":2043},[55,15366,15367],{"class":717},"batch",[55,15369,13651],{"class":2053},[55,15371,15372,15375,15378,15380,15382,15384,15386],{"class":57,"line":83},[55,15373,15374],{"class":2043},"    new",[55,15376,15377],{"class":2069}," ImportCsv",[55,15379,2054],{"class":2053},[55,15381,15053],{"class":2069},[55,15383,525],{"class":2053},[55,15385,8569],{"class":2069},[55,15387,9631],{"class":2053},[55,15389,15390,15392,15394,15396,15399,15401,15403],{"class":57,"line":89},[55,15391,15374],{"class":2043},[55,15393,15377],{"class":2069},[55,15395,2054],{"class":2053},[55,15397,15398],{"class":2069},"101",[55,15400,525],{"class":2053},[55,15402,2196],{"class":2069},[55,15404,9631],{"class":2053},[55,15406,15407,15409,15411,15413,15416,15418,15421],{"class":57,"line":95},[55,15408,15374],{"class":2043},[55,15410,15377],{"class":2069},[55,15412,2054],{"class":2053},[55,15414,15415],{"class":2069},"201",[55,15417,525],{"class":2053},[55,15419,15420],{"class":2069},"300",[55,15422,9631],{"class":2053},[55,15424,15425,15427,15429,15431,15433,15435],{"class":57,"line":101},[55,15426,13698],{"class":2053},[55,15428,13701],{"class":2043},[55,15430,6061],{"class":717},[55,15432,2054],{"class":2053},[55,15434,9734],{"class":2043},[55,15436,15437],{"class":2053}," ($batch) {\n",[55,15439,15440],{"class":57,"line":107},[55,15441,15442],{"class":711},"    // All jobs completed successfully\n",[55,15444,15445,15448,15450,15452,15454,15456],{"class":57,"line":113},[55,15446,15447],{"class":2053},"})",[55,15449,13701],{"class":2043},[55,15451,5899],{"class":717},[55,15453,2054],{"class":2053},[55,15455,9734],{"class":2043},[55,15457,15458],{"class":2053}," ($batch, $e) {\n",[55,15460,15461],{"class":57,"line":119},[55,15462,15463],{"class":711},"    // A job failed\n",[55,15465,15466,15468,15470,15472],{"class":57,"line":125},[55,15467,15447],{"class":2053},[55,15469,13701],{"class":2043},[55,15471,14611],{"class":717},[55,15473,7014],{"class":2053},[18,15475,15476,15477,15480,15481,15484,15485,15488],{},"You can also add callbacks for when the batch is created (",[22,15478,15479],{},"before",") or for progress updates (",[22,15482,15483],{},"progress","). Each callback receives the ",[22,15486,15487],{},"Batch"," instance, which you can use to inspect the batch’s status.",[343,15490,15492],{"id":15491},"working-with-batches-inside-a-job","Working with Batches Inside a Job",[18,15494,15495,15496,15499],{},"Inside a batchable job, you can access the current batch via the ",[22,15497,15498],{},"batch()"," method:",[46,15501,15503],{"className":13615,"code":15502,"language":13609,"meta":51,"style":51},"use Illuminate\\Bus\\Batchable;\n\nclass ImportCsv implements ShouldQueue\n{\n    use Batchable;\n\n    public function handle()\n    {\n        if ($this->batch()->cancelled()) {\n            return;\n        }\n        // process a chunk...\n    }\n}\n",[22,15504,15505,15514,15518,15528,15532,15541,15545,15556,15560,15583,15589,15593,15598,15602],{"__ignoreMap":51},[55,15506,15507,15509,15512],{"class":57,"line":58},[55,15508,13623],{"class":2043},[55,15510,15511],{"class":2069}," Illuminate\\Bus\\Batchable",[55,15513,5761],{"class":2053},[55,15515,15516],{"class":57,"line":64},[55,15517,68],{"emptyLinePlaceholder":67},[55,15519,15520,15522,15524,15526],{"class":57,"line":71},[55,15521,14445],{"class":2043},[55,15523,15377],{"class":717},[55,15525,14451],{"class":2043},[55,15527,14454],{"class":717},[55,15529,15530],{"class":57,"line":77},[55,15531,4945],{"class":2053},[55,15533,15534,15536,15539],{"class":57,"line":83},[55,15535,14463],{"class":2043},[55,15537,15538],{"class":2069}," Batchable",[55,15540,5761],{"class":2053},[55,15542,15543],{"class":57,"line":89},[55,15544,68],{"emptyLinePlaceholder":67},[55,15546,15547,15549,15551,15553],{"class":57,"line":95},[55,15548,14477],{"class":2043},[55,15550,2047],{"class":2043},[55,15552,14511],{"class":717},[55,15554,15555],{"class":2053},"()\n",[55,15557,15558],{"class":57,"line":101},[55,15559,5089],{"class":2053},[55,15561,15562,15564,15566,15569,15571,15573,15575,15577,15580],{"class":57,"line":107},[55,15563,10287],{"class":2043},[55,15565,2093],{"class":2053},[55,15567,15568],{"class":2069},"$this",[55,15570,13701],{"class":2043},[55,15572,15367],{"class":717},[55,15574,14514],{"class":2053},[55,15576,13701],{"class":2043},[55,15578,15579],{"class":717},"cancelled",[55,15581,15582],{"class":2053},"()) {\n",[55,15584,15585,15587],{"class":57,"line":113},[55,15586,7215],{"class":2043},[55,15588,5761],{"class":2053},[55,15590,15591],{"class":57,"line":119},[55,15592,10323],{"class":2053},[55,15594,15595],{"class":57,"line":125},[55,15596,15597],{"class":711},"        // process a chunk...\n",[55,15599,15600],{"class":57,"line":131},[55,15601,1962],{"class":2053},[55,15603,15604],{"class":57,"line":137},[55,15605,2005],{"class":2053},[343,15607,15609],{"id":15608},"inspecting-batches","Inspecting Batches",[18,15611,15612],{},"You can retrieve a batch by its ID and return it from a route to show progress in your frontend:",[46,15614,15616],{"className":13615,"code":15615,"language":13609,"meta":51,"style":51},"Route::get(‘/batch/{batchId}’, function ($batchId) {\n    return Bus::findBatch($batchId);\n});\n",[22,15617,15618,15657,15671],{"__ignoreMap":51},[55,15619,15620,15623,15625,15627,15629,15632,15634,15636,15638,15641,15644,15647,15650,15652,15654],{"class":57,"line":58},[55,15621,15622],{"class":2069},"Route",[55,15624,13645],{"class":2043},[55,15626,2164],{"class":717},[55,15628,2054],{"class":2053},[55,15630,15631],{"class":2069},"‘",[55,15633,3857],{"class":2043},[55,15635,15367],{"class":2069},[55,15637,3857],{"class":2043},[55,15639,15640],{"class":2053},"{",[55,15642,15643],{"class":2069},"batchId",[55,15645,15646],{"class":2053},"}",[55,15648,15649],{"class":2069},"’",[55,15651,525],{"class":2053},[55,15653,9734],{"class":2043},[55,15655,15656],{"class":2053}," ($batchId) {\n",[55,15658,15659,15661,15663,15665,15668],{"class":57,"line":64},[55,15660,2356],{"class":2043},[55,15662,15362],{"class":2069},[55,15664,13645],{"class":2043},[55,15666,15667],{"class":717},"findBatch",[55,15669,15670],{"class":2053},"($batchId);\n",[55,15672,15673],{"class":57,"line":71},[55,15674,6125],{"class":2053},[18,15676,15677,15678,525,15681,525,15684,15687,15688,532],{},"The returned JSON contains properties like ",[22,15679,15680],{},"totalJobs",[22,15682,15683],{},"pendingJobs",[22,15685,15686],{},"failedJobs",", and ",[22,15689,15690],{},"progress()",[343,15692,15694],{"id":15693},"adding-jobs-to-an-existing-batch","Adding Jobs to an Existing Batch",[18,15696,15697],{},"Sometimes you don’t know all jobs upfront. You can dispatch a “loader” job that adds more jobs to the batch:",[46,15699,15701],{"className":13615,"code":15700,"language":13609,"meta":51,"style":51},"$this->batch()->add([\n    new ImportCsv(301, 400),\n    // ...\n]);\n",[22,15702,15703,15720,15737,15741],{"__ignoreMap":51},[55,15704,15705,15707,15709,15711,15713,15715,15718],{"class":57,"line":58},[55,15706,15568],{"class":2069},[55,15708,13701],{"class":2043},[55,15710,15367],{"class":717},[55,15712,14514],{"class":2053},[55,15714,13701],{"class":2043},[55,15716,15717],{"class":717},"add",[55,15719,13651],{"class":2053},[55,15721,15722,15724,15726,15728,15731,15733,15735],{"class":57,"line":64},[55,15723,15374],{"class":2043},[55,15725,15377],{"class":2069},[55,15727,2054],{"class":2053},[55,15729,15730],{"class":2069},"301",[55,15732,525],{"class":2053},[55,15734,2124],{"class":2069},[55,15736,9631],{"class":2053},[55,15738,15739],{"class":57,"line":71},[55,15740,15186],{"class":711},[55,15742,15743],{"class":57,"line":77},[55,15744,15745],{"class":2053},"]);\n",[18,15747,15748],{},"Batching is ideal for tasks like importing large files, sending newsletters, or any workflow where you need to wait for multiple independent tasks to complete.",[13,15750,15752],{"id":15751},"unique-jobs-preventing-duplicate-work","Unique Jobs – Preventing Duplicate Work",[18,15754,15755],{},"There are scenarios where you absolutely do not want the same job to be queued twice while a previous instance is still running. For example, you might have a job that rebuilds a user’s search index. If the user triggers the rebuild multiple times in a row, you want only one to actually run.",[18,15757,15758,15759,15762],{},"Laravel provides the ",[22,15760,15761],{},"ShouldBeUnique"," interface for this.",[343,15764,15766],{"id":15765},"making-a-job-unique","Making a Job Unique",[18,15768,15769],{},"Implement the interface on your job class:",[46,15771,15773],{"className":13615,"code":15772,"language":13609,"meta":51,"style":51},"use Illuminate\\Contracts\\Queue\\ShouldBeUnique;\n\nclass UpdateSearchIndex implements ShouldQueue, ShouldBeUnique\n{\n    // ...\n}\n",[22,15774,15775,15784,15788,15805,15809,15813],{"__ignoreMap":51},[55,15776,15777,15779,15782],{"class":57,"line":58},[55,15778,13623],{"class":2043},[55,15780,15781],{"class":2069}," Illuminate\\Contracts\\Queue\\ShouldBeUnique",[55,15783,5761],{"class":2053},[55,15785,15786],{"class":57,"line":64},[55,15787,68],{"emptyLinePlaceholder":67},[55,15789,15790,15792,15795,15797,15800,15802],{"class":57,"line":71},[55,15791,14445],{"class":2043},[55,15793,15794],{"class":717}," UpdateSearchIndex",[55,15796,14451],{"class":2043},[55,15798,15799],{"class":717}," ShouldQueue",[55,15801,525],{"class":2053},[55,15803,15804],{"class":717},"ShouldBeUnique\n",[55,15806,15807],{"class":57,"line":77},[55,15808,4945],{"class":2053},[55,15810,15811],{"class":57,"line":83},[55,15812,15186],{"class":711},[55,15814,15815],{"class":57,"line":89},[55,15816,2005],{"class":2053},[18,15818,15819],{},"Now if you dispatch this job while another instance is still in the queue (or processing), the new dispatch will be ignored.",[343,15821,15823],{"id":15822},"defining-the-uniqueness-key","Defining the Uniqueness Key",[18,15825,15826,15827,15499],{},"Often you want uniqueness based on some model ID. Override the ",[22,15828,15829],{},"uniqueId()",[46,15831,15833],{"className":13615,"code":15832,"language":13609,"meta":51,"style":51},"public function uniqueId()\n{\n    return $this->user->id;\n}\n",[22,15834,15835,15846,15850,15867],{"__ignoreMap":51},[55,15836,15837,15839,15841,15844],{"class":57,"line":58},[55,15838,14487],{"class":2043},[55,15840,2047],{"class":2043},[55,15842,15843],{"class":717}," uniqueId",[55,15845,15555],{"class":2053},[55,15847,15848],{"class":57,"line":64},[55,15849,4945],{"class":2053},[55,15851,15852,15854,15857,15859,15862,15864],{"class":57,"line":71},[55,15853,2356],{"class":2043},[55,15855,15856],{"class":2069}," $this",[55,15858,13701],{"class":2043},[55,15860,15861],{"class":2053},"user",[55,15863,13701],{"class":2043},[55,15865,15866],{"class":2053},"id;\n",[55,15868,15869],{"class":57,"line":77},[55,15870,2005],{"class":2053},[18,15872,15873,15874,15877],{},"Now only one ",[22,15875,15876],{},"UpdateSearchIndex"," job per user ID will be queued at a time.",[343,15879,15881],{"id":15880},"uniqueness-timeout","Uniqueness Timeout",[18,15883,15884,15885,15888],{},"You can also specify how long the uniqueness lock should last using the ",[22,15886,15887],{},"UniqueFor"," attribute:",[46,15890,15892],{"className":13615,"code":15891,"language":13609,"meta":51,"style":51},"use Illuminate\\Queue\\Attributes\\UniqueFor;\n\n#[UniqueFor(3600)] // 1 hour\nclass UpdateSearchIndex implements ShouldQueue, ShouldBeUnique\n",[22,15893,15894,15903,15907,15924],{"__ignoreMap":51},[55,15895,15896,15898,15901],{"class":57,"line":58},[55,15897,13623],{"class":2043},[55,15899,15900],{"class":2069}," Illuminate\\Queue\\Attributes\\UniqueFor",[55,15902,5761],{"class":2053},[55,15904,15905],{"class":57,"line":64},[55,15906,68],{"emptyLinePlaceholder":67},[55,15908,15909,15912,15914,15916,15918,15921],{"class":57,"line":71},[55,15910,15911],{"class":2053},"#[",[55,15913,15887],{"class":2069},[55,15915,2054],{"class":2053},[55,15917,691],{"class":2069},[55,15919,15920],{"class":2053},")] ",[55,15922,15923],{"class":711},"// 1 hour\n",[55,15925,15926,15928,15930,15932,15934,15936],{"class":57,"line":77},[55,15927,14445],{"class":2043},[55,15929,15794],{"class":717},[55,15931,14451],{"class":2043},[55,15933,15799],{"class":717},[55,15935,525],{"class":2053},[55,15937,15804],{"class":717},[18,15939,15940],{},"If the job hasn’t finished within that time, another job with the same key can be dispatched.",[343,15942,15944],{"id":15943},"unique-until-processing","Unique Until Processing",[18,15946,15947,15948,15951],{},"By default, the lock is released after the job finishes (or fails). If you want the lock to be released as soon as processing begins (so that subsequent jobs can queue behind), implement ",[22,15949,15950],{},"ShouldBeUniqueUntilProcessing"," instead.",[18,15953,15954],{},"Unique jobs are a simple but powerful way to avoid redundant work.",[13,15956,15958],{"id":15957},"queue-priorities-handling-important-work-first","Queue Priorities – Handling Important Work First",[18,15960,15961],{},"When you have different types of jobs, you may want some to be processed before others. Laravel lets you assign jobs to different “queues” (like “high” and “low”) and then tell the worker which queues to process in which order.",[343,15963,15965],{"id":15964},"dispatching-to-a-specific-queue","Dispatching to a Specific Queue",[46,15967,15969],{"className":13615,"code":15968,"language":13609,"meta":51,"style":51},"ProcessPodcast::dispatch($podcast)->onQueue('high');\n",[22,15970,15971],{"__ignoreMap":51},[55,15972,15973,15975,15977,15979,15981,15983,15985,15987,15990],{"class":57,"line":58},[55,15974,14368],{"class":2069},[55,15976,13645],{"class":2043},[55,15978,14611],{"class":717},[55,15980,14614],{"class":2053},[55,15982,13701],{"class":2043},[55,15984,14619],{"class":717},[55,15986,2054],{"class":2053},[55,15988,15989],{"class":721},"'high'",[55,15991,2178],{"class":2053},[343,15993,15995],{"id":15994},"running-a-worker-with-priority","Running a Worker with Priority",[18,15997,15998],{},"Start the worker with a comma‑separated list of queues, ordered by priority:",[46,16000,16002],{"className":702,"code":16001,"language":704,"meta":51,"style":51},"php artisan queue:work --queue=high,low\n",[22,16003,16004],{"__ignoreMap":51},[55,16005,16006,16008,16010,16013],{"class":57,"line":58},[55,16007,13609],{"class":717},[55,16009,14753],{"class":721},[55,16011,16012],{"class":721}," queue:work",[55,16014,16015],{"class":2069}," --queue=high,low\n",[18,16017,16018,16019,16021,16022,16024,16025,16027,16028,532],{},"Now the worker will process all jobs from the ",[22,16020,14632],{}," queue before moving to the ",[22,16023,14636],{}," queue. If the ",[22,16026,14632],{}," queue is empty, it will process jobs from ",[22,16029,14636],{},[343,16031,16033],{"id":16032},"default-queue","Default Queue",[18,16035,16036,16037,16039,16040,16043],{},"You can set a default queue for a connection in ",[22,16038,14584],{}," under the ",[22,16041,16042],{},"queue"," key. For example:",[46,16045,16047],{"className":13615,"code":16046,"language":13609,"meta":51,"style":51},"'redis' => [\n    'driver' => 'redis',\n    'queue' => 'default',\n    // ...\n],\n",[22,16048,16049,16058,16070,16082,16086],{"__ignoreMap":51},[55,16050,16051,16054,16056],{"class":57,"line":58},[55,16052,16053],{"class":721},"'redis'",[55,16055,13659],{"class":2043},[55,16057,6851],{"class":2053},[55,16059,16060,16063,16065,16068],{"class":57,"line":64},[55,16061,16062],{"class":721},"    'driver'",[55,16064,13659],{"class":2043},[55,16066,16067],{"class":721}," 'redis'",[55,16069,2250],{"class":2053},[55,16071,16072,16075,16077,16080],{"class":57,"line":71},[55,16073,16074],{"class":721},"    'queue'",[55,16076,13659],{"class":2043},[55,16078,16079],{"class":721}," 'default'",[55,16081,2250],{"class":2053},[55,16083,16084],{"class":57,"line":77},[55,16085,15186],{"class":711},[55,16087,16088],{"class":57,"line":83},[55,16089,5473],{"class":2053},[18,16091,16092],{},"Priorities are essential when you have time‑sensitive jobs (like sending password reset emails) that should not wait behind bulk imports.",[13,16094,16096],{"id":16095},"testing-queues-keeping-your-tests-fast","Testing Queues – Keeping Your Tests Fast",[18,16098,16099,16100,16102,16103,16106],{},"When testing code that dispatches jobs, you don’t want to actually run the jobs. That would slow down tests and might have side effects. Laravel provides a ",[22,16101,14589],{}," facade with a ",[22,16104,16105],{},"fake()"," method that intercepts job dispatches.",[343,16108,16110],{"id":16109},"basic-fake-and-assertions","Basic Fake and Assertions",[46,16112,16114],{"className":13615,"code":16113,"language":13609,"meta":51,"style":51},"use Illuminate\\Support\\Facades\\Queue;\n\npublic function test_orders_are_shipped()\n{\n    Queue::fake();\n\n    // Call your code that dispatches a job...\n    // e.g., $this->post(‘/orders’, [...]);\n\n    Queue::assertPushed(ShipOrder::class);\n    Queue::assertPushedTimes(ShipOrder::class, 1);\n    Queue::assertPushedOn(‘shipping’, ShipOrder::class);\n}\n",[22,16115,16116,16125,16129,16140,16144,16156,16160,16165,16170,16174,16193,16214,16236],{"__ignoreMap":51},[55,16117,16118,16120,16123],{"class":57,"line":58},[55,16119,13623],{"class":2043},[55,16121,16122],{"class":2069}," Illuminate\\Support\\Facades\\Queue",[55,16124,5761],{"class":2053},[55,16126,16127],{"class":57,"line":64},[55,16128,68],{"emptyLinePlaceholder":67},[55,16130,16131,16133,16135,16138],{"class":57,"line":71},[55,16132,14487],{"class":2043},[55,16134,2047],{"class":2043},[55,16136,16137],{"class":717}," test_orders_are_shipped",[55,16139,15555],{"class":2053},[55,16141,16142],{"class":57,"line":77},[55,16143,4945],{"class":2053},[55,16145,16146,16149,16151,16154],{"class":57,"line":83},[55,16147,16148],{"class":2069},"    Queue",[55,16150,13645],{"class":2043},[55,16152,16153],{"class":717},"fake",[55,16155,7014],{"class":2053},[55,16157,16158],{"class":57,"line":89},[55,16159,68],{"emptyLinePlaceholder":67},[55,16161,16162],{"class":57,"line":95},[55,16163,16164],{"class":711},"    // Call your code that dispatches a job...\n",[55,16166,16167],{"class":57,"line":101},[55,16168,16169],{"class":711},"    // e.g., $this->post(‘/orders’, [...]);\n",[55,16171,16172],{"class":57,"line":107},[55,16173,68],{"emptyLinePlaceholder":67},[55,16175,16176,16178,16180,16183,16185,16188,16191],{"class":57,"line":113},[55,16177,16148],{"class":2069},[55,16179,13645],{"class":2043},[55,16181,16182],{"class":717},"assertPushed",[55,16184,2054],{"class":2053},[55,16186,16187],{"class":2069},"ShipOrder",[55,16189,16190],{"class":2043},"::class",[55,16192,2178],{"class":2053},[55,16194,16195,16197,16199,16202,16204,16206,16208,16210,16212],{"class":57,"line":119},[55,16196,16148],{"class":2069},[55,16198,13645],{"class":2043},[55,16200,16201],{"class":717},"assertPushedTimes",[55,16203,2054],{"class":2053},[55,16205,16187],{"class":2069},[55,16207,16190],{"class":2043},[55,16209,525],{"class":2053},[55,16211,15053],{"class":2069},[55,16213,2178],{"class":2053},[55,16215,16216,16218,16220,16223,16225,16228,16230,16232,16234],{"class":57,"line":125},[55,16217,16148],{"class":2069},[55,16219,13645],{"class":2043},[55,16221,16222],{"class":717},"assertPushedOn",[55,16224,2054],{"class":2053},[55,16226,16227],{"class":2069},"‘shipping’",[55,16229,525],{"class":2053},[55,16231,16187],{"class":2069},[55,16233,16190],{"class":2043},[55,16235,2178],{"class":2053},[55,16237,16238],{"class":57,"line":131},[55,16239,2005],{"class":2053},[18,16241,16242],{},"You can also pass a closure to make more specific assertions:",[46,16244,16246],{"className":13615,"code":16245,"language":13609,"meta":51,"style":51},"Queue::assertPushed(function (ShipOrder $job) use ($order) {\n    return $job->order->id === $order->id;\n});\n",[22,16247,16248,16272,16298],{"__ignoreMap":51},[55,16249,16250,16252,16254,16256,16258,16260,16262,16264,16267,16269],{"class":57,"line":58},[55,16251,14589],{"class":2069},[55,16253,13645],{"class":2043},[55,16255,16182],{"class":717},[55,16257,2054],{"class":2053},[55,16259,9734],{"class":2043},[55,16261,2093],{"class":2053},[55,16263,16187],{"class":2069},[55,16265,16266],{"class":2053}," $job) ",[55,16268,13623],{"class":2043},[55,16270,16271],{"class":2053}," ($order) {\n",[55,16273,16274,16276,16279,16281,16284,16286,16289,16291,16294,16296],{"class":57,"line":64},[55,16275,2356],{"class":2043},[55,16277,16278],{"class":2053}," $job",[55,16280,13701],{"class":2043},[55,16282,16283],{"class":2053},"order",[55,16285,13701],{"class":2043},[55,16287,16288],{"class":2053},"id ",[55,16290,6604],{"class":2043},[55,16292,16293],{"class":2053}," $order",[55,16295,13701],{"class":2043},[55,16297,15866],{"class":2053},[55,16299,16300],{"class":57,"line":71},[55,16301,6125],{"class":2053},[343,16303,16305],{"id":16304},"faking-only-specific-jobs","Faking Only Specific Jobs",[18,16307,16308,16309,5495],{},"If you want to let some jobs run normally while faking others, pass an array of class names to ",[22,16310,16105],{},[46,16312,16314],{"className":13615,"code":16313,"language":13609,"meta":51,"style":51},"Queue::fake([ShipOrder::class]); // only ShipOrder is faked, others run\n",[22,16315,16316],{"__ignoreMap":51},[55,16317,16318,16320,16322,16324,16326,16328,16330,16333],{"class":57,"line":58},[55,16319,14589],{"class":2069},[55,16321,13645],{"class":2043},[55,16323,16153],{"class":717},[55,16325,9871],{"class":2053},[55,16327,16187],{"class":2069},[55,16329,16190],{"class":2043},[55,16331,16332],{"class":2053},"]); ",[55,16334,16335],{"class":711},"// only ShipOrder is faked, others run\n",[343,16337,16339],{"id":16338},"testing-job-chaining-and-batching","Testing Job Chaining and Batching",[18,16341,16342,16343,16346],{},"For chains and batches, use ",[22,16344,16345],{},"Bus::fake()"," instead:",[46,16348,16350],{"className":13615,"code":16349,"language":13609,"meta":51,"style":51},"Bus::fake();\n\n// Dispatch chain or batch...\n\nBus::assertChained([ProcessPodcast::class, OptimizePodcast::class]);\nBus::assertBatched(function ($batch) {\n    return $batch->jobs->count() === 3;\n});\n",[22,16351,16352,16362,16366,16371,16375,16399,16414,16441],{"__ignoreMap":51},[55,16353,16354,16356,16358,16360],{"class":57,"line":58},[55,16355,15324],{"class":2069},[55,16357,13645],{"class":2043},[55,16359,16153],{"class":717},[55,16361,7014],{"class":2053},[55,16363,16364],{"class":57,"line":64},[55,16365,68],{"emptyLinePlaceholder":67},[55,16367,16368],{"class":57,"line":71},[55,16369,16370],{"class":711},"// Dispatch chain or batch...\n",[55,16372,16373],{"class":57,"line":77},[55,16374,68],{"emptyLinePlaceholder":67},[55,16376,16377,16379,16381,16384,16386,16388,16390,16392,16395,16397],{"class":57,"line":83},[55,16378,15324],{"class":2069},[55,16380,13645],{"class":2043},[55,16382,16383],{"class":717},"assertChained",[55,16385,9871],{"class":2053},[55,16387,14368],{"class":2069},[55,16389,16190],{"class":2043},[55,16391,525],{"class":2053},[55,16393,16394],{"class":2069},"OptimizePodcast",[55,16396,16190],{"class":2043},[55,16398,15745],{"class":2053},[55,16400,16401,16403,16405,16408,16410,16412],{"class":57,"line":89},[55,16402,15324],{"class":2069},[55,16404,13645],{"class":2043},[55,16406,16407],{"class":717},"assertBatched",[55,16409,2054],{"class":2053},[55,16411,9734],{"class":2043},[55,16413,15437],{"class":2053},[55,16415,16416,16418,16421,16423,16426,16428,16431,16434,16436,16439],{"class":57,"line":95},[55,16417,2356],{"class":2043},[55,16419,16420],{"class":2053}," $batch",[55,16422,13701],{"class":2043},[55,16424,16425],{"class":2053},"jobs",[55,16427,13701],{"class":2043},[55,16429,16430],{"class":717},"count",[55,16432,16433],{"class":2053},"() ",[55,16435,6604],{"class":2043},[55,16437,16438],{"class":2069}," 3",[55,16440,5761],{"class":2053},[55,16442,16443],{"class":57,"line":101},[55,16444,6125],{"class":2053},[18,16446,16447],{},"Testing with fakes ensures your tests remain fast and focused on the dispatching logic, not the actual job execution.",[13,16449,16451],{"id":16450},"common-pitfalls-and-best-practices","Common Pitfalls and Best Practices",[18,16453,16454],{},"Even with a solid understanding, beginners often stumble on a few points. Here are some tips to avoid trouble.",[343,16456,16458],{"id":16457},"_1-restart-workers-after-deployments","1. Restart Workers After Deployments",[18,16460,16461,16462,16464],{},"Workers keep the application in memory. If you change code (e.g., in ",[22,16463,14554],{},"), the running worker won’t see the changes. Always run:",[46,16466,16468],{"className":702,"code":16467,"language":704,"meta":51,"style":51},"php artisan queue:restart\n",[22,16469,16470],{"__ignoreMap":51},[55,16471,16472,16474,16476],{"class":57,"line":58},[55,16473,13609],{"class":717},[55,16475,14753],{"class":721},[55,16477,16478],{"class":721}," queue:restart\n",[18,16480,16481],{},"after deployment. The command sends a signal that causes workers to exit gracefully after their current job.",[343,16483,16485,16486,16489],{"id":16484},"_2-set-appropriate-retry_after-and-timeout-values","2. Set Appropriate ",[22,16487,16488],{},"retry_after"," and Timeout Values",[18,16491,16492,16493,16495,16496,16498],{},"In ",[22,16494,14584],{},", each connection has a ",[22,16497,16488],{}," value. This is how long the queue driver waits before making a job available again if it’s not completed. If your job takes longer than this, the driver will think it failed and retry it, leading to duplicate processing.",[1396,16500,16501,16507],{},[1399,16502,16503,16504,16506],{},"Ensure ",[22,16505,16488],{}," is longer than your longest‑running job.",[1399,16508,16509,16510,16513,16514,532],{},"Also set the worker timeout with ",[22,16511,16512],{},"--timeout"," – this should be less than ",[22,16515,16488],{},[18,16517,16518],{},"Example:",[1396,16520,16521],{},[1399,16522,16523,16524,321,16527,532],{},"Job takes up to 90 seconds → set ",[22,16525,16526],{},"retry_after = 120",[22,16528,16529],{},"--timeout=100",[343,16531,16533],{"id":16532},"_3-use-the-database-queue-driver-for-development","3. Use the Database Queue Driver for Development",[18,16535,16536,16537,321,16540,16543,16544,16546],{},"The database driver is the easiest to set up and inspect. Run ",[22,16538,16539],{},"php artisan make:queue-table",[22,16541,16542],{},"php artisan migrate",". You can then look at the ",[22,16545,16425],{}," table to see what’s pending.",[343,16548,16550],{"id":16549},"_4-handle-failures-gracefully","4. Handle Failures Gracefully",[18,16552,16553,16554,16556],{},"Always define a ",[22,16555,14826],{}," method on jobs that might need cleanup or user notification:",[46,16558,16560],{"className":13615,"code":16559,"language":13609,"meta":51,"style":51},"public function failed(Throwable $exception): void\n{\n    Log::error(‘Job failed: ‘ . $exception->getMessage());\n    $this->podcast->update([‘status’ => ‘failed’]);\n}\n",[22,16561,16562,16583,16587,16620,16647],{"__ignoreMap":51},[55,16563,16564,16566,16568,16571,16573,16576,16579,16581],{"class":57,"line":58},[55,16565,14487],{"class":2043},[55,16567,2047],{"class":2043},[55,16569,16570],{"class":717}," failed",[55,16572,2054],{"class":2053},[55,16574,16575],{"class":2069},"Throwable",[55,16577,16578],{"class":2053}," $exception)",[55,16580,5495],{"class":2043},[55,16582,14519],{"class":2043},[55,16584,16585],{"class":57,"line":64},[55,16586,4945],{"class":2053},[55,16588,16589,16592,16594,16596,16598,16601,16603,16605,16607,16610,16613,16615,16618],{"class":57,"line":71},[55,16590,16591],{"class":2069},"    Log",[55,16593,13645],{"class":2043},[55,16595,5909],{"class":717},[55,16597,2054],{"class":2053},[55,16599,16600],{"class":2069},"‘Job",[55,16602,16570],{"class":2069},[55,16604,3323],{"class":2053},[55,16606,15631],{"class":2069},[55,16608,16609],{"class":2043}," .",[55,16611,16612],{"class":2053}," $exception",[55,16614,13701],{"class":2043},[55,16616,16617],{"class":717},"getMessage",[55,16619,6343],{"class":2053},[55,16621,16622,16625,16627,16630,16632,16635,16637,16640,16642,16645],{"class":57,"line":77},[55,16623,16624],{"class":2069},"    $this",[55,16626,13701],{"class":2043},[55,16628,16629],{"class":2053},"podcast",[55,16631,13701],{"class":2043},[55,16633,16634],{"class":717},"update",[55,16636,9871],{"class":2053},[55,16638,16639],{"class":2069},"‘status’",[55,16641,13659],{"class":2043},[55,16643,16644],{"class":2069}," ‘failed’",[55,16646,15745],{"class":2053},[55,16648,16649],{"class":57,"line":83},[55,16650,2005],{"class":2053},[343,16652,16654],{"id":16653},"_5-keep-jobs-small-and-focused","5. Keep Jobs Small and Focused",[18,16656,16657],{},"A job should do one thing. If you need multiple steps, consider chaining jobs or using batches. Large, complex jobs are harder to debug and retry.",[343,16659,16661],{"id":16660},"_6-monitor-your-queues","6. Monitor Your Queues",[18,16663,16664,16665,16668],{},"Schedule ",[22,16666,16667],{},"queue:monitor"," to detect when a queue gets too long. Combine it with notifications to stay ahead of issues.",[343,16670,16672],{"id":16671},"_7-beware-of-database-transactions","7. Beware of Database Transactions",[18,16674,16675,16676,15499],{},"If you dispatch a job inside a database transaction, the job may run before the transaction is committed, causing it to see stale data. Use the ",[22,16677,16678],{},"afterCommit()",[46,16680,16682],{"className":13615,"code":16681,"language":13609,"meta":51,"style":51},"ProcessPodcast::dispatch($podcast)->afterCommit();\n",[22,16683,16684],{"__ignoreMap":51},[55,16685,16686,16688,16690,16692,16694,16696,16699],{"class":57,"line":58},[55,16687,14368],{"class":2069},[55,16689,13645],{"class":2043},[55,16691,14611],{"class":717},[55,16693,14614],{"class":2053},[55,16695,13701],{"class":2043},[55,16697,16698],{"class":717},"afterCommit",[55,16700,7014],{"class":2053},[18,16702,16703,16704,16707,16708,532],{},"Or set the ",[22,16705,16706],{},"after_commit"," option in your queue connection configuration to ",[22,16709,9610],{},[13,16711,16713],{"id":16712},"conclusion","Conclusion",[18,16715,16716],{},"Queues are one of the most powerful features of Laravel. They turn a slow, synchronous application into a fast, responsive one. Start simple – use the database driver, create a few jobs, run a worker manually, and then gradually explore advanced features like job middleware, batching, and custom retry strategies.",[18,16718,16719],{},"Here are some helpful tips:",[1396,16721,16722,16730,16737,16744],{},[1399,16723,16724,16725,321,16727,16729],{},"Set up the database queue driver – run ",[22,16726,16539],{},[22,16728,16542],{}," to create the jobs table. This is the easiest driver to start with.",[1399,16731,16732,16733,16736],{},"Use Supervisor in production to keep your workers alive. A typical ",[22,16734,16735],{},"laravel-worker.conf"," file runs 8 workers and restarts them automatically.",[1399,16738,16739,16740,16743],{},"Monitor your queues – Laravel can dispatch a ",[22,16741,16742],{},"QueueBusy"," event when a queue exceeds a certain size. Use it to send alerts.",[1399,16745,16746,16747,16750],{},"Testing – Use ",[22,16748,16749],{},"Queue::fake()"," in your tests to prevent real queue processing, and assert that jobs were dispatched.",[18,16752,4723],{},[4725,16754,16755],{},"html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}",{"title":51,"searchDepth":64,"depth":64,"links":16757},[16758,16763,16771,16776,16783,16789,16794,16799,16809],{"id":14283,"depth":64,"text":14284,"children":16759},[16760,16761,16762],{"id":14287,"depth":71,"text":14288},{"id":14297,"depth":71,"text":14298},{"id":14327,"depth":71,"text":14328},{"id":14351,"depth":64,"text":14352,"children":16764},[16765,16766,16767,16768,16769,16770],{"id":14361,"depth":71,"text":14362},{"id":14558,"depth":71,"text":14559},{"id":14640,"depth":71,"text":14641},{"id":14728,"depth":71,"text":14729},{"id":14774,"depth":71,"text":14775},{"id":14830,"depth":71,"text":14831},{"id":14871,"depth":64,"text":14872,"children":16772},[16773,16774,16775],{"id":14895,"depth":71,"text":14896},{"id":15141,"depth":71,"text":15142},{"id":15245,"depth":71,"text":15246},{"id":15280,"depth":64,"text":15281,"children":16777},[16778,16779,16780,16781,16782],{"id":15287,"depth":71,"text":15288},{"id":15317,"depth":71,"text":15318},{"id":15491,"depth":71,"text":15492},{"id":15608,"depth":71,"text":15609},{"id":15693,"depth":71,"text":15694},{"id":15751,"depth":64,"text":15752,"children":16784},[16785,16786,16787,16788],{"id":15765,"depth":71,"text":15766},{"id":15822,"depth":71,"text":15823},{"id":15880,"depth":71,"text":15881},{"id":15943,"depth":71,"text":15944},{"id":15957,"depth":64,"text":15958,"children":16790},[16791,16792,16793],{"id":15964,"depth":71,"text":15965},{"id":15994,"depth":71,"text":15995},{"id":16032,"depth":71,"text":16033},{"id":16095,"depth":64,"text":16096,"children":16795},[16796,16797,16798],{"id":16109,"depth":71,"text":16110},{"id":16304,"depth":71,"text":16305},{"id":16338,"depth":71,"text":16339},{"id":16450,"depth":64,"text":16451,"children":16800},[16801,16802,16804,16805,16806,16807,16808],{"id":16457,"depth":71,"text":16458},{"id":16484,"depth":71,"text":16803},"2. Set Appropriate retry_after and Timeout Values",{"id":16532,"depth":71,"text":16533},{"id":16549,"depth":71,"text":16550},{"id":16653,"depth":71,"text":16654},{"id":16660,"depth":71,"text":16661},{"id":16671,"depth":71,"text":16672},{"id":16712,"depth":64,"text":16713},"Backend","/blog-covers/laravel-queues-explained-simply.png","2026-03-31","Learn how Laravel queues work and how to use them effectively in your applications using Laravel 13.",false,{},"/posts/laravel-queues-explained-simply",{"title":14278,"description":16813},{"loc":16816,"lastmod":16812},"posts/laravel-queues-explained-simply",[16821,16822,16823,16824],"laravel","queues","jobs processing","backend","A7itBn-psCn1V20fz0tcYtCGOjHTHKL1VRTT6Y793gQ",{"id":16827,"title":16828,"author":8,"body":16829,"category":16810,"cover":17542,"date":17543,"description":17544,"extension":4794,"featured":16814,"meta":17545,"navigation":67,"path":17546,"published":67,"readingTime":17547,"seo":17548,"sitemap":17549,"stem":17550,"tags":17551,"updated":17556,"__hash__":17557},"posts/posts/authentication-in-backend-development.md","Complete Conceptual Guide to Authentication Fundamentals in Backend Development",{"type":10,"value":16830,"toc":17526},[16831,16835,16839,16846,16852,16855,16858,16878,16883,16887,16891,16898,16907,16911,16914,16928,16934,16938,16941,16958,16964,16968,16971,16978,16992,16998,17003,17007,17010,17014,17037,17041,17067,17069,17072,17489,17492,17518,17521,17523],[13,16832,16834],{"id":16833},"overview-of-authentication-fundamentals-in-backend-development","Overview of Authentication Fundamentals in Backend Development",[343,16836,16838],{"id":16837},"what-is-authentication-and-why-is-it-important","What is Authentication and Why is it Important?",[18,16840,16841,16842,16845],{},"In the simplest terms, ",[518,16843,16844],{},"Authentication (AuthN) is the process of verifying who a user is",". When you log into a website or app, the backend needs a way to confirm that you are indeed the person you claim to be before it gives you access to your private data stored in the system.",[18,16847,16848,16849,532],{},"Without authentication, the internet as we know it couldn't exist. Just imagine if anyone could read your emails, transfer money from your bank account, or post as you on social media just by typing in your username. We can't do that, right? It's because ",[518,16850,16851],{},"authentication creates a secure boundary between public data and private information",[18,16853,16854],{},"Let's consider a real-world example: The Airport",[18,16856,16857],{},"What happens at an airport:",[1396,16859,16860,16866,16872],{},[1399,16861,16862,16865],{},[518,16863,16864],{},"Identification",": You show your Passport (the \"Credentials\").",[1399,16867,16868,16871],{},[518,16869,16870],{},"Authentication",": The TSA agent checks the passport against your face and their database to verify it's real and it's yours.",[1399,16873,16874,16877],{},[518,16875,16876],{},"Authorization"," (The next step): Your Boarding Pass determines which gate you can go to. (Note: Authentication is \"Who are you?\", Authorization is \"What are you allowed to do?\").",[300,16879,16880],{},[18,16881,16882],{},"Authorization is a separate but related concept. It determines what you can do after you've been authenticated. We'll discuss it in other posts.",[13,16884,16886],{"id":16885},"fundamental-concepts-of-authentication-in-backend-development","Fundamental Concepts of Authentication in Backend Development",[343,16888,16890],{"id":16889},"the-stateless-nature-of-the-web","The \"Stateless\" Nature of the Web",[18,16892,16893,16894,16897],{},"To understand backend authentication, you must first understand that ",[518,16895,16896],{},"the web is stateless",". This means that every time you click a link or refresh a page, the server \"forgets\" who you are. It treats every single request as if it’s seeing you for the very first time.",[18,16899,16900,16901,321,16904,532],{},"To fix this, we use two main methods to stay logged in: ",[518,16902,16903],{},"Sessions",[518,16905,16906],{},"Tokens (like JWT)",[343,16908,16910],{"id":16909},"sessions-the-coat-check-analogy","Sessions (The \"Coat Check\" Analogy)",[18,16912,16913],{},"In a Session-based system:",[2527,16915,16916,16919,16922,16925],{},[1399,16917,16918],{},"You provide your password.",[1399,16920,16921],{},"The server verifies it and creates a \"Session File\" in its memory (like a coat check room).",[1399,16923,16924],{},"The server gives you a Session ID (the little plastic tag).",[1399,16926,16927],{},"Every time you ask for a page, you show that ID. The server looks at its \"coat room,\" finds your session, and remembers who you are.",[18,16929,16930],{},[4835,16931],{"alt":16932,"src":16933},"Session-based Authentication","/blog-post-images/authentication-in-backend/session-based-auth-flow-diagram.png",[343,16935,16937],{"id":16936},"tokens-jwt-the-member-badge-analogy","Tokens / JWT (The \"Member Badge\" Analogy)",[18,16939,16940],{},"In a Token-based system (like JSON Web Tokens or JWT):",[2527,16942,16943,16945,16952,16955],{},[1399,16944,16918],{},[1399,16946,16947,16948,16951],{},"Instead of storing something on the server, the server gives you a ",[518,16949,16950],{},"signed, encrypted badge"," (the Token).",[1399,16953,16954],{},"This badge contains your info (User ID, Expiration date).",[1399,16956,16957],{},"Because the badge is digitally \"sealed\" by the server, the server doesn't need to look anything up in a database. It just looks at the badge, verifies the seal hasn't been tampered with, and lets you in.",[18,16959,16960],{},[4835,16961],{"alt":16962,"src":16963},"Token-based Authentication","/blog-post-images/authentication-in-backend/token-based-auth-flow-diagram.png",[343,16965,16967],{"id":16966},"hashing-never-store-plain-text-passwords","Hashing: Never Store Plain Text Passwords",[18,16969,16970],{},"As a backend engineer, you should never store a user's actual password (e.g., \"P@ssword153\") in your database. If a hacker steals it, they have everyone's login.",[18,16972,16973,16974,16977],{},"Instead, we use ",[518,16975,16976],{},"Hashing",". Always. A hash function turns a password into a long string of gibberish.",[1396,16979,16980,16986,16989],{},[1399,16981,16982,16983],{},"\"P@ssword123\" becomes ",[22,16984,16985],{},"$2b$10$n9jB0...",[1399,16987,16988],{},"It is a one-way street: You can't turn the gibberish back into the actual password.",[1399,16990,16991],{},"When the user logs in later, you hash the password they just typed and see if the gibberish matches the gibberish in the database.",[18,16993,16994],{},[4835,16995],{"alt":16996,"src":16997},"Hashing Passwords","/blog-post-images/authentication-in-backend/password-hashing-flow-diagram.png",[300,16999,17000],{},[18,17001,17002],{},"Salting is an additional security measure where you add a random string to the password before hashing it, making it even harder for hackers to crack. Read more about it.",[13,17004,17006],{"id":17005},"other-terms-in-authentication","Other Terms in Authentication",[18,17008,17009],{},"As you grow as a developer, you will encounter different flavors of authentication depending on whether a human or a machine is talking to your server.",[343,17011,17013],{"id":17012},"common-http-schemes","Common HTTP Schemes",[1396,17015,17016,17022,17031],{},[1399,17017,17018,17021],{},[518,17019,17020],{},"Basic Auth",": The simplest form. The username and password are combined, encoded in Base64, and sent in the header. It's like writing your name and password on the outside of the envelope. (Only safe over HTTPS!).",[1399,17023,17024,17027,17028],{},[518,17025,17026],{},"Bearer Token",": A category where the sender says, \"I am the bearer of this secret token; give me access.\" ",[518,17029,17030],{},"JWTs are the most common type of Bearer token.",[1399,17032,17033,17036],{},[518,17034,17035],{},"API Keys",": A long, unique string (like sk_live_678...) given to a developer. It’s like a permanent VIP pass for a specific software tool to talk to your API.",[343,17038,17040],{"id":17039},"advanced-modern-auth","Advanced & Modern Auth",[1396,17042,17043,17049,17055,17061],{},[1399,17044,17045,17048],{},[518,17046,17047],{},"OAuth 2.0",": This isn't just a login; it's a \"delegation\" framework. It allows one site to access data from another (e.g., \"Log in with Google\"). It's like giving a valet a \"valet key\" that starts the car but can't open the trunk.",[1399,17050,17051,17054],{},[518,17052,17053],{},"Passwordless Authentication",": Instead of a password, the server sends a \"Magic Link\" to your email or a code to your phone (OTP). The server verifies you by checking if you have the keys to your own house (your email/phone).",[1399,17056,17057,17060],{},[518,17058,17059],{},"Personal Access Tokens (PAT)",": Used primarily by developers (like on GitHub) to access APIs from the command line. They act like passwords but can be scoped to specific tasks and revoked easily.",[1399,17062,17063,17066],{},[518,17064,17065],{},"mTLS (Mutual TLS)",": High-security auth used between two servers. Not only does the client verify the server's identity, but the server also requires the client to present its own digital certificate (X.509 Certificate). It's like two secret agents showing each other their badges simultaneously before speaking. No credentials transmitted over the network, making it very secure. Its use case includes microservice-to-microservice communication.",[13,17068,16713],{"id":16712},[18,17070,17071],{},"So we now know how backend authentication works conceptually. Confusion is super common between various terms and strategies in backend authentication. The following structure would help you understand the relationships between them:",[46,17073,17075],{"className":702,"code":17074,"language":704,"meta":51,"style":51},"Authentication\n│\n├── 1. Identity Verification (Login Phase)\n│   ├── Username + Password\n│   ├── OAuth Login (Google, GitHub, etc.)\n│   └── Multi-Factor Authentication (OTP, 2FA)\n│\n├── 2. Session Management (After Login)\n│   │\n│   ├── A. Stateful (Server stores session)\n│   │   ├── Session ID (stored in DB / memory)\n│   │   └── Cookie-based auth\n│   │\n│   └── B. Stateless (Server stores nothing)\n│       │\n│       ├── Token-Based Authentication\n│       │   │\n│       │   ├── Bearer Token  ← (IMPORTANT)\n│       │   │   ├── JWT (JSON Web Token)\n│       │   │   ├── Personal Access Token (PAT)\n│       │   │   └── OAuth Access Token\n│       │   │\n│       │   └── API Keys (simpler tokens)\n│\n└── 3. Authorization (Permissions)\n    ├── Roles (Admin, User)\n    └── Scopes (read, write, delete)\n",[22,17076,17077,17082,17087,17109,17126,17149,17170,17174,17195,17202,17225,17255,17269,17275,17296,17303,17316,17325,17345,17368,17389,17406,17414,17436,17440,17454,17470],{"__ignoreMap":51},[55,17078,17079],{"class":57,"line":58},[55,17080,17081],{"class":717},"Authentication\n",[55,17083,17084],{"class":57,"line":64},[55,17085,17086],{"class":717},"│\n",[55,17088,17089,17092,17095,17098,17101,17104,17107],{"class":57,"line":71},[55,17090,17091],{"class":717},"├──",[55,17093,17094],{"class":721}," 1.",[55,17096,17097],{"class":721}," Identity",[55,17099,17100],{"class":721}," Verification",[55,17102,17103],{"class":2053}," (Login ",[55,17105,17106],{"class":721},"Phase",[55,17108,14142],{"class":2053},[55,17110,17111,17114,17117,17120,17123],{"class":57,"line":77},[55,17112,17113],{"class":717},"│",[55,17115,17116],{"class":721},"   ├──",[55,17118,17119],{"class":721}," Username",[55,17121,17122],{"class":721}," +",[55,17124,17125],{"class":721}," Password\n",[55,17127,17128,17130,17132,17135,17138,17141,17144,17147],{"class":57,"line":83},[55,17129,17113],{"class":717},[55,17131,17116],{"class":721},[55,17133,17134],{"class":721}," OAuth",[55,17136,17137],{"class":721}," Login",[55,17139,17140],{"class":2053}," (Google, ",[55,17142,17143],{"class":721},"GitHub,",[55,17145,17146],{"class":721}," etc.",[55,17148,14142],{"class":2053},[55,17150,17151,17153,17156,17159,17162,17165,17168],{"class":57,"line":89},[55,17152,17113],{"class":717},[55,17154,17155],{"class":721},"   └──",[55,17157,17158],{"class":721}," Multi-Factor",[55,17160,17161],{"class":721}," Authentication",[55,17163,17164],{"class":2053}," (OTP, ",[55,17166,17167],{"class":721},"2FA",[55,17169,14142],{"class":2053},[55,17171,17172],{"class":57,"line":95},[55,17173,17086],{"class":717},[55,17175,17176,17178,17181,17184,17187,17190,17193],{"class":57,"line":101},[55,17177,17091],{"class":717},[55,17179,17180],{"class":721}," 2.",[55,17182,17183],{"class":721}," Session",[55,17185,17186],{"class":721}," Management",[55,17188,17189],{"class":2053}," (After ",[55,17191,17192],{"class":721},"Login",[55,17194,14142],{"class":2053},[55,17196,17197,17199],{"class":57,"line":107},[55,17198,17113],{"class":717},[55,17200,17201],{"class":721},"   │\n",[55,17203,17204,17206,17208,17211,17214,17217,17220,17223],{"class":57,"line":113},[55,17205,17113],{"class":717},[55,17207,17116],{"class":721},[55,17209,17210],{"class":721}," A.",[55,17212,17213],{"class":721}," Stateful",[55,17215,17216],{"class":2053}," (Server ",[55,17218,17219],{"class":721},"stores",[55,17221,17222],{"class":721}," session",[55,17224,14142],{"class":2053},[55,17226,17227,17229,17232,17234,17236,17239,17242,17245,17248,17250,17253],{"class":57,"line":119},[55,17228,17113],{"class":717},[55,17230,17231],{"class":721},"   │",[55,17233,17116],{"class":721},[55,17235,17183],{"class":721},[55,17237,17238],{"class":721}," ID",[55,17240,17241],{"class":2053}," (stored ",[55,17243,17244],{"class":721},"in",[55,17246,17247],{"class":721}," DB",[55,17249,11490],{"class":721},[55,17251,17252],{"class":721}," memory",[55,17254,14142],{"class":2053},[55,17256,17257,17259,17261,17263,17266],{"class":57,"line":125},[55,17258,17113],{"class":717},[55,17260,17231],{"class":721},[55,17262,17155],{"class":721},[55,17264,17265],{"class":721}," Cookie-based",[55,17267,17268],{"class":721}," auth\n",[55,17270,17271,17273],{"class":57,"line":131},[55,17272,17113],{"class":717},[55,17274,17201],{"class":721},[55,17276,17277,17279,17281,17284,17287,17289,17291,17294],{"class":57,"line":137},[55,17278,17113],{"class":717},[55,17280,17155],{"class":721},[55,17282,17283],{"class":721}," B.",[55,17285,17286],{"class":721}," Stateless",[55,17288,17216],{"class":2053},[55,17290,17219],{"class":721},[55,17292,17293],{"class":721}," nothing",[55,17295,14142],{"class":2053},[55,17297,17298,17300],{"class":57,"line":143},[55,17299,17113],{"class":717},[55,17301,17302],{"class":721},"       │\n",[55,17304,17305,17307,17310,17313],{"class":57,"line":149},[55,17306,17113],{"class":717},[55,17308,17309],{"class":721},"       ├──",[55,17311,17312],{"class":721}," Token-Based",[55,17314,17315],{"class":721}," Authentication\n",[55,17317,17318,17320,17323],{"class":57,"line":441},[55,17319,17113],{"class":717},[55,17321,17322],{"class":721},"       │",[55,17324,17201],{"class":721},[55,17326,17327,17329,17331,17333,17336,17339,17342],{"class":57,"line":447},[55,17328,17113],{"class":717},[55,17330,17322],{"class":721},[55,17332,17116],{"class":721},[55,17334,17335],{"class":721}," Bearer",[55,17337,17338],{"class":721}," Token",[55,17340,17341],{"class":721},"  ←",[55,17343,17344],{"class":2053}," (IMPORTANT)\n",[55,17346,17347,17349,17351,17353,17355,17358,17361,17364,17366],{"class":57,"line":453},[55,17348,17113],{"class":717},[55,17350,17322],{"class":721},[55,17352,17231],{"class":721},[55,17354,17116],{"class":721},[55,17356,17357],{"class":721}," JWT",[55,17359,17360],{"class":2053}," (JSON ",[55,17362,17363],{"class":721},"Web",[55,17365,17338],{"class":721},[55,17367,14142],{"class":2053},[55,17369,17370,17372,17374,17376,17378,17381,17384,17386],{"class":57,"line":459},[55,17371,17113],{"class":717},[55,17373,17322],{"class":721},[55,17375,17231],{"class":721},[55,17377,17116],{"class":721},[55,17379,17380],{"class":721}," Personal",[55,17382,17383],{"class":721}," Access",[55,17385,17338],{"class":721},[55,17387,17388],{"class":2053}," (PAT)\n",[55,17390,17391,17393,17395,17397,17399,17401,17403],{"class":57,"line":464},[55,17392,17113],{"class":717},[55,17394,17322],{"class":721},[55,17396,17231],{"class":721},[55,17398,17155],{"class":721},[55,17400,17134],{"class":721},[55,17402,17383],{"class":721},[55,17404,17405],{"class":721}," Token\n",[55,17407,17408,17410,17412],{"class":57,"line":470},[55,17409,17113],{"class":717},[55,17411,17322],{"class":721},[55,17413,17201],{"class":721},[55,17415,17416,17418,17420,17422,17425,17428,17431,17434],{"class":57,"line":475},[55,17417,17113],{"class":717},[55,17419,17322],{"class":721},[55,17421,17155],{"class":721},[55,17423,17424],{"class":721}," API",[55,17426,17427],{"class":721}," Keys",[55,17429,17430],{"class":2053}," (simpler ",[55,17432,17433],{"class":721},"tokens",[55,17435,14142],{"class":2053},[55,17437,17438],{"class":57,"line":481},[55,17439,17086],{"class":717},[55,17441,17442,17445,17448,17451],{"class":57,"line":486},[55,17443,17444],{"class":717},"└──",[55,17446,17447],{"class":721}," 3.",[55,17449,17450],{"class":721}," Authorization",[55,17452,17453],{"class":2053}," (Permissions)\n",[55,17455,17456,17459,17462,17465,17468],{"class":57,"line":492},[55,17457,17458],{"class":717},"    ├──",[55,17460,17461],{"class":721}," Roles",[55,17463,17464],{"class":2053}," (Admin, ",[55,17466,17467],{"class":721},"User",[55,17469,14142],{"class":2053},[55,17471,17472,17475,17478,17481,17484,17487],{"class":57,"line":497},[55,17473,17474],{"class":717},"    └──",[55,17476,17477],{"class":721}," Scopes",[55,17479,17480],{"class":2053}," (read, ",[55,17482,17483],{"class":721},"write,",[55,17485,17486],{"class":721}," delete",[55,17488,14142],{"class":2053},[18,17490,17491],{},"Choosing the right strategy for your use case is very important. Here’s a quick guide:",[1396,17493,17494,17500,17506,17512],{},[1399,17495,17496,17499],{},[518,17497,17498],{},"For user‑facing web apps",": Session-based (with shared session store) or token-based (with careful CSRF/XSS protection).",[1399,17501,17502,17505],{},[518,17503,17504],{},"For APIs and mobile apps",": Token-based (JWT) with short-lived tokens and refresh tokens.",[1399,17507,17508,17511],{},[518,17509,17510],{},"For third‑party integrations",": OAuth 2.0/OIDC.",[1399,17513,17514,17517],{},[518,17515,17516],{},"For machine‑to‑machine",": API keys or mTLS.",[18,17519,17520],{},"Hybrid approaches are also common, where you might use sessions for the web app and JWTs for API access. Always consider the security implications of your choice and implement best practices to protect your users' data.",[18,17522,4723],{},[4725,17524,17525],{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}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);}",{"title":51,"searchDepth":64,"depth":64,"links":17527},[17528,17531,17537,17541],{"id":16833,"depth":64,"text":16834,"children":17529},[17530],{"id":16837,"depth":71,"text":16838},{"id":16885,"depth":64,"text":16886,"children":17532},[17533,17534,17535,17536],{"id":16889,"depth":71,"text":16890},{"id":16909,"depth":71,"text":16910},{"id":16936,"depth":71,"text":16937},{"id":16966,"depth":71,"text":16967},{"id":17005,"depth":64,"text":17006,"children":17538},[17539,17540],{"id":17012,"depth":71,"text":17013},{"id":17039,"depth":71,"text":17040},{"id":16712,"depth":64,"text":16713},"/blog-covers/authentication-in-backend-development.png","2026-03-28","Learn the core concepts of authentication in backend development, including JWT, OAuth, and session management.",{},"/posts/authentication-in-backend-development","10 min read",{"title":16828,"description":17544},{"loc":17546,"lastmod":17543},"posts/authentication-in-backend-development",[17552,16824,17553,17554,17555],"authentication","jwt","oauth","session-management","2026-03-24","zrSM0ISTJlEZC-_I088K6NTmp7koqqlebV-92ND_PaE",{"id":17559,"title":17560,"author":13315,"body":17561,"category":20199,"cover":20200,"date":20201,"description":20202,"extension":4794,"featured":67,"meta":20203,"navigation":67,"path":20204,"published":67,"readingTime":20205,"seo":20206,"sitemap":20207,"stem":20208,"tags":20209,"updated":20201,"__hash__":20213},"posts/posts/laravel-13-deep-dive.md","Laravel 13: A Deep Technical Dive Into What Actually Changed Under the Hood",{"type":10,"value":17562,"toc":20174},[17563,17566,17569,17573,17588,17591,17595,17602,17609,17613,17623,17796,17799,17939,17942,17951,17961,18002,18005,18009,18090,18094,18109,18325,18335,18347,18412,18416,18423,18612,18624,18628,18765,18772,18776,18921,18927,18931,19203,19216,19220,19227,19293,19300,19304,19334,19337,19341,19435,19439,19449,19549,19558,19562,19583,19587,19590,19703,19706,19720,19724,19734,19737,19789,19792,19796,19803,19823,20020,20023,20027,20030,20036,20046,20050,20053,20069,20076,20111,20114,20118,20121,20148,20158,20161,20165,20168,20171],[18,17564,17565],{},"Laravel 13 dropped on March 17, 2026 with a promise that sounds almost too good: zero breaking changes. And for the most part, Taylor Otwell delivered. But \"zero breaking changes\" doesn't mean \"nothing happened.\" Quite the opposite — this release reshapes how you structure models, configure jobs, query vectors, and build AI features, all without forcing a single line of migration code.",[18,17567,17568],{},"Let's skip the marketing bullet points and dig into what actually matters.",[13,17570,17572],{"id":17571},"php-83-is-now-the-floor","PHP 8.3 Is Now the Floor",[18,17574,17575,17576,17579,17580,17583,17584,17587],{},"Laravel 13 drops PHP 8.2 support. This isn't just a version bump — it unlocks ",[22,17577,17578],{},"json_validate()",", typed class constants, ",[22,17581,17582],{},"#[\\Override]",", and the ",[22,17585,17586],{},"Randomizer"," additions that the framework now leans on internally. If you're still on 8.2, this is your forcing function.",[18,17589,17590],{},"Support window: bug fixes through Q3 2027, security patches through Q1 2028.",[13,17592,17594],{"id":17593},"php-attributes-36-new-attributes-one-architectural-shift","PHP Attributes: 36 New Attributes, One Architectural Shift",[18,17596,17597,17598,17601],{},"This is the headline feature, and it deserves more than a passing mention. Laravel 13 introduces ",[518,17599,17600],{},"36 new attribute classes"," across models, jobs, commands, form requests, resources, factories, and tests. Combined with the 17+ that already existed since Laravel 11-12, you now have 50+ attributes available.",[18,17603,17604,17605,17608],{},"But here's what most articles miss: ",[518,17606,17607],{},"this isn't syntactic sugar."," It's a shift toward declarative configuration that the framework can introspect at boot time, opening the door for better static analysis, IDE support, and tooling.",[343,17610,17612],{"id":17611},"eloquent-model-attributes","Eloquent Model Attributes",[18,17614,4542,17615,17618,17619,17622],{},[22,17616,17617],{},"#[Table]"," attribute is the most powerful — it replaces ",[518,17620,17621],{},"six"," separate properties in a single declaration:",[46,17624,17626],{"className":13615,"code":17625,"language":13609,"meta":51,"style":51},"use Illuminate\\Database\\Eloquent\\Attributes\\Table;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Fillable;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Hidden;\n\n#[Table(\n    name: 'external_orders',\n    key: 'uuid',\n    keyType: 'string',\n    incrementing: false,\n    timestamps: false,\n)]\n#[Fillable(['customer_id', 'total', 'currency', 'status'])]\n#[Hidden(['internal_notes', 'margin'])]\nclass ExternalOrder extends Model {}\n",[22,17627,17628,17637,17646,17655,17659,17668,17680,17692,17704,17715,17726,17731,17761,17780],{"__ignoreMap":51},[55,17629,17630,17632,17635],{"class":57,"line":58},[55,17631,13623],{"class":2043},[55,17633,17634],{"class":2069}," Illuminate\\Database\\Eloquent\\Attributes\\Table",[55,17636,5761],{"class":2053},[55,17638,17639,17641,17644],{"class":57,"line":64},[55,17640,13623],{"class":2043},[55,17642,17643],{"class":2069}," Illuminate\\Database\\Eloquent\\Attributes\\Fillable",[55,17645,5761],{"class":2053},[55,17647,17648,17650,17653],{"class":57,"line":71},[55,17649,13623],{"class":2043},[55,17651,17652],{"class":2069}," Illuminate\\Database\\Eloquent\\Attributes\\Hidden",[55,17654,5761],{"class":2053},[55,17656,17657],{"class":57,"line":77},[55,17658,68],{"emptyLinePlaceholder":67},[55,17660,17661,17663,17666],{"class":57,"line":83},[55,17662,15911],{"class":2053},[55,17664,17665],{"class":2069},"Table",[55,17667,2242],{"class":2053},[55,17669,17670,17673,17675,17678],{"class":57,"line":89},[55,17671,17672],{"class":717},"    name",[55,17674,3323],{"class":2053},[55,17676,17677],{"class":721},"'external_orders'",[55,17679,2250],{"class":2053},[55,17681,17682,17685,17687,17690],{"class":57,"line":95},[55,17683,17684],{"class":717},"    key",[55,17686,3323],{"class":2053},[55,17688,17689],{"class":721},"'uuid'",[55,17691,2250],{"class":2053},[55,17693,17694,17697,17699,17702],{"class":57,"line":101},[55,17695,17696],{"class":717},"    keyType",[55,17698,3323],{"class":2053},[55,17700,17701],{"class":721},"'string'",[55,17703,2250],{"class":2053},[55,17705,17706,17709,17711,17713],{"class":57,"line":107},[55,17707,17708],{"class":717},"    incrementing",[55,17710,3323],{"class":2053},[55,17712,10104],{"class":2069},[55,17714,2250],{"class":2053},[55,17716,17717,17720,17722,17724],{"class":57,"line":113},[55,17718,17719],{"class":717},"    timestamps",[55,17721,3323],{"class":2053},[55,17723,10104],{"class":2069},[55,17725,2250],{"class":2053},[55,17727,17728],{"class":57,"line":119},[55,17729,17730],{"class":2053},")]\n",[55,17732,17733,17735,17738,17740,17743,17745,17748,17750,17753,17755,17758],{"class":57,"line":125},[55,17734,15911],{"class":2053},[55,17736,17737],{"class":2069},"Fillable",[55,17739,9871],{"class":2053},[55,17741,17742],{"class":721},"'customer_id'",[55,17744,525],{"class":2053},[55,17746,17747],{"class":721},"'total'",[55,17749,525],{"class":2053},[55,17751,17752],{"class":721},"'currency'",[55,17754,525],{"class":2053},[55,17756,17757],{"class":721},"'status'",[55,17759,17760],{"class":2053},"])]\n",[55,17762,17763,17765,17768,17770,17773,17775,17778],{"class":57,"line":131},[55,17764,15911],{"class":2053},[55,17766,17767],{"class":2069},"Hidden",[55,17769,9871],{"class":2053},[55,17771,17772],{"class":721},"'internal_notes'",[55,17774,525],{"class":2053},[55,17776,17777],{"class":721},"'margin'",[55,17779,17760],{"class":2053},[55,17781,17782,17784,17787,17790,17793],{"class":57,"line":137},[55,17783,14445],{"class":2043},[55,17785,17786],{"class":717}," ExternalOrder",[55,17788,17789],{"class":2043}," extends",[55,17791,17792],{"class":717}," Model",[55,17794,17795],{"class":2053}," {}\n",[18,17797,17798],{},"Compare that to the old way:",[46,17800,17802],{"className":13615,"code":17801,"language":13609,"meta":51,"style":51},"class ExternalOrder extends Model\n{\n    protected $table = 'external_orders';\n    protected $primaryKey = 'uuid';\n    protected $keyType = 'string';\n    public $incrementing = false;\n    public $timestamps = false;\n    protected $fillable = ['customer_id', 'total', 'currency', 'status'];\n    protected $hidden = ['internal_notes', 'margin'];\n}\n",[22,17803,17804,17815,17819,17834,17848,17862,17876,17889,17916,17935],{"__ignoreMap":51},[55,17805,17806,17808,17810,17812],{"class":57,"line":58},[55,17807,14445],{"class":2043},[55,17809,17786],{"class":717},[55,17811,17789],{"class":2043},[55,17813,17814],{"class":717}," Model\n",[55,17816,17817],{"class":57,"line":64},[55,17818,4945],{"class":2053},[55,17820,17821,17824,17827,17829,17832],{"class":57,"line":71},[55,17822,17823],{"class":2043},"    protected",[55,17825,17826],{"class":2053}," $table ",[55,17828,3475],{"class":2043},[55,17830,17831],{"class":721}," 'external_orders'",[55,17833,5761],{"class":2053},[55,17835,17836,17838,17841,17843,17846],{"class":57,"line":77},[55,17837,17823],{"class":2043},[55,17839,17840],{"class":2053}," $primaryKey ",[55,17842,3475],{"class":2043},[55,17844,17845],{"class":721}," 'uuid'",[55,17847,5761],{"class":2053},[55,17849,17850,17852,17855,17857,17860],{"class":57,"line":83},[55,17851,17823],{"class":2043},[55,17853,17854],{"class":2053}," $keyType ",[55,17856,3475],{"class":2043},[55,17858,17859],{"class":721}," 'string'",[55,17861,5761],{"class":2053},[55,17863,17864,17866,17869,17871,17874],{"class":57,"line":89},[55,17865,14477],{"class":2043},[55,17867,17868],{"class":2053}," $incrementing ",[55,17870,3475],{"class":2043},[55,17872,17873],{"class":2069}," false",[55,17875,5761],{"class":2053},[55,17877,17878,17880,17883,17885,17887],{"class":57,"line":95},[55,17879,14477],{"class":2043},[55,17881,17882],{"class":2053}," $timestamps ",[55,17884,3475],{"class":2043},[55,17886,17873],{"class":2069},[55,17888,5761],{"class":2053},[55,17890,17891,17893,17896,17898,17900,17902,17904,17906,17908,17910,17912,17914],{"class":57,"line":101},[55,17892,17823],{"class":2043},[55,17894,17895],{"class":2053}," $fillable ",[55,17897,3475],{"class":2043},[55,17899,7064],{"class":2053},[55,17901,17742],{"class":721},[55,17903,525],{"class":2053},[55,17905,17747],{"class":721},[55,17907,525],{"class":2053},[55,17909,17752],{"class":721},[55,17911,525],{"class":2053},[55,17913,17757],{"class":721},[55,17915,6898],{"class":2053},[55,17917,17918,17920,17923,17925,17927,17929,17931,17933],{"class":57,"line":107},[55,17919,17823],{"class":2043},[55,17921,17922],{"class":2053}," $hidden ",[55,17924,3475],{"class":2043},[55,17926,7064],{"class":2053},[55,17928,17772],{"class":721},[55,17930,525],{"class":2053},[55,17932,17777],{"class":721},[55,17934,6898],{"class":2053},[55,17936,17937],{"class":57,"line":113},[55,17938,2005],{"class":2053},[18,17940,17941],{},"That's 7 lines of scattered property declarations collapsed into 3 focused attributes at the top of the class. The model body is now free for relationships, scopes, and business logic.",[17943,17944,17946,17947,17950],"h4",{"id":17945},"the-new-unguarded-attribute","The New ",[22,17948,17949],{},"#[Unguarded]"," Attribute",[18,17952,17953,17954,17957,17958,17960],{},"This one has no property equivalent. Previously, to make a model unguarded, you had to call ",[22,17955,17956],{},"Model::unguard()"," globally in a service provider, which affected ",[518,17959,7142],{}," models. Now:",[46,17962,17964],{"className":13615,"code":17963,"language":13609,"meta":51,"style":51},"use Illuminate\\Database\\Eloquent\\Attributes\\Unguarded;\n\n#[Unguarded]\nclass AppSetting extends Model {}\n",[22,17965,17966,17975,17979,17989],{"__ignoreMap":51},[55,17967,17968,17970,17973],{"class":57,"line":58},[55,17969,13623],{"class":2043},[55,17971,17972],{"class":2069}," Illuminate\\Database\\Eloquent\\Attributes\\Unguarded",[55,17974,5761],{"class":2053},[55,17976,17977],{"class":57,"line":64},[55,17978,68],{"emptyLinePlaceholder":67},[55,17980,17981,17983,17986],{"class":57,"line":71},[55,17982,15911],{"class":2053},[55,17984,17985],{"class":2069},"Unguarded",[55,17987,17988],{"class":2053},"]\n",[55,17990,17991,17993,17996,17998,18000],{"class":57,"line":77},[55,17992,14445],{"class":2043},[55,17994,17995],{"class":717}," AppSetting",[55,17997,17789],{"class":2043},[55,17999,17792],{"class":717},[55,18001,17795],{"class":2053},[18,18003,18004],{},"Scoped unguarding per model. This is useful for internal config tables or seed-only models where mass assignment protection is meaningless overhead.",[17943,18006,18008],{"id":18007},"other-model-attributes","Other Model Attributes",[46,18010,18012],{"className":13615,"code":18011,"language":13609,"meta":51,"style":51},"#[Connection('analytics')]   // Dedicated DB connection\n#[Touches(['post'])]         // Auto-touch parent timestamps\n#[Visible(['id', 'name'])]   // Whitelist serialization (inverse of Hidden)\n#[Appends(['full_name'])]    // Append accessors to serialization\n",[22,18013,18014,18031,18049,18072],{"__ignoreMap":51},[55,18015,18016,18018,18020,18022,18025,18028],{"class":57,"line":58},[55,18017,15911],{"class":2053},[55,18019,14569],{"class":2069},[55,18021,2054],{"class":2053},[55,18023,18024],{"class":721},"'analytics'",[55,18026,18027],{"class":2053},")]   ",[55,18029,18030],{"class":711},"// Dedicated DB connection\n",[55,18032,18033,18035,18038,18040,18043,18046],{"class":57,"line":64},[55,18034,15911],{"class":2053},[55,18036,18037],{"class":2069},"Touches",[55,18039,9871],{"class":2053},[55,18041,18042],{"class":721},"'post'",[55,18044,18045],{"class":2053},"])]         ",[55,18047,18048],{"class":711},"// Auto-touch parent timestamps\n",[55,18050,18051,18053,18056,18058,18061,18063,18066,18069],{"class":57,"line":71},[55,18052,15911],{"class":2053},[55,18054,18055],{"class":2069},"Visible",[55,18057,9871],{"class":2053},[55,18059,18060],{"class":721},"'id'",[55,18062,525],{"class":2053},[55,18064,18065],{"class":721},"'name'",[55,18067,18068],{"class":2053},"])]   ",[55,18070,18071],{"class":711},"// Whitelist serialization (inverse of Hidden)\n",[55,18073,18074,18076,18079,18081,18084,18087],{"class":57,"line":77},[55,18075,15911],{"class":2053},[55,18077,18078],{"class":2069},"Appends",[55,18080,9871],{"class":2053},[55,18082,18083],{"class":721},"'full_name'",[55,18085,18086],{"class":2053},"])]    ",[55,18088,18089],{"class":711},"// Append accessors to serialization\n",[343,18091,18093],{"id":18092},"queue-job-attributes","Queue Job Attributes",[18,18095,18096,18097,18100,18101,18104,18105,18108],{},"This is where attributes shine the most. Job configuration was always awkward — you'd define ",[22,18098,18099],{},"$tries"," as a property, ",[22,18102,18103],{},"$backoff"," as a method, and ",[22,18106,18107],{},"$timeout"," somewhere in between. Now everything sits at the class level:",[46,18110,18112],{"className":13615,"code":18111,"language":13609,"meta":51,"style":51},"use Illuminate\\Queue\\Attributes\\{Connection, Queue, Tries, Timeout, Backoff, MaxExceptions, FailOnTimeout};\n\n#[Connection('redis')]\n#[Queue('payments')]\n#[Tries(3)]\n#[Timeout(60)]\n#[Backoff([5, 15, 30])]\n#[MaxExceptions(2)]\n#[FailOnTimeout]\nclass ProcessPayment implements ShouldQueue\n{\n    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n\n    public function handle(): void\n    {\n        // Your job logic — no configuration noise here\n    }\n}\n",[22,18113,18114,18156,18160,18172,18185,18198,18210,18231,18243,18251,18262,18266,18290,18294,18308,18312,18317,18321],{"__ignoreMap":51},[55,18115,18116,18118,18121,18123,18125,18127,18129,18131,18134,18136,18139,18141,18144,18146,18149,18151,18154],{"class":57,"line":58},[55,18117,13623],{"class":2043},[55,18119,18120],{"class":2069}," Illuminate\\Queue\\Attributes\\",[55,18122,15640],{"class":2053},[55,18124,14569],{"class":2069},[55,18126,525],{"class":2053},[55,18128,14589],{"class":2069},[55,18130,525],{"class":2053},[55,18132,18133],{"class":2069},"Tries",[55,18135,525],{"class":2053},[55,18137,18138],{"class":2069},"Timeout",[55,18140,525],{"class":2053},[55,18142,18143],{"class":2069},"Backoff",[55,18145,525],{"class":2053},[55,18147,18148],{"class":2069},"MaxExceptions",[55,18150,525],{"class":2053},[55,18152,18153],{"class":2069},"FailOnTimeout",[55,18155,11886],{"class":2053},[55,18157,18158],{"class":57,"line":64},[55,18159,68],{"emptyLinePlaceholder":67},[55,18161,18162,18164,18166,18168,18170],{"class":57,"line":71},[55,18163,15911],{"class":2053},[55,18165,14569],{"class":2069},[55,18167,2054],{"class":2053},[55,18169,16053],{"class":721},[55,18171,17730],{"class":2053},[55,18173,18174,18176,18178,18180,18183],{"class":57,"line":77},[55,18175,15911],{"class":2053},[55,18177,14589],{"class":2069},[55,18179,2054],{"class":2053},[55,18181,18182],{"class":721},"'payments'",[55,18184,17730],{"class":2053},[55,18186,18187,18189,18191,18193,18196],{"class":57,"line":83},[55,18188,15911],{"class":2053},[55,18190,18133],{"class":2069},[55,18192,2054],{"class":2053},[55,18194,18195],{"class":2069},"3",[55,18197,17730],{"class":2053},[55,18199,18200,18202,18204,18206,18208],{"class":57,"line":89},[55,18201,15911],{"class":2053},[55,18203,18138],{"class":2069},[55,18205,2054],{"class":2053},[55,18207,11658],{"class":2069},[55,18209,17730],{"class":2053},[55,18211,18212,18214,18216,18218,18220,18222,18225,18227,18229],{"class":57,"line":95},[55,18213,15911],{"class":2053},[55,18215,18143],{"class":2069},[55,18217,9871],{"class":2053},[55,18219,11566],{"class":2069},[55,18221,525],{"class":2053},[55,18223,18224],{"class":2069},"15",[55,18226,525],{"class":2053},[55,18228,11663],{"class":2069},[55,18230,17760],{"class":2053},[55,18232,18233,18235,18237,18239,18241],{"class":57,"line":101},[55,18234,15911],{"class":2053},[55,18236,18148],{"class":2069},[55,18238,2054],{"class":2053},[55,18240,3355],{"class":2069},[55,18242,17730],{"class":2053},[55,18244,18245,18247,18249],{"class":57,"line":107},[55,18246,15911],{"class":2053},[55,18248,18153],{"class":2069},[55,18250,17988],{"class":2053},[55,18252,18253,18255,18258,18260],{"class":57,"line":113},[55,18254,14445],{"class":2043},[55,18256,18257],{"class":717}," ProcessPayment",[55,18259,14451],{"class":2043},[55,18261,14454],{"class":717},[55,18263,18264],{"class":57,"line":119},[55,18265,4945],{"class":2053},[55,18267,18268,18270,18273,18275,18278,18280,18283,18285,18288],{"class":57,"line":125},[55,18269,14463],{"class":2043},[55,18271,18272],{"class":2069}," Dispatchable",[55,18274,525],{"class":2053},[55,18276,18277],{"class":2069},"InteractsWithQueue",[55,18279,525],{"class":2053},[55,18281,18282],{"class":2069},"Queueable",[55,18284,525],{"class":2053},[55,18286,18287],{"class":2069},"SerializesModels",[55,18289,5761],{"class":2053},[55,18291,18292],{"class":57,"line":131},[55,18293,68],{"emptyLinePlaceholder":67},[55,18295,18296,18298,18300,18302,18304,18306],{"class":57,"line":137},[55,18297,14477],{"class":2043},[55,18299,2047],{"class":2043},[55,18301,14511],{"class":717},[55,18303,14514],{"class":2053},[55,18305,5495],{"class":2043},[55,18307,14519],{"class":2043},[55,18309,18310],{"class":57,"line":143},[55,18311,5089],{"class":2053},[55,18313,18314],{"class":57,"line":149},[55,18315,18316],{"class":711},"        // Your job logic — no configuration noise here\n",[55,18318,18319],{"class":57,"line":441},[55,18320,1962],{"class":2053},[55,18322,18323],{"class":57,"line":447},[55,18324,2005],{"class":2053},[18,18326,4542,18327,18330,18331,18334],{},[22,18328,18329],{},"#[Backoff([5, 15, 30])]"," syntax is new — pass an array for exponential backoff intervals. Previously this required a ",[22,18332,18333],{},"backoff()"," method returning an array.",[18,18336,4542,18337,18340,18341,18343,18344,5495],{},[22,18338,18339],{},"#[UniqueFor]"," attribute replaces implementing ",[22,18342,15761],{}," plus defining ",[22,18345,18346],{},"$uniqueFor",[46,18348,18350],{"className":13615,"code":18349,"language":13609,"meta":51,"style":51},"use Illuminate\\Queue\\Attributes\\UniqueFor;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\n\n#[UniqueFor(3600)]\nclass RebuildSearchIndex implements ShouldQueue, ShouldBeUnique\n{\n    // Unique lock for 1 hour — no property/method needed\n}\n",[22,18351,18352,18360,18368,18372,18384,18399,18403,18408],{"__ignoreMap":51},[55,18353,18354,18356,18358],{"class":57,"line":58},[55,18355,13623],{"class":2043},[55,18357,15900],{"class":2069},[55,18359,5761],{"class":2053},[55,18361,18362,18364,18366],{"class":57,"line":64},[55,18363,13623],{"class":2043},[55,18365,15781],{"class":2069},[55,18367,5761],{"class":2053},[55,18369,18370],{"class":57,"line":71},[55,18371,68],{"emptyLinePlaceholder":67},[55,18373,18374,18376,18378,18380,18382],{"class":57,"line":77},[55,18375,15911],{"class":2053},[55,18377,15887],{"class":2069},[55,18379,2054],{"class":2053},[55,18381,691],{"class":2069},[55,18383,17730],{"class":2053},[55,18385,18386,18388,18391,18393,18395,18397],{"class":57,"line":83},[55,18387,14445],{"class":2043},[55,18389,18390],{"class":717}," RebuildSearchIndex",[55,18392,14451],{"class":2043},[55,18394,15799],{"class":717},[55,18396,525],{"class":2053},[55,18398,15804],{"class":717},[55,18400,18401],{"class":57,"line":89},[55,18402,4945],{"class":2053},[55,18404,18405],{"class":57,"line":95},[55,18406,18407],{"class":711},"    // Unique lock for 1 hour — no property/method needed\n",[55,18409,18410],{"class":57,"line":101},[55,18411,2005],{"class":2053},[343,18413,18415],{"id":18414},"controller-attributes-middleware-and-authorization","Controller Attributes: Middleware and Authorization",[18,18417,18418,18419,18422],{},"This removes the need for ",[22,18420,18421],{},"__construct()"," middleware registration entirely:",[46,18424,18426],{"className":13615,"code":18425,"language":13609,"meta":51,"style":51},"use Illuminate\\Routing\\Attributes\\Controllers\\{Middleware, Authorize};\n\n#[Middleware('auth')]\n#[Authorize('manage-users', only: ['edit', 'update', 'destroy'])]\nclass UserController extends Controller\n{\n    #[Middleware('throttle:10,1')]\n    public function store(Request $request)\n    {\n        // ...\n    }\n\n    #[Authorize('delete', User::class)]\n    public function destroy(User $user)\n    {\n        // ...\n    }\n}\n",[22,18427,18428,18447,18451,18464,18497,18509,18513,18527,18544,18548,18553,18557,18561,18580,18596,18600,18604,18608],{"__ignoreMap":51},[55,18429,18430,18432,18435,18437,18440,18442,18445],{"class":57,"line":58},[55,18431,13623],{"class":2043},[55,18433,18434],{"class":2069}," Illuminate\\Routing\\Attributes\\Controllers\\",[55,18436,15640],{"class":2053},[55,18438,18439],{"class":2069},"Middleware",[55,18441,525],{"class":2053},[55,18443,18444],{"class":2069},"Authorize",[55,18446,11886],{"class":2053},[55,18448,18449],{"class":57,"line":64},[55,18450,68],{"emptyLinePlaceholder":67},[55,18452,18453,18455,18457,18459,18462],{"class":57,"line":71},[55,18454,15911],{"class":2053},[55,18456,18439],{"class":2069},[55,18458,2054],{"class":2053},[55,18460,18461],{"class":721},"'auth'",[55,18463,17730],{"class":2053},[55,18465,18466,18468,18470,18472,18475,18477,18480,18482,18485,18487,18490,18492,18495],{"class":57,"line":77},[55,18467,15911],{"class":2053},[55,18469,18444],{"class":2069},[55,18471,2054],{"class":2053},[55,18473,18474],{"class":721},"'manage-users'",[55,18476,525],{"class":2053},[55,18478,18479],{"class":717},"only",[55,18481,5462],{"class":2053},[55,18483,18484],{"class":721},"'edit'",[55,18486,525],{"class":2053},[55,18488,18489],{"class":721},"'update'",[55,18491,525],{"class":2053},[55,18493,18494],{"class":721},"'destroy'",[55,18496,17760],{"class":2053},[55,18498,18499,18501,18504,18506],{"class":57,"line":83},[55,18500,14445],{"class":2043},[55,18502,18503],{"class":717}," UserController",[55,18505,17789],{"class":2043},[55,18507,18508],{"class":717}," Controller\n",[55,18510,18511],{"class":57,"line":89},[55,18512,4945],{"class":2053},[55,18514,18515,18518,18520,18522,18525],{"class":57,"line":95},[55,18516,18517],{"class":2053},"    #[",[55,18519,18439],{"class":2069},[55,18521,2054],{"class":2053},[55,18523,18524],{"class":721},"'throttle:10,1'",[55,18526,17730],{"class":2053},[55,18528,18529,18531,18533,18536,18538,18541],{"class":57,"line":101},[55,18530,14477],{"class":2043},[55,18532,2047],{"class":2043},[55,18534,18535],{"class":717}," store",[55,18537,2054],{"class":2053},[55,18539,18540],{"class":2069},"Request",[55,18542,18543],{"class":2053}," $request)\n",[55,18545,18546],{"class":57,"line":107},[55,18547,5089],{"class":2053},[55,18549,18550],{"class":57,"line":113},[55,18551,18552],{"class":711},"        // ...\n",[55,18554,18555],{"class":57,"line":119},[55,18556,1962],{"class":2053},[55,18558,18559],{"class":57,"line":125},[55,18560,68],{"emptyLinePlaceholder":67},[55,18562,18563,18565,18567,18569,18572,18574,18576,18578],{"class":57,"line":131},[55,18564,18517],{"class":2053},[55,18566,18444],{"class":2069},[55,18568,2054],{"class":2053},[55,18570,18571],{"class":721},"'delete'",[55,18573,525],{"class":2053},[55,18575,17467],{"class":2069},[55,18577,16190],{"class":2043},[55,18579,17730],{"class":2053},[55,18581,18582,18584,18586,18589,18591,18593],{"class":57,"line":137},[55,18583,14477],{"class":2043},[55,18585,2047],{"class":2043},[55,18587,18588],{"class":717}," destroy",[55,18590,2054],{"class":2053},[55,18592,17467],{"class":2069},[55,18594,18595],{"class":2053}," $user)\n",[55,18597,18598],{"class":57,"line":143},[55,18599,5089],{"class":2053},[55,18601,18602],{"class":57,"line":149},[55,18603,18552],{"class":711},[55,18605,18606],{"class":57,"line":441},[55,18607,1962],{"class":2053},[55,18609,18610],{"class":57,"line":447},[55,18611,2005],{"class":2053},[18,18613,4542,18614,321,18616,18619,18620,18623],{},[22,18615,18479],{},[22,18617,18618],{},"except"," parameters work just like they did with ",[22,18621,18622],{},"$this->middleware()",", but are now statically analyzable.",[343,18625,18627],{"id":18626},"console-command-attributes","Console Command Attributes",[46,18629,18631],{"className":13615,"code":18630,"language":13609,"meta":51,"style":51},"use Illuminate\\Console\\Attributes\\{Signature, Description, Usage, Hidden};\n\n#[Signature('orders:reconcile {--since= : Start date} {--dry-run}')]\n#[Description('Reconcile order totals against payment gateway')]\n#[Usage('orders:reconcile --since=2026-01-01')]\n#[Usage('orders:reconcile --since=2026-01-01 --dry-run')]\nclass ReconcileOrders extends Command\n{\n    public function handle(): int\n    {\n        // No $signature or $description properties needed\n    }\n}\n",[22,18632,18633,18661,18665,18678,18691,18704,18717,18729,18733,18748,18752,18757,18761],{"__ignoreMap":51},[55,18634,18635,18637,18640,18642,18645,18647,18650,18652,18655,18657,18659],{"class":57,"line":58},[55,18636,13623],{"class":2043},[55,18638,18639],{"class":2069}," Illuminate\\Console\\Attributes\\",[55,18641,15640],{"class":2053},[55,18643,18644],{"class":2069},"Signature",[55,18646,525],{"class":2053},[55,18648,18649],{"class":2069},"Description",[55,18651,525],{"class":2053},[55,18653,18654],{"class":2069},"Usage",[55,18656,525],{"class":2053},[55,18658,17767],{"class":2069},[55,18660,11886],{"class":2053},[55,18662,18663],{"class":57,"line":64},[55,18664,68],{"emptyLinePlaceholder":67},[55,18666,18667,18669,18671,18673,18676],{"class":57,"line":71},[55,18668,15911],{"class":2053},[55,18670,18644],{"class":2069},[55,18672,2054],{"class":2053},[55,18674,18675],{"class":721},"'orders:reconcile {--since= : Start date} {--dry-run}'",[55,18677,17730],{"class":2053},[55,18679,18680,18682,18684,18686,18689],{"class":57,"line":77},[55,18681,15911],{"class":2053},[55,18683,18649],{"class":2069},[55,18685,2054],{"class":2053},[55,18687,18688],{"class":721},"'Reconcile order totals against payment gateway'",[55,18690,17730],{"class":2053},[55,18692,18693,18695,18697,18699,18702],{"class":57,"line":83},[55,18694,15911],{"class":2053},[55,18696,18654],{"class":2069},[55,18698,2054],{"class":2053},[55,18700,18701],{"class":721},"'orders:reconcile --since=2026-01-01'",[55,18703,17730],{"class":2053},[55,18705,18706,18708,18710,18712,18715],{"class":57,"line":89},[55,18707,15911],{"class":2053},[55,18709,18654],{"class":2069},[55,18711,2054],{"class":2053},[55,18713,18714],{"class":721},"'orders:reconcile --since=2026-01-01 --dry-run'",[55,18716,17730],{"class":2053},[55,18718,18719,18721,18724,18726],{"class":57,"line":95},[55,18720,14445],{"class":2043},[55,18722,18723],{"class":717}," ReconcileOrders",[55,18725,17789],{"class":2043},[55,18727,18728],{"class":717}," Command\n",[55,18730,18731],{"class":57,"line":101},[55,18732,4945],{"class":2053},[55,18734,18735,18737,18739,18741,18743,18745],{"class":57,"line":107},[55,18736,14477],{"class":2043},[55,18738,2047],{"class":2043},[55,18740,14511],{"class":717},[55,18742,14514],{"class":2053},[55,18744,5495],{"class":2043},[55,18746,18747],{"class":2043}," int\n",[55,18749,18750],{"class":57,"line":113},[55,18751,5089],{"class":2053},[55,18753,18754],{"class":57,"line":119},[55,18755,18756],{"class":711},"        // No $signature or $description properties needed\n",[55,18758,18759],{"class":57,"line":125},[55,18760,1962],{"class":2053},[55,18762,18763],{"class":57,"line":131},[55,18764,2005],{"class":2053},[18,18766,18767,18768,18771],{},"Multiple ",[22,18769,18770],{},"#[Usage]"," attributes stack — a small but thoughtful touch for help screen documentation.",[343,18773,18775],{"id":18774},"form-request-attributes","Form Request Attributes",[46,18777,18779],{"className":13615,"code":18778,"language":13609,"meta":51,"style":51},"use Illuminate\\Foundation\\Http\\Attributes\\{ErrorBag, RedirectToRoute, StopOnFirstFailure};\n\n#[ErrorBag('createPost')]\n#[RedirectToRoute('posts.create')]\n#[StopOnFirstFailure]\nclass StorePostRequest extends FormRequest\n{\n    public function rules(): array\n    {\n        return [\n            'title' => 'required|max:255',\n            'body' => 'required',\n        ];\n    }\n}\n",[22,18780,18781,18805,18809,18822,18835,18843,18855,18859,18874,18878,18884,18896,18908,18913,18917],{"__ignoreMap":51},[55,18782,18783,18785,18788,18790,18793,18795,18798,18800,18803],{"class":57,"line":58},[55,18784,13623],{"class":2043},[55,18786,18787],{"class":2069}," Illuminate\\Foundation\\Http\\Attributes\\",[55,18789,15640],{"class":2053},[55,18791,18792],{"class":2069},"ErrorBag",[55,18794,525],{"class":2053},[55,18796,18797],{"class":2069},"RedirectToRoute",[55,18799,525],{"class":2053},[55,18801,18802],{"class":2069},"StopOnFirstFailure",[55,18804,11886],{"class":2053},[55,18806,18807],{"class":57,"line":64},[55,18808,68],{"emptyLinePlaceholder":67},[55,18810,18811,18813,18815,18817,18820],{"class":57,"line":71},[55,18812,15911],{"class":2053},[55,18814,18792],{"class":2069},[55,18816,2054],{"class":2053},[55,18818,18819],{"class":721},"'createPost'",[55,18821,17730],{"class":2053},[55,18823,18824,18826,18828,18830,18833],{"class":57,"line":77},[55,18825,15911],{"class":2053},[55,18827,18797],{"class":2069},[55,18829,2054],{"class":2053},[55,18831,18832],{"class":721},"'posts.create'",[55,18834,17730],{"class":2053},[55,18836,18837,18839,18841],{"class":57,"line":83},[55,18838,15911],{"class":2053},[55,18840,18802],{"class":2069},[55,18842,17988],{"class":2053},[55,18844,18845,18847,18850,18852],{"class":57,"line":89},[55,18846,14445],{"class":2043},[55,18848,18849],{"class":717}," StorePostRequest",[55,18851,17789],{"class":2043},[55,18853,18854],{"class":717}," FormRequest\n",[55,18856,18857],{"class":57,"line":95},[55,18858,4945],{"class":2053},[55,18860,18861,18863,18865,18868,18870,18872],{"class":57,"line":101},[55,18862,14477],{"class":2043},[55,18864,2047],{"class":2043},[55,18866,18867],{"class":717}," rules",[55,18869,14514],{"class":2053},[55,18871,5495],{"class":2043},[55,18873,15206],{"class":2043},[55,18875,18876],{"class":57,"line":107},[55,18877,5089],{"class":2053},[55,18879,18880,18882],{"class":57,"line":113},[55,18881,2116],{"class":2043},[55,18883,6851],{"class":2053},[55,18885,18886,18889,18891,18894],{"class":57,"line":119},[55,18887,18888],{"class":721},"            'title'",[55,18890,13659],{"class":2043},[55,18892,18893],{"class":721}," 'required|max:255'",[55,18895,2250],{"class":2053},[55,18897,18898,18901,18903,18906],{"class":57,"line":125},[55,18899,18900],{"class":721},"            'body'",[55,18902,13659],{"class":2043},[55,18904,18905],{"class":721}," 'required'",[55,18907,2250],{"class":2053},[55,18909,18910],{"class":57,"line":131},[55,18911,18912],{"class":2053},"        ];\n",[55,18914,18915],{"class":57,"line":137},[55,18916,1962],{"class":2053},[55,18918,18919],{"class":57,"line":143},[55,18920,2005],{"class":2053},[18,18922,18923,18926],{},[22,18924,18925],{},"#[StopOnFirstFailure]"," is particularly useful for wizard-style forms where validating everything at once is wasted effort.",[343,18928,18930],{"id":18929},"testing-attributes","Testing Attributes",[46,18932,18934],{"className":13615,"code":18933,"language":13609,"meta":51,"style":51},"use Illuminate\\Foundation\\Testing\\Attributes\\{Seed, SetUp, TearDown};\n\n#[Seed(RoleAndPermissionSeeder::class)]\nclass AdminAccessTest extends TestCase\n{\n    private User $admin;\n\n    #[SetUp]\n    public function createAdmin(): void\n    {\n        $this->admin = User::factory()->admin()->create();\n    }\n\n    #[TearDown]\n    public function clearPermissionCache(): void\n    {\n        app()['permission.registrar']->forgetCachedPermissions();\n    }\n\n    public function test_admin_can_access_dashboard(): void\n    {\n        $this->actingAs($this->admin)\n            ->get('/admin')\n            ->assertOk();\n    }\n}\n",[22,18935,18936,18960,18964,18979,18991,18995,19006,19010,19018,19033,19037,19072,19076,19080,19088,19103,19107,19128,19132,19136,19151,19155,19173,19186,19195,19199],{"__ignoreMap":51},[55,18937,18938,18940,18943,18945,18948,18950,18953,18955,18958],{"class":57,"line":58},[55,18939,13623],{"class":2043},[55,18941,18942],{"class":2069}," Illuminate\\Foundation\\Testing\\Attributes\\",[55,18944,15640],{"class":2053},[55,18946,18947],{"class":2069},"Seed",[55,18949,525],{"class":2053},[55,18951,18952],{"class":2069},"SetUp",[55,18954,525],{"class":2053},[55,18956,18957],{"class":2069},"TearDown",[55,18959,11886],{"class":2053},[55,18961,18962],{"class":57,"line":64},[55,18963,68],{"emptyLinePlaceholder":67},[55,18965,18966,18968,18970,18972,18975,18977],{"class":57,"line":71},[55,18967,15911],{"class":2053},[55,18969,18947],{"class":2069},[55,18971,2054],{"class":2053},[55,18973,18974],{"class":2069},"RoleAndPermissionSeeder",[55,18976,16190],{"class":2043},[55,18978,17730],{"class":2053},[55,18980,18981,18983,18986,18988],{"class":57,"line":77},[55,18982,14445],{"class":2043},[55,18984,18985],{"class":717}," AdminAccessTest",[55,18987,17789],{"class":2043},[55,18989,18990],{"class":717}," TestCase\n",[55,18992,18993],{"class":57,"line":83},[55,18994,4945],{"class":2053},[55,18996,18997,19000,19003],{"class":57,"line":89},[55,18998,18999],{"class":2043},"    private",[55,19001,19002],{"class":2069}," User",[55,19004,19005],{"class":2053}," $admin;\n",[55,19007,19008],{"class":57,"line":95},[55,19009,68],{"emptyLinePlaceholder":67},[55,19011,19012,19014,19016],{"class":57,"line":101},[55,19013,18517],{"class":2053},[55,19015,18952],{"class":2069},[55,19017,17988],{"class":2053},[55,19019,19020,19022,19024,19027,19029,19031],{"class":57,"line":107},[55,19021,14477],{"class":2043},[55,19023,2047],{"class":2043},[55,19025,19026],{"class":717}," createAdmin",[55,19028,14514],{"class":2053},[55,19030,5495],{"class":2043},[55,19032,14519],{"class":2043},[55,19034,19035],{"class":57,"line":113},[55,19036,5089],{"class":2053},[55,19038,19039,19042,19044,19047,19049,19051,19053,19056,19058,19060,19063,19065,19067,19070],{"class":57,"line":119},[55,19040,19041],{"class":2069},"        $this",[55,19043,13701],{"class":2043},[55,19045,19046],{"class":2053},"admin ",[55,19048,3475],{"class":2043},[55,19050,19002],{"class":2069},[55,19052,13645],{"class":2043},[55,19054,19055],{"class":717},"factory",[55,19057,14514],{"class":2053},[55,19059,13701],{"class":2043},[55,19061,19062],{"class":717},"admin",[55,19064,14514],{"class":2053},[55,19066,13701],{"class":2043},[55,19068,19069],{"class":717},"create",[55,19071,7014],{"class":2053},[55,19073,19074],{"class":57,"line":125},[55,19075,1962],{"class":2053},[55,19077,19078],{"class":57,"line":131},[55,19079,68],{"emptyLinePlaceholder":67},[55,19081,19082,19084,19086],{"class":57,"line":137},[55,19083,18517],{"class":2053},[55,19085,18957],{"class":2069},[55,19087,17988],{"class":2053},[55,19089,19090,19092,19094,19097,19099,19101],{"class":57,"line":143},[55,19091,14477],{"class":2043},[55,19093,2047],{"class":2043},[55,19095,19096],{"class":717}," clearPermissionCache",[55,19098,14514],{"class":2053},[55,19100,5495],{"class":2043},[55,19102,14519],{"class":2043},[55,19104,19105],{"class":57,"line":149},[55,19106,5089],{"class":2053},[55,19108,19109,19112,19115,19118,19121,19123,19126],{"class":57,"line":441},[55,19110,19111],{"class":717},"        app",[55,19113,19114],{"class":2053},"()[",[55,19116,19117],{"class":721},"'permission.registrar'",[55,19119,19120],{"class":2053},"]",[55,19122,13701],{"class":2043},[55,19124,19125],{"class":717},"forgetCachedPermissions",[55,19127,7014],{"class":2053},[55,19129,19130],{"class":57,"line":447},[55,19131,1962],{"class":2053},[55,19133,19134],{"class":57,"line":453},[55,19135,68],{"emptyLinePlaceholder":67},[55,19137,19138,19140,19142,19145,19147,19149],{"class":57,"line":459},[55,19139,14477],{"class":2043},[55,19141,2047],{"class":2043},[55,19143,19144],{"class":717}," test_admin_can_access_dashboard",[55,19146,14514],{"class":2053},[55,19148,5495],{"class":2043},[55,19150,14519],{"class":2043},[55,19152,19153],{"class":57,"line":464},[55,19154,5089],{"class":2053},[55,19156,19157,19159,19161,19164,19166,19168,19170],{"class":57,"line":470},[55,19158,19041],{"class":2069},[55,19160,13701],{"class":2043},[55,19162,19163],{"class":717},"actingAs",[55,19165,2054],{"class":2053},[55,19167,15568],{"class":2069},[55,19169,13701],{"class":2043},[55,19171,19172],{"class":2053},"admin)\n",[55,19174,19175,19177,19179,19181,19184],{"class":57,"line":475},[55,19176,15045],{"class":2043},[55,19178,2164],{"class":717},[55,19180,2054],{"class":2053},[55,19182,19183],{"class":721},"'/admin'",[55,19185,14142],{"class":2053},[55,19187,19188,19190,19193],{"class":57,"line":481},[55,19189,15045],{"class":2043},[55,19191,19192],{"class":717},"assertOk",[55,19194,7014],{"class":2053},[55,19196,19197],{"class":57,"line":486},[55,19198,1962],{"class":2053},[55,19200,19201],{"class":57,"line":492},[55,19202,2005],{"class":2053},[18,19204,19205,321,19208,19211,19212,19215],{},[22,19206,19207],{},"#[SetUp]",[22,19209,19210],{},"#[TearDown]"," let you name your setup methods descriptively instead of overriding ",[22,19213,19214],{},"setUp()"," with a growing block of unrelated initialization.",[13,19217,19219],{"id":19218},"native-vector-search-with-pgvector","Native Vector Search with pgvector",[18,19221,19222,19223,19226],{},"This is the feature that flew under the radar. Laravel 13 adds ",[22,19224,19225],{},"whereVectorSimilarTo()"," to the query builder for native semantic search using PostgreSQL's pgvector extension.",[46,19228,19230],{"className":13615,"code":19229,"language":13609,"meta":51,"style":51},"$results = DB::table('documents')\n    ->whereVectorSimilarTo('embedding', 'Best wineries in Napa Valley')\n    ->limit(10)\n    ->get();\n",[22,19231,19232,19252,19272,19285],{"__ignoreMap":51},[55,19233,19234,19237,19239,19241,19243,19245,19247,19250],{"class":57,"line":58},[55,19235,19236],{"class":2053},"$results ",[55,19238,3475],{"class":2043},[55,19240,17247],{"class":2069},[55,19242,13645],{"class":2043},[55,19244,186],{"class":717},[55,19246,2054],{"class":2053},[55,19248,19249],{"class":721},"'documents'",[55,19251,14142],{"class":2053},[55,19253,19254,19257,19260,19262,19265,19267,19270],{"class":57,"line":64},[55,19255,19256],{"class":2043},"    ->",[55,19258,19259],{"class":717},"whereVectorSimilarTo",[55,19261,2054],{"class":2053},[55,19263,19264],{"class":721},"'embedding'",[55,19266,525],{"class":2053},[55,19268,19269],{"class":721},"'Best wineries in Napa Valley'",[55,19271,14142],{"class":2053},[55,19273,19274,19276,19279,19281,19283],{"class":57,"line":71},[55,19275,19256],{"class":2043},[55,19277,19278],{"class":717},"limit",[55,19280,2054],{"class":2053},[55,19282,14716],{"class":2069},[55,19284,14142],{"class":2053},[55,19286,19287,19289,19291],{"class":57,"line":77},[55,19288,19256],{"class":2043},[55,19290,2164],{"class":717},[55,19292,7014],{"class":2053},[18,19294,19295,19296,19299],{},"What's happening here: the string is converted to an embedding vector via the configured AI provider (through the AI SDK), then compared against stored vectors using ",[518,19297,19298],{},"cosine similarity",". Results are automatically ordered by relevance.",[343,19301,19303],{"id":19302},"how-it-works-internally","How It Works Internally",[2527,19305,19306,19309,19320,19327],{},[1399,19307,19308],{},"The query string is sent to your configured embedding model (OpenAI, Anthropic, etc.) via the AI SDK",[1399,19310,19311,19312,19315,19316,19319],{},"The returned vector is compared against the ",[22,19313,19314],{},"embedding"," column using pgvector's ",[22,19317,19318],{},"\u003C=>"," cosine distance operator",[1399,19321,19322,19323,19326],{},"Results below the ",[22,19324,19325],{},"minSimilarity"," threshold are filtered out",[1399,19328,19329,19330,19333],{},"An ",[518,19331,19332],{},"HNSW (Hierarchical Navigable Small World) index"," is automatically created on vector columns, giving you approximate nearest neighbor search that scales to millions of rows",[18,19335,19336],{},"This eliminates the need for separate vector databases like Pinecone or Weaviate for most use cases. Your embeddings live alongside your relational data in PostgreSQL.",[343,19338,19340],{"id":19339},"migration-example","Migration Example",[46,19342,19344],{"className":13615,"code":19343,"language":13609,"meta":51,"style":51},"Schema::create('documents', function (Blueprint $table) {\n    $table->id();\n    $table->text('content');\n    $table->vector('embedding', 1536); // 1536 dimensions for OpenAI ada-002\n    $table->timestamps();\n});\n",[22,19345,19346,19371,19382,19397,19420,19431],{"__ignoreMap":51},[55,19347,19348,19351,19353,19355,19357,19359,19361,19363,19365,19368],{"class":57,"line":58},[55,19349,19350],{"class":2069},"Schema",[55,19352,13645],{"class":2043},[55,19354,19069],{"class":717},[55,19356,2054],{"class":2053},[55,19358,19249],{"class":721},[55,19360,525],{"class":2053},[55,19362,9734],{"class":2043},[55,19364,2093],{"class":2053},[55,19366,19367],{"class":2069},"Blueprint",[55,19369,19370],{"class":2053}," $table) {\n",[55,19372,19373,19376,19378,19380],{"class":57,"line":64},[55,19374,19375],{"class":2053},"    $table",[55,19377,13701],{"class":2043},[55,19379,2402],{"class":717},[55,19381,7014],{"class":2053},[55,19383,19384,19386,19388,19391,19393,19395],{"class":57,"line":71},[55,19385,19375],{"class":2053},[55,19387,13701],{"class":2043},[55,19389,19390],{"class":717},"text",[55,19392,2054],{"class":2053},[55,19394,13890],{"class":721},[55,19396,2178],{"class":2053},[55,19398,19399,19401,19403,19406,19408,19410,19412,19415,19417],{"class":57,"line":77},[55,19400,19375],{"class":2053},[55,19402,13701],{"class":2043},[55,19404,19405],{"class":717},"vector",[55,19407,2054],{"class":2053},[55,19409,19264],{"class":721},[55,19411,525],{"class":2053},[55,19413,19414],{"class":2069},"1536",[55,19416,15122],{"class":2053},[55,19418,19419],{"class":711},"// 1536 dimensions for OpenAI ada-002\n",[55,19421,19422,19424,19426,19429],{"class":57,"line":83},[55,19423,19375],{"class":2053},[55,19425,13701],{"class":2043},[55,19427,19428],{"class":717},"timestamps",[55,19430,7014],{"class":2053},[55,19432,19433],{"class":57,"line":89},[55,19434,6125],{"class":2053},[13,19436,19438],{"id":19437},"cachetouch-finally","Cache::touch() — Finally",[18,19440,19441,19442,19445,19446,532],{},"A deceptively simple addition that solves a real problem. ",[22,19443,19444],{},"Cache::touch()"," extends a cached item's TTL ",[518,19447,19448],{},"without reading or re-storing the value",[46,19450,19452],{"className":13615,"code":19451,"language":13609,"meta":51,"style":51},"// Extend session TTL on activity — no deserialization overhead\nCache::touch('user_session:123', 3600);\n\n// Use Carbon for readability\nCache::touch('analytics_data', now()->addHours(6));\n\n// Remove TTL entirely (make permanent)\nCache::touch('report_cache', null);\n",[22,19453,19454,19459,19480,19484,19489,19520,19524,19529],{"__ignoreMap":51},[55,19455,19456],{"class":57,"line":58},[55,19457,19458],{"class":711},"// Extend session TTL on activity — no deserialization overhead\n",[55,19460,19461,19464,19466,19469,19471,19474,19476,19478],{"class":57,"line":64},[55,19462,19463],{"class":2069},"Cache",[55,19465,13645],{"class":2043},[55,19467,19468],{"class":717},"touch",[55,19470,2054],{"class":2053},[55,19472,19473],{"class":721},"'user_session:123'",[55,19475,525],{"class":2053},[55,19477,691],{"class":2069},[55,19479,2178],{"class":2053},[55,19481,19482],{"class":57,"line":71},[55,19483,68],{"emptyLinePlaceholder":67},[55,19485,19486],{"class":57,"line":77},[55,19487,19488],{"class":711},"// Use Carbon for readability\n",[55,19490,19491,19493,19495,19497,19499,19502,19504,19506,19508,19510,19513,19515,19518],{"class":57,"line":83},[55,19492,19463],{"class":2069},[55,19494,13645],{"class":2043},[55,19496,19468],{"class":717},[55,19498,2054],{"class":2053},[55,19500,19501],{"class":721},"'analytics_data'",[55,19503,525],{"class":2053},[55,19505,14704],{"class":717},[55,19507,14514],{"class":2053},[55,19509,13701],{"class":2043},[55,19511,19512],{"class":717},"addHours",[55,19514,2054],{"class":2053},[55,19516,19517],{"class":2069},"6",[55,19519,8359],{"class":2053},[55,19521,19522],{"class":57,"line":89},[55,19523,68],{"emptyLinePlaceholder":67},[55,19525,19526],{"class":57,"line":95},[55,19527,19528],{"class":711},"// Remove TTL entirely (make permanent)\n",[55,19530,19531,19533,19535,19537,19539,19542,19544,19547],{"class":57,"line":101},[55,19532,19463],{"class":2069},[55,19534,13645],{"class":2043},[55,19536,19468],{"class":717},[55,19538,2054],{"class":2053},[55,19540,19541],{"class":721},"'report_cache'",[55,19543,525],{"class":2053},[55,19545,19546],{"class":2069},"null",[55,19548,2178],{"class":2053},[18,19550,19551,19552,19554,19555,19557],{},"Returns ",[22,19553,9610],{}," on success, ",[22,19556,10104],{}," if the key doesn't exist.",[343,19559,19561],{"id":19560},"why-this-matters","Why This Matters",[18,19563,19564,19565,19568,19569,19572,19573,19575,19576,19579,19580,19582],{},"Previously, extending a TTL required ",[22,19566,19567],{},"Cache::get()"," + ",[22,19570,19571],{},"Cache::put()",", which meant deserializing and re-serializing the value. For large cached objects (compiled views, serialized collections, aggregated reports), this was wasteful. ",[22,19574,19444],{}," is a single atomic operation on every driver — Redis ",[22,19577,19578],{},"EXPIRE",", Memcached ",[22,19581,19468],{},", and equivalent operations on Database, DynamoDB, File, and APC.",[13,19584,19586],{"id":19585},"queueroute-centralized-job-routing","Queue::route() — Centralized Job Routing",[18,19588,19589],{},"Instead of scattering queue/connection config across dozens of job classes:",[46,19591,19593],{"className":13615,"code":19592,"language":13609,"meta":51,"style":51},"// In a service provider\nQueue::route(ProcessPayment::class, connection: 'redis', queue: 'payments');\nQueue::route(SendNewsletter::class, connection: 'sqs', queue: 'emails');\nQueue::route(GenerateReport::class, connection: 'redis', queue: 'reports');\n",[22,19594,19595,19600,19634,19669],{"__ignoreMap":51},[55,19596,19597],{"class":57,"line":58},[55,19598,19599],{"class":711},"// In a service provider\n",[55,19601,19602,19604,19606,19609,19611,19614,19616,19618,19620,19622,19624,19626,19628,19630,19632],{"class":57,"line":64},[55,19603,14589],{"class":2069},[55,19605,13645],{"class":2043},[55,19607,19608],{"class":717},"route",[55,19610,2054],{"class":2053},[55,19612,19613],{"class":2069},"ProcessPayment",[55,19615,16190],{"class":2043},[55,19617,525],{"class":2053},[55,19619,963],{"class":717},[55,19621,3323],{"class":2053},[55,19623,16053],{"class":721},[55,19625,525],{"class":2053},[55,19627,16042],{"class":717},[55,19629,3323],{"class":2053},[55,19631,18182],{"class":721},[55,19633,2178],{"class":2053},[55,19635,19636,19638,19640,19642,19644,19647,19649,19651,19653,19655,19658,19660,19662,19664,19667],{"class":57,"line":71},[55,19637,14589],{"class":2069},[55,19639,13645],{"class":2043},[55,19641,19608],{"class":717},[55,19643,2054],{"class":2053},[55,19645,19646],{"class":2069},"SendNewsletter",[55,19648,16190],{"class":2043},[55,19650,525],{"class":2053},[55,19652,963],{"class":717},[55,19654,3323],{"class":2053},[55,19656,19657],{"class":721},"'sqs'",[55,19659,525],{"class":2053},[55,19661,16042],{"class":717},[55,19663,3323],{"class":2053},[55,19665,19666],{"class":721},"'emails'",[55,19668,2178],{"class":2053},[55,19670,19671,19673,19675,19677,19679,19682,19684,19686,19688,19690,19692,19694,19696,19698,19701],{"class":57,"line":77},[55,19672,14589],{"class":2069},[55,19674,13645],{"class":2043},[55,19676,19608],{"class":717},[55,19678,2054],{"class":2053},[55,19680,19681],{"class":2069},"GenerateReport",[55,19683,16190],{"class":2043},[55,19685,525],{"class":2053},[55,19687,963],{"class":717},[55,19689,3323],{"class":2053},[55,19691,16053],{"class":721},[55,19693,525],{"class":2053},[55,19695,16042],{"class":717},[55,19697,3323],{"class":2053},[55,19699,19700],{"class":721},"'reports'",[55,19702,2178],{"class":2053},[18,19704,19705],{},"This gives you a single place to see and manage your entire job topology. It also means you can change routing without modifying job classes — useful when migrating queue backends or rebalancing workers.",[18,19707,19708,19709,525,19712,19715,19716,19719],{},"Job-level attributes (",[22,19710,19711],{},"#[Connection]",[22,19713,19714],{},"#[Queue]",") take precedence over ",[22,19717,19718],{},"Queue::route()",", so you can use routing as a default with per-job overrides.",[13,19721,19723],{"id":19722},"jsonapi-resources","JSON:API Resources",[18,19725,19726,19727,1448,19730,19733],{},"Laravel 13 ships first-party JSON:API specification support. If you've used ",[22,19728,19729],{},"spatie/laravel-json-api",[22,19731,19732],{},"laravel-json-api/laravel",", you know the pain of wiring up compliant responses. Now it's built in.",[18,19735,19736],{},"The new resource classes handle:",[1396,19738,19739,19757,19766,19775,19781],{},[1399,19740,19741,19744,19745,525,19748,525,19750,525,19753,19756],{},[518,19742,19743],{},"Resource object serialization"," — ",[22,19746,19747],{},"type",[22,19749,2402],{},[22,19751,19752],{},"attributes",[22,19754,19755],{},"relationships"," structure",[1399,19758,19759,19744,19762,19765],{},[518,19760,19761],{},"Relationship inclusion",[22,19763,19764],{},"?include=author,comments"," query parameter",[1399,19767,19768,19744,19771,19774],{},[518,19769,19770],{},"Sparse fieldsets",[22,19772,19773],{},"?fields[posts]=title,body"," to reduce payload",[1399,19776,19777,19780],{},[518,19778,19779],{},"Links"," — self, related, and pagination links",[1399,19782,19783,19744,19786],{},[518,19784,19785],{},"Compliant headers",[22,19787,19788],{},"Content-Type: application/vnd.api+json",[18,19790,19791],{},"This is significant for teams building APIs consumed by Ember.js, JSON:API-compliant mobile clients, or any frontend that benefits from a standardized response format.",[13,19793,19795],{"id":19794},"laravel-ai-sdk-production-stable","Laravel AI SDK — Production Stable",[18,19797,19798,19799,19802],{},"The AI SDK that was beta in Laravel 12 is now stable. It provides a ",[518,19800,19801],{},"provider-agnostic interface"," for:",[1396,19804,19805,19808,19811,19814,19817,19820],{},[1399,19806,19807],{},"Text generation",[1399,19809,19810],{},"Tool-calling agents",[1399,19812,19813],{},"Image creation",[1399,19815,19816],{},"Audio synthesis",[1399,19818,19819],{},"Embedding generation",[1399,19821,19822],{},"Vector store integrations",[46,19824,19826],{"className":13615,"code":19825,"language":13609,"meta":51,"style":51},"use App\\Agents\\SalesCoach;\n\n// Streaming response\nRoute::get('/coach', function () {\n    return (new SalesCoach)->stream('Analyze this sales transcript...');\n});\n\n// Queued agent with promise-style callbacks\nRoute::post('/coach', function (Request $request) {\n    return (new SalesCoach)\n        ->queue($request->input('transcript'))\n        ->then(function (AgentResponse $response) {\n            // Process result\n        })\n        ->catch(function (Throwable $e) {\n            // Handle error\n        });\n});\n",[22,19827,19828,19837,19841,19846,19866,19891,19895,19899,19904,19927,19939,19961,19979,19984,19989,20006,20011,20016],{"__ignoreMap":51},[55,19829,19830,19832,19835],{"class":57,"line":58},[55,19831,13623],{"class":2043},[55,19833,19834],{"class":2069}," App\\Agents\\SalesCoach",[55,19836,5761],{"class":2053},[55,19838,19839],{"class":57,"line":64},[55,19840,68],{"emptyLinePlaceholder":67},[55,19842,19843],{"class":57,"line":71},[55,19844,19845],{"class":711},"// Streaming response\n",[55,19847,19848,19850,19852,19854,19856,19859,19861,19863],{"class":57,"line":77},[55,19849,15622],{"class":2069},[55,19851,13645],{"class":2043},[55,19853,2164],{"class":717},[55,19855,2054],{"class":2053},[55,19857,19858],{"class":721},"'/coach'",[55,19860,525],{"class":2053},[55,19862,9734],{"class":2043},[55,19864,19865],{"class":2053}," () {\n",[55,19867,19868,19870,19872,19874,19877,19879,19881,19884,19886,19889],{"class":57,"line":83},[55,19869,2356],{"class":2043},[55,19871,2093],{"class":2053},[55,19873,15219],{"class":2043},[55,19875,19876],{"class":2069}," SalesCoach",[55,19878,12108],{"class":2053},[55,19880,13701],{"class":2043},[55,19882,19883],{"class":717},"stream",[55,19885,2054],{"class":2053},[55,19887,19888],{"class":721},"'Analyze this sales transcript...'",[55,19890,2178],{"class":2053},[55,19892,19893],{"class":57,"line":89},[55,19894,6125],{"class":2053},[55,19896,19897],{"class":57,"line":95},[55,19898,68],{"emptyLinePlaceholder":67},[55,19900,19901],{"class":57,"line":101},[55,19902,19903],{"class":711},"// Queued agent with promise-style callbacks\n",[55,19905,19906,19908,19910,19912,19914,19916,19918,19920,19922,19924],{"class":57,"line":107},[55,19907,15622],{"class":2069},[55,19909,13645],{"class":2043},[55,19911,13704],{"class":717},[55,19913,2054],{"class":2053},[55,19915,19858],{"class":721},[55,19917,525],{"class":2053},[55,19919,9734],{"class":2043},[55,19921,2093],{"class":2053},[55,19923,18540],{"class":2069},[55,19925,19926],{"class":2053}," $request) {\n",[55,19928,19929,19931,19933,19935,19937],{"class":57,"line":113},[55,19930,2356],{"class":2043},[55,19932,2093],{"class":2053},[55,19934,15219],{"class":2043},[55,19936,19876],{"class":2069},[55,19938,14142],{"class":2053},[55,19940,19941,19944,19946,19949,19951,19954,19956,19959],{"class":57,"line":119},[55,19942,19943],{"class":2043},"        ->",[55,19945,16042],{"class":717},[55,19947,19948],{"class":2053},"($request",[55,19950,13701],{"class":2043},[55,19952,19953],{"class":717},"input",[55,19955,2054],{"class":2053},[55,19957,19958],{"class":721},"'transcript'",[55,19960,7447],{"class":2053},[55,19962,19963,19965,19967,19969,19971,19973,19976],{"class":57,"line":125},[55,19964,19943],{"class":2043},[55,19966,6061],{"class":717},[55,19968,2054],{"class":2053},[55,19970,9734],{"class":2043},[55,19972,2093],{"class":2053},[55,19974,19975],{"class":2069},"AgentResponse",[55,19977,19978],{"class":2053}," $response) {\n",[55,19980,19981],{"class":57,"line":131},[55,19982,19983],{"class":711},"            // Process result\n",[55,19985,19986],{"class":57,"line":137},[55,19987,19988],{"class":2053},"        })\n",[55,19990,19991,19993,19995,19997,19999,20001,20003],{"class":57,"line":143},[55,19992,19943],{"class":2043},[55,19994,5899],{"class":717},[55,19996,2054],{"class":2053},[55,19998,9734],{"class":2043},[55,20000,2093],{"class":2053},[55,20002,16575],{"class":2069},[55,20004,20005],{"class":2053}," $e) {\n",[55,20007,20008],{"class":57,"line":149},[55,20009,20010],{"class":711},"            // Handle error\n",[55,20012,20013],{"class":57,"line":441},[55,20014,20015],{"class":2053},"        });\n",[55,20017,20018],{"class":57,"line":447},[55,20019,6125],{"class":2053},[18,20021,20022],{},"The SDK handles retry logic, error normalization, and queue integration behind the scenes. Swap providers by changing config, not code.",[13,20024,20026],{"id":20025},"multi-tenancy-teams-in-starter-kits","Multi-Tenancy Teams in Starter Kits",[18,20028,20029],{},"Laravel 13 brings team-based multi-tenancy back to official starter kits — but with a critical architectural change from the old Jetstream implementation.",[18,20031,20032,20035],{},[518,20033,20034],{},"The old way (Jetstream):"," Teams were session-based. The \"current team\" was stored in the session, which meant you couldn't operate in two team contexts simultaneously — opening a second tab shared the same session state.",[18,20037,20038,20041,20042,20045],{},[518,20039,20040],{},"The new way (Laravel 13):"," Teams are ",[518,20043,20044],{},"URL-routed",". The team context is part of the URL path, so different tabs can operate in different team contexts. This is how multi-tenancy should work in a stateless HTTP world.",[13,20047,20049],{"id":20048},"security-cache-serialization-hardening","Security: Cache Serialization Hardening",[18,20051,20052],{},"A subtle but important change. The default cache config now includes:",[46,20054,20056],{"className":13615,"code":20055,"language":13609,"meta":51,"style":51},"'serializable_classes' => false,\n",[22,20057,20058],{"__ignoreMap":51},[55,20059,20060,20063,20065,20067],{"class":57,"line":58},[55,20061,20062],{"class":721},"'serializable_classes'",[55,20064,13659],{"class":2043},[55,20066,17873],{"class":2069},[55,20068,2250],{"class":2053},[18,20070,20071,20072,20075],{},"This prevents PHP object deserialization from cache stores entirely by default. If your ",[22,20073,20074],{},"APP_KEY"," is leaked, an attacker can no longer exploit deserialization gadget chains through cached values. If you intentionally cache PHP objects, you'll need to explicitly whitelist those classes:",[46,20077,20079],{"className":13615,"code":20078,"language":13609,"meta":51,"style":51},"'serializable_classes' => [\n    App\\DataTransferObjects\\ReportData::class,\n    App\\ValueObjects\\Money::class,\n],\n",[22,20080,20081,20089,20098,20107],{"__ignoreMap":51},[55,20082,20083,20085,20087],{"class":57,"line":58},[55,20084,20062],{"class":721},[55,20086,13659],{"class":2043},[55,20088,6851],{"class":2053},[55,20090,20091,20094,20096],{"class":57,"line":64},[55,20092,20093],{"class":2069},"    App\\DataTransferObjects\\ReportData",[55,20095,16190],{"class":2043},[55,20097,2250],{"class":2053},[55,20099,20100,20103,20105],{"class":57,"line":71},[55,20101,20102],{"class":2069},"    App\\ValueObjects\\Money",[55,20104,16190],{"class":2043},[55,20106,2250],{"class":2053},[55,20108,20109],{"class":57,"line":77},[55,20110,5473],{"class":2053},[18,20112,20113],{},"This is defense-in-depth done right — secure by default, opt-in for specific use cases.",[13,20115,20117],{"id":20116},"upgrading-from-laravel-12","Upgrading from Laravel 12",[18,20119,20120],{},"The upgrade is genuinely painless for most apps:",[2527,20122,20123,20132,20135,20142],{},[1399,20124,20125,20126,3323,20129],{},"Update ",[22,20127,20128],{},"composer.json",[22,20130,20131],{},"\"laravel/framework\": \"^13.0\"",[1399,20133,20134],{},"Ensure PHP >= 8.3",[1399,20136,20137,20138,20141],{},"Review the ",[22,20139,20140],{},"serializable_classes"," cache config if you cache PHP objects",[1399,20143,20144,20145],{},"Run ",[22,20146,20147],{},"composer update",[18,20149,20150,20153,20154,20157],{},[518,20151,20152],{},"Laravel Boost"," (the first-party MCP server) offers guided upgrades via ",[22,20155,20156],{},"/upgrade-laravel-v13"," in Claude Code, Cursor, or VS Code if you want automated migration assistance.",[18,20159,20160],{},"All attribute features are opt-in. Your existing property-based configuration continues to work unchanged. You can adopt attributes incrementally — even mixing properties and attributes in the same class.",[13,20162,20164],{"id":20163},"final-thoughts","Final Thoughts",[18,20166,20167],{},"Laravel 13 is a \"quality of life\" release that manages to be architecturally significant without being disruptive. The attributes system, vector search, and AI SDK aren't just conveniences — they signal where the framework is headed: declarative configuration, AI-native tooling, and fewer external dependencies for common patterns.",[18,20169,20170],{},"The zero-breaking-changes promise is real. Upgrade, then adopt new features at your own pace. That's how framework evolution should work.",[4725,20172,20173],{},"html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":51,"searchDepth":64,"depth":64,"links":20175},[20176,20177,20185,20189,20192,20193,20194,20195,20196,20197,20198],{"id":17571,"depth":64,"text":17572},{"id":17593,"depth":64,"text":17594,"children":20178},[20179,20180,20181,20182,20183,20184],{"id":17611,"depth":71,"text":17612},{"id":18092,"depth":71,"text":18093},{"id":18414,"depth":71,"text":18415},{"id":18626,"depth":71,"text":18627},{"id":18774,"depth":71,"text":18775},{"id":18929,"depth":71,"text":18930},{"id":19218,"depth":64,"text":19219,"children":20186},[20187,20188],{"id":19302,"depth":71,"text":19303},{"id":19339,"depth":71,"text":19340},{"id":19437,"depth":64,"text":19438,"children":20190},[20191],{"id":19560,"depth":71,"text":19561},{"id":19585,"depth":64,"text":19586},{"id":19722,"depth":64,"text":19723},{"id":19794,"depth":64,"text":19795},{"id":20025,"depth":64,"text":20026},{"id":20048,"depth":64,"text":20049},{"id":20116,"depth":64,"text":20117},{"id":20163,"depth":64,"text":20164},"Backend Development","/blog-covers/laravel-13-deep-dive.png","2026-03-27","A senior developer's breakdown of Laravel 13 — PHP attributes across 36+ locations, native vector search with pgvector, Cache::touch(), Queue::route(), JSON:API resources, and the AI SDK. Real code, real architecture decisions.",{},"/posts/laravel-13-deep-dive","20 min read",{"title":17560,"description":20202},{"loc":20204,"lastmod":20201},"posts/laravel-13-deep-dive",[16821,13609,20210,16824,20211,14272,20212],"laravel-13","eloquent","vector-search","xTIJddn2d209ktlH2VGH2rYaTJQ8JR2e1gSjK6os0p0",1777897734313]