<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: WAVX solutions</title>
    <description>The latest articles on DEV Community by WAVX solutions (@wavx_solutions_1c671328ad).</description>
    <link>https://dev.to/wavx_solutions_1c671328ad</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4158958%2F1a91e474-6e76-4f3f-bda5-cb1064077c3c.jpg</url>
      <title>DEV Community: WAVX solutions</title>
      <link>https://dev.to/wavx_solutions_1c671328ad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wavx_solutions_1c671328ad"/>
    <language>en</language>
    <item>
      <title>Shopify App Development Cost in India (2026)</title>
      <dc:creator>WAVX solutions</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:49:49 +0000</pubDate>
      <link>https://dev.to/wavx_solutions_1c671328ad/shopify-app-development-cost-in-india-2026-2i87</link>
      <guid>https://dev.to/wavx_solutions_1c671328ad/shopify-app-development-cost-in-india-2026-2i87</guid>
      <description>&lt;p&gt;A custom Shopify app built in India costs roughly what a small custom web application costs, because that is what it is: a web application that talks to Shopify's APIs. On WavX's published cost model, the planning range is about ₹2.2 lakh to ₹3.25 lakh for a small private app that does one job, ₹5.6 lakh to ₹8.25 lakh for a full embedded app with its own data and admin screens, and ₹8.5 lakh to ₹12.5 lakh for a public app sold on the Shopify App Store. These are planning ranges with the assumptions shown below, not quotes. Shopify's own charges (the plan, the transaction fee and, for some features, the Plus plan) are separate, and are listed here as read on Shopify's pricing page on 2 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as a Shopify app
&lt;/h2&gt;

&lt;p&gt;Shopify's help centre describes three kinds of app, and the kind decides most of the cost.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Who it is for&lt;/th&gt;
&lt;th&gt;Review by Shopify&lt;/th&gt;
&lt;th&gt;Billing through Shopify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public app&lt;/td&gt;
&lt;td&gt;Many stores. Installed from the Shopify App Store, listed or unlisted&lt;/td&gt;
&lt;td&gt;Yes, it goes through app review&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom app&lt;/td&gt;
&lt;td&gt;One store, or several stores inside the same Plus organisation&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No. Shopify's distribution guide says a custom app cannot charge merchants through Shopify's app billing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App made by Shopify&lt;/td&gt;
&lt;td&gt;Any store&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;Usually free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A custom app is created and managed in Shopify's Dev Dashboard. The older route, creating a custom app inside the Shopify admin, is no longer available for new apps according to the same guide.&lt;/p&gt;

&lt;p&gt;Most merchants asking about cost want a custom app: something that connects the store to an accounting system, a courier, a CRM or a warehouse, or that adds a rule Shopify does not have. That is the work described on WavX's &lt;a href="https://www.wavxsolutions.in/services/shopify/custom-app-development" rel="noopener noreferrer"&gt;custom Shopify app development&lt;/a&gt; page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning ranges, with the assumptions shown
&lt;/h2&gt;

&lt;p&gt;WavX does not publish a separate Shopify rate card. A custom app is priced the way any custom web application with integrations is priced, so the figures below come from the &lt;a href="https://www.wavxsolutions.in/tools/software-cost-estimator" rel="noopener noreferrer"&gt;software cost estimator&lt;/a&gt;, the same model behind every WavX software planning range. The method is simple: take the base for the type of build, add the listed features, and show the result as a range from 15% below to 25% above that subtotal.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Assumptions taken from the cost model&lt;/th&gt;
&lt;th&gt;Subtotal&lt;/th&gt;
&lt;th&gt;Planning range&lt;/th&gt;
&lt;th&gt;Planning timeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small private app. One job, such as pushing paid orders into an accounting system or tagging customers by a rule&lt;/td&gt;
&lt;td&gt;Internal tool base ₹2,00,000 + third-party integrations ₹60,000&lt;/td&gt;
&lt;td&gt;₹2,60,000&lt;/td&gt;
&lt;td&gt;₹2,21,000 to ₹3,25,000&lt;/td&gt;
&lt;td&gt;3–6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration app with reporting. Two-way sync with an ERP, courier or CRM, a status screen and exports&lt;/td&gt;
&lt;td&gt;Internal tool base ₹2,00,000 + third-party integrations ₹60,000 + real-time ₹70,000 + exports and reporting ₹50,000&lt;/td&gt;
&lt;td&gt;₹3,80,000&lt;/td&gt;
&lt;td&gt;₹3,23,000 to ₹4,75,000&lt;/td&gt;
&lt;td&gt;6–10 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full custom app. Embedded admin app with its own database, staff roles, an admin panel and storefront or checkout extensions&lt;/td&gt;
&lt;td&gt;Custom web application base ₹4,50,000 + auth and roles ₹60,000 + third-party integrations ₹60,000 + admin panel ₹90,000&lt;/td&gt;
&lt;td&gt;₹6,60,000&lt;/td&gt;
&lt;td&gt;₹5,61,000 to ₹8,25,000&lt;/td&gt;
&lt;td&gt;10–16 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public App Store app. A product for many merchants, with billing and onboarding&lt;/td&gt;
&lt;td&gt;SaaS product base ₹7,00,000 + auth and roles ₹60,000 + payments and billing ₹90,000 + admin panel ₹90,000 + third-party integrations ₹60,000&lt;/td&gt;
&lt;td&gt;₹10,00,000&lt;/td&gt;
&lt;td&gt;₹8,50,000 to ₹12,50,000&lt;/td&gt;
&lt;td&gt;10–16 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row assumes the model's small scale (multiplier 1.0) and standard design (multiplier 1.0). The growing scale multiplies the subtotal by 1.3 and the large scale by 1.7. Timelines are the model's bands: a subtotal under ₹3,00,000 maps to 3–6 weeks, under ₹6,00,000 to 6–10 weeks, and anything above to 10–16 weeks.&lt;/p&gt;

&lt;p&gt;Three caveats belong next to this table.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first two rows use the estimator's "internal tool" base. The reasoning is that a back-office sync app with one status screen is closer to an internal tool than to a customer-facing web application. If your app has a substantial interface of its own, read the third row instead.&lt;/li&gt;
&lt;li&gt;The model has one line for third-party integrations. An app that connects four systems is not the same job as one that connects a single system, and the real quote reflects that. The table is for budgeting, and a quote follows a written scope.&lt;/li&gt;
&lt;li&gt;Smaller jobs exist. WavX's &lt;a href="https://www.wavxsolutions.in/blog/shopify-store-setup-cost-in-india" rel="noopener noreferrer"&gt;Shopify store setup cost guide&lt;/a&gt; puts installing and configuring existing App Store apps at ₹40,000 inside its mid-tier example, and the &lt;a href="https://www.wavxsolutions.in/blog/ecommerce-website-cost-in-india" rel="noopener noreferrer"&gt;ecommerce website cost guide&lt;/a&gt; says bespoke integration work typically adds ₹30,000 to ₹1,50,000 to a build. If all you need is a short script or a configuration, it may not be an app project at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What moves the number
&lt;/h2&gt;

&lt;p&gt;The assumptions column is where a real project differs from the table. These are the questions that change the price, roughly in order of effect.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How many systems, and in which direction.&lt;/strong&gt; Sending orders out of Shopify is simpler than keeping stock, prices and orders in step both ways, where two systems can disagree and one has to win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the app shows up.&lt;/strong&gt; An app that only runs in the background is the cheapest. Screens inside the Shopify admin add interface work. Anything the shopper sees on the storefront or in checkout adds design, testing on real devices and stricter performance limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether it holds its own data.&lt;/strong&gt; An app that stores nothing and only moves data between two APIs is small. An app with its own database needs backups, access control and a plan for what happens when Shopify and the database disagree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume.&lt;/strong&gt; A few hundred orders a day can be processed one at a time. Sale-day peaks need queues and retries, because Shopify rate-limits its Admin API by plan. Shopify's pricing page lists API rate limits as standard on Basic and Grow, up to 2x on Advanced and up to 10x on Plus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The other system.&lt;/strong&gt; A documented API with a sandbox is quick to integrate. Desktop accounting software, a courier with a spreadsheet upload, or an ERP nobody has credentials for can each take longer than the Shopify side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure handling.&lt;/strong&gt; What should happen when the ERP is down for an hour? Deciding that, and building the retry and the alert, is a large part of any integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Shopify's own fees that sit beside the build
&lt;/h2&gt;

&lt;p&gt;The build cost is paid once. Shopify's fees are paid every month and are set by Shopify, so they are quoted here exactly as shown on Shopify's India pricing page on 2 October 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price, billed monthly&lt;/th&gt;
&lt;th&gt;Price per month, billed yearly&lt;/th&gt;
&lt;th&gt;Fee on third-party payment providers&lt;/th&gt;
&lt;th&gt;API rate limits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;₹1,994&lt;/td&gt;
&lt;td&gt;₹1,499&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grow&lt;/td&gt;
&lt;td&gt;₹7,447&lt;/td&gt;
&lt;td&gt;₹5,599&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced&lt;/td&gt;
&lt;td&gt;₹30,164&lt;/td&gt;
&lt;td&gt;₹22,680&lt;/td&gt;
&lt;td&gt;0.6%&lt;/td&gt;
&lt;td&gt;Up to 2x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plus&lt;/td&gt;
&lt;td&gt;From ₹1,75,000&lt;/td&gt;
&lt;td&gt;From ₹1,75,000&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;td&gt;Up to 10x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three points about these figures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The transaction fee matters more in India than elsewhere.&lt;/strong&gt; Shopify Payments is not on Shopify's list of supported countries for India, so an Indian store uses a third-party gateway and pays the percentage in the table on those orders, on top of the gateway's own fee. Shopify's help centre says manual payment methods, including cash on delivery, are not charged this fee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GST applies.&lt;/strong&gt; Shopify's billing help page says a store located in India is charged GST on its Shopify fees, and that the exemption for a store that provides its GSTIN applies only to invoices issued after the GSTIN is submitted. Add it to the billing profile before the first bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prices change.&lt;/strong&gt; Earlier WavX guides quote plan prices as they stood on the day they were written. Use Shopify's page for the current figure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shopify does not charge a fee for installing a custom app on your own store. App Store apps are different: each sets its own subscription price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan rules that change the scope
&lt;/h2&gt;

&lt;p&gt;Two Shopify rules decide whether a custom app can do what you want on your current plan. Both are quoted from Shopify's developer documentation as of October 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shopify Functions.&lt;/strong&gt; Functions are how an app changes discounts, delivery options, payment methods and cart validation on Shopify's servers. Shopify's documentation says stores on any plan can use public apps from the App Store that contain Functions, and that only stores on a Shopify Plus plan can use custom apps that contain Functions. So a custom discount rule written only for your store needs Plus. On other plans the choices are an App Store app that offers the rule, or a different design. The Scripts to Functions migration guide covers this in detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkout interface.&lt;/strong&gt; Shopify's checkout UI extensions reference says extensions for the information, shipping and payment steps are available only to stores on a Shopify Plus plan.&lt;/p&gt;

&lt;p&gt;For a store on Basic, Grow or Advanced, this means a requirement such as "hide cash on delivery above ₹5,000 with our own rule" may turn a ₹3 lakh app discussion into a decision about a plan that starts at ₹1,75,000 a month. It is better to find that out before the build than during it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs after launch
&lt;/h2&gt;

&lt;p&gt;A Shopify app is not finished when it is installed. Three recurring costs follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hosting.&lt;/strong&gt; Shopify's deployment guide describes deployment as moving your code to a hosting service, and names Vercel, Google Cloud Run, Fly.io and Render as options. The app's web backend runs on hosting you pay for. Extensions and Functions are deployed to Shopify with the Shopify CLI. Hosting is billed by the provider you choose and depends on traffic, so no figure is given here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API versions.&lt;/strong&gt; Shopify releases a new API version every three months and supports each stable version for at least 12 months. When an app calls a version that has been retired, Shopify responds with the oldest version still supported, which may behave differently. In practice someone has to review and update the app at least once a year. Shopify also marks the REST Admin API as legacy since 1 October 2024, and requires new public apps to use the GraphQL Admin API from 1 April 2025, so an older app built on REST carries a migration cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance.&lt;/strong&gt; WavX publishes 15% to 25% of the build cost per year as its maintenance band, explained in the &lt;a href="https://www.wavxsolutions.in/blog/app-maintenance-cost-per-year-india" rel="noopener noreferrer"&gt;app maintenance cost guide&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Planning range for the build&lt;/th&gt;
&lt;th&gt;Maintenance per year at 15% to 25%&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small private app&lt;/td&gt;
&lt;td&gt;₹2,21,000 to ₹3,25,000&lt;/td&gt;
&lt;td&gt;₹33,150 to ₹81,250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration app with reporting&lt;/td&gt;
&lt;td&gt;₹3,23,000 to ₹4,75,000&lt;/td&gt;
&lt;td&gt;₹48,450 to ₹1,18,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full custom app&lt;/td&gt;
&lt;td&gt;₹5,61,000 to ₹8,25,000&lt;/td&gt;
&lt;td&gt;₹84,150 to ₹2,06,250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public App Store app&lt;/td&gt;
&lt;td&gt;₹8,50,000 to ₹12,50,000&lt;/td&gt;
&lt;td&gt;₹1,27,500 to ₹3,12,500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The low figure is 15% of the low end of the range and the high figure is 25% of the high end.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example over three years
&lt;/h2&gt;

&lt;p&gt;Take the second row: an app that keeps orders and stock in step between Shopify and an ERP, with a status screen and exports.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;Low&lt;/th&gt;
&lt;th&gt;High&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build, year one&lt;/td&gt;
&lt;td&gt;₹3,23,000&lt;/td&gt;
&lt;td&gt;₹4,75,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance, year two&lt;/td&gt;
&lt;td&gt;₹48,450&lt;/td&gt;
&lt;td&gt;₹1,18,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance, year three&lt;/td&gt;
&lt;td&gt;₹48,450&lt;/td&gt;
&lt;td&gt;₹1,18,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-year total, before hosting and GST&lt;/td&gt;
&lt;td&gt;₹4,19,900&lt;/td&gt;
&lt;td&gt;₹7,12,500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This follows the cost model's own method, which treats the first year's maintenance as part of the build. It is a model for budgeting. Hosting, Shopify's plan and any fee charged by the other system are extra.&lt;/p&gt;

&lt;p&gt;The useful comparison is the monthly cost of doing the same job by hand or with a subscription app. ₹4,19,900 over 36 months is about ₹11,700 a month, and ₹7,12,500 is about ₹19,800 a month. If the work the app replaces costs less than that, the app does not pay for itself on cost alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the app is going on the App Store
&lt;/h2&gt;

&lt;p&gt;A public app is a product business, and its costs go beyond the build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; A public app goes through Shopify's app review before it can be installed from the App Store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revenue share.&lt;/strong&gt; As of October 2026, Shopify's revenue share page says a developer keeps 100% of the first US$1,000,000 in gross app revenue earned from 1 January 2025 and 85% above that, with a one-time US$19 registration fee per partner account and a 2.9% processing fee on billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support and onboarding.&lt;/strong&gt; Every merchant who installs the app can raise a ticket. That is a running cost a custom app for one store does not have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many stores, many edge cases.&lt;/strong&gt; Themes, plans, currencies and other apps differ from store to store, so testing takes longer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fourth row of the table is a starting point for a first version, not the cost of running an app business.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should not build an app
&lt;/h2&gt;

&lt;p&gt;A page about app pricing should say when the answer is not to buy one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An App Store app already does it.&lt;/strong&gt; Shopify's help centre says apps made by Shopify are usually free, and a third-party app such as Judge.me Product Reviews lists its paid plan at US$15 a month as of October 2026. A custom build rarely beats that on cost. The comparison, with real subscription prices over two years, is in custom Shopify app or App Store app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The process is still changing.&lt;/strong&gt; If the team has not settled how returns or wholesale orders should work, software will fix the wrong process in place. Run it by hand for a month first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The need is seasonal.&lt;/strong&gt; A rule needed for one sale is better handled with Shopify's built-in discounts or a monthly app you cancel afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule needs Plus and you are not on Plus.&lt;/strong&gt; A custom app with Functions will not run on your plan. Look for a public app first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The other system has no way in.&lt;/strong&gt; If the ERP or courier offers no API and no file exchange, the app cannot be built reliably at any price. The first job is on that side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to write down before asking for a quote
&lt;/h2&gt;

&lt;p&gt;A scope on one page gets a firmer number than a conversation. These six answers are enough for any developer to price the work, WavX included.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What the app must do, as a list of events: "when an order is paid, create an invoice in the accounting system".&lt;/li&gt;
&lt;li&gt;Which systems it touches, and whether each has an API and test credentials.&lt;/li&gt;
&lt;li&gt;Your Shopify plan, and whether the app needs to change discounts, shipping, payments or checkout.&lt;/li&gt;
&lt;li&gt;Daily order volume on a normal day and on your biggest sale day.&lt;/li&gt;
&lt;li&gt;Who uses the app's screens, if it has any, and what they need to see.&lt;/li&gt;
&lt;li&gt;What should happen when something fails, and who should be told.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With those, the planning ranges above can be replaced by a quote. The wider Shopify work WavX offers is listed on the &lt;a href="https://www.wavxsolutions.in/services/shopify" rel="noopener noreferrer"&gt;Shopify and app development&lt;/a&gt; page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.shopify.com/in/pricing" rel="noopener noreferrer"&gt;Shopify India pricing: plan prices, third-party payment provider fees, API rate limits&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.shopify.com/en/manual/apps/app-types" rel="noopener noreferrer"&gt;Shopify Help Center: types of apps&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/apps/launch/distribution" rel="noopener noreferrer"&gt;Shopify developer documentation: app distribution&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/apps/build/functions" rel="noopener noreferrer"&gt;Shopify developer documentation: Shopify Functions, plan availability&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/api/checkout-ui-extensions/latest" rel="noopener noreferrer"&gt;Shopify developer documentation: checkout UI extensions&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/api/usage/versioning" rel="noopener noreferrer"&gt;Shopify developer documentation: API versioning&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/api/admin-rest" rel="noopener noreferrer"&gt;Shopify developer documentation: REST Admin API legacy notice&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/apps/launch/deployment" rel="noopener noreferrer"&gt;Shopify developer documentation: app deployment&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/apps/launch/distribution/revenue-share" rel="noopener noreferrer"&gt;Shopify developer documentation: App Store revenue share&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.shopify.com/judgeme" rel="noopener noreferrer"&gt;Shopify App Store: Judge.me Product Reviews pricing&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.shopify.com/en/manual/payments/shopify-payments/supported-countries" rel="noopener noreferrer"&gt;Shopify Help Center: Shopify Payments supported countries&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.shopify.com/en/manual/your-account/manage-billing/billing-charges/types-of-charges/third-party-charges/third-party-transaction-fees" rel="noopener noreferrer"&gt;Shopify Help Center: third-party transaction fees&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.shopify.com/en/manual/your-account/manage-billing/billing-charges/types-of-charges/taxes/billing-taxes-india-malaysia-singapore-thailand" rel="noopener noreferrer"&gt;Shopify Help Center: taxes on Shopify bills for India&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>costguide</category>
      <category>india</category>
    </item>
    <item>
      <title>Service Workers and Caching Strategies, Explained with Code</title>
      <dc:creator>WAVX solutions</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:49:20 +0000</pubDate>
      <link>https://dev.to/wavx_solutions_1c671328ad/service-workers-and-caching-strategies-explained-with-code-1a0h</link>
      <guid>https://dev.to/wavx_solutions_1c671328ad/service-workers-and-caching-strategies-explained-with-code-1a0h</guid>
      <description>&lt;p&gt;A service worker is a JavaScript file the browser runs in the background, separate from your pages, that can intercept every network request those pages make. A caching strategy is the rule it applies to one kind of request: answer from a cache, go to the network, or combine the two. Most sites need three rules: cache first for versioned CSS, JavaScript and fonts; network first for HTML pages, with an offline fallback; and stale-while-revalidate for images. Anything that changes data or involves money goes straight to the network.&lt;/p&gt;

&lt;p&gt;The code in this guide is written against the Service Worker and Cache APIs as documented on MDN, web.dev and Chrome for Developers on 2 October 2026. It is an example for this article, not a published, device-tested demo. Run it in the browsers your visitors use before you ship it.&lt;/p&gt;

&lt;p&gt;If the term is new, start with the short definition of a service worker, or with what a progressive web app is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a service worker is allowed to do
&lt;/h2&gt;

&lt;p&gt;Four rules explain most of the surprises.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS only.&lt;/strong&gt; MDN states that service workers are restricted to running across HTTPS. &lt;code&gt;localhost&lt;/code&gt; counts as secure so that you can develop locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope follows the file's location.&lt;/strong&gt; By default a worker controls only URLs in or below the directory it is served from. A file at &lt;code&gt;/js/sw.js&lt;/code&gt; controls &lt;code&gt;/js/&lt;/code&gt; and nothing else. To control the whole site, serve it from the root as &lt;code&gt;/sw.js&lt;/code&gt;. A server can widen the scope with the &lt;code&gt;Service-Worker-Allowed&lt;/code&gt; response header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not control the page that installs it.&lt;/strong&gt; On the first visit the page loads without a worker, registers one, and stays uncontrolled until the next load unless the worker calls &lt;code&gt;clients.claim()&lt;/code&gt;. Do not judge your caching on a first visit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is event-driven.&lt;/strong&gt; The browser starts the worker when an event such as &lt;code&gt;fetch&lt;/code&gt; or &lt;code&gt;push&lt;/code&gt; arrives and may stop it afterwards. Treat variables in the worker as temporary and keep anything that must survive in the Cache API or IndexedDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The five caching strategies
&lt;/h2&gt;

&lt;p&gt;These names come from Jake Archibald's Offline Cookbook on web.dev and are used the same way in the Chrome and MDN documentation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;How fresh&lt;/th&gt;
&lt;th&gt;Works offline&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache only&lt;/td&gt;
&lt;td&gt;Answers from the cache and never touches the network&lt;/td&gt;
&lt;td&gt;As fresh as the last release&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Files stored at install: the offline page, the app shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network only&lt;/td&gt;
&lt;td&gt;Passes the request through untouched&lt;/td&gt;
&lt;td&gt;Always current&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;POST requests, cart, checkout, payments, analytics pings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache first, falling back to network&lt;/td&gt;
&lt;td&gt;Looks in the cache; on a miss, fetches and stores&lt;/td&gt;
&lt;td&gt;Stale until the file name or cache name changes&lt;/td&gt;
&lt;td&gt;Yes, once stored&lt;/td&gt;
&lt;td&gt;Hashed CSS, JavaScript and fonts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network first, falling back to cache&lt;/td&gt;
&lt;td&gt;Tries the network; on failure, serves the last stored copy&lt;/td&gt;
&lt;td&gt;Current whenever online&lt;/td&gt;
&lt;td&gt;Yes, for pages already visited&lt;/td&gt;
&lt;td&gt;HTML pages, API reads that should be current&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale-while-revalidate&lt;/td&gt;
&lt;td&gt;Serves the cached copy at once and refreshes the cache in the background&lt;/td&gt;
&lt;td&gt;One visit behind&lt;/td&gt;
&lt;td&gt;Yes, once stored&lt;/td&gt;
&lt;td&gt;Product images, avatars, content that can be slightly old&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MDN's caching guide makes the point that matters most: a single app normally uses several of these, one per type of resource. Choosing one strategy for the whole site is the usual beginner mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete service worker
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;sw.js&lt;/code&gt; at the root of the site. It stores an offline page and two core files at install, removes old caches when a new version activates, and routes each request to one of three strategies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sw.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STATIC_CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`static-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PAGE_CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`pages-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMAGE_CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`images-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OFFLINE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/offline.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PRECACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OFFLINE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/styles/main.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/scripts/app.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NEVER_CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STATIC_CACHE&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PRECACHE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;activate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;STATIC_CACHE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PAGE_CACHE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IMAGE_CACHE&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;putInCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cacheFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;putInCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;networkFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;putInCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OFFLINE_URL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;putInCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Leave non-GET and cross-origin requests to the browser.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Network only: never store cart, checkout, account or API traffic.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NEVER_CACHE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navigate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;networkFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PAGE_CACHE&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;font&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cacheFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;STATIC_CACHE&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IMAGE_CACHE&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it from your pages. The feature check keeps older browsers working as before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/sw.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Service worker registration failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also need a plain &lt;code&gt;/offline.html&lt;/code&gt; page that does not depend on anything outside the precache list.&lt;/p&gt;

&lt;h2&gt;
  
  
  How each part behaves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install.&lt;/strong&gt; &lt;code&gt;cache.addAll()&lt;/code&gt; is all or nothing. MDN documents that it rejects if any response is outside the 200 range, and when the promise passed to &lt;code&gt;event.waitUntil()&lt;/code&gt; rejects, the install fails. One wrong path in &lt;code&gt;PRECACHE&lt;/code&gt; therefore means no service worker at all. Keep the list short and check every entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activate.&lt;/strong&gt; MDN recommends cleaning up old caches in the &lt;code&gt;activate&lt;/code&gt; event, because at that point no earlier version of the worker is running. The &lt;code&gt;VERSION&lt;/code&gt; constant is what makes this work: change it and every old cache is deleted on activation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache first.&lt;/strong&gt; Fast, and dangerous with file names that never change. If &lt;code&gt;/styles/main.css&lt;/code&gt; is stored under cache first, visitors keep the old file until &lt;code&gt;VERSION&lt;/code&gt; changes. Either bump &lt;code&gt;VERSION&lt;/code&gt; on every release or, better, use hashed file names such as &lt;code&gt;main.4f9a1c.css&lt;/code&gt;, which most build tools produce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network first.&lt;/strong&gt; The right default for HTML. Online visitors always get the current page; offline visitors get the last copy they saw, and the offline page for anything they have not visited. The cost is that a slow connection is still slow, because the cache is consulted only after the network fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale-while-revalidate.&lt;/strong&gt; The visitor sees the stored image at once and the worker refreshes the cache behind it, so a changed image appears on the following visit. Do not use it for prices, stock levels or anything a customer acts on.&lt;/p&gt;

&lt;p&gt;Three details in the code are easy to get wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response.clone()&lt;/code&gt;&lt;/strong&gt;. A response body can be read once. The clone goes to the cache and the original goes to the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response.ok&lt;/code&gt;&lt;/strong&gt;. Without this check a 404 or 500 page gets cached and served again. It also skips opaque cross-origin responses, whose reported status is 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;event.waitUntil()&lt;/code&gt;&lt;/strong&gt;. It tells the browser the worker still has work to finish after the response has been returned. MDN's own example uses the same pattern for the cache write.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What must never be cached
&lt;/h2&gt;

&lt;p&gt;The early &lt;code&gt;return&lt;/code&gt; statements in the fetch handler matter more than the strategies. Requests that fall through are handled by the browser as if no worker existed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anything that is not a GET request. MDN notes that caching is never appropriate for requests such as POST.&lt;/li&gt;
&lt;li&gt;Cart, checkout and payment pages and the calls behind them.&lt;/li&gt;
&lt;li&gt;Login, account and order-history pages. A page cached for one user must never be shown to another on a shared device.&lt;/li&gt;
&lt;li&gt;API responses that carry prices, stock, balances or anything else that must be current.&lt;/li&gt;
&lt;li&gt;Third-party scripts for payments and analytics. Let them load normally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On an online store, serving a stale price from a cache is worse than showing an error. The guide to PWAs for e-commerce in India covers the split for a storefront.&lt;/p&gt;

&lt;h2&gt;
  
  
  How updates reach users
&lt;/h2&gt;

&lt;p&gt;This is the part that catches teams after launch.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The browser checks for a new worker&lt;/strong&gt; on every navigation to a page in scope, and on events such as &lt;code&gt;push&lt;/code&gt; unless it checked in the previous 24 hours. The lifecycle article on web.dev notes that most browsers ignore HTTP caching headers for this check by default; the &lt;code&gt;updateViaCache&lt;/code&gt; option on &lt;code&gt;register()&lt;/code&gt; controls it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any byte of difference counts.&lt;/strong&gt; If the new &lt;code&gt;sw.js&lt;/code&gt; differs at all, it is installed alongside the old one. Changing &lt;code&gt;VERSION&lt;/code&gt; is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The new worker waits.&lt;/strong&gt; It activates only when the old worker controls no open pages. A visitor who keeps a tab open keeps the old version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can skip the wait.&lt;/strong&gt; Calling &lt;code&gt;self.skipWaiting()&lt;/code&gt; in the install handler activates the new worker immediately. The risk is that a page loaded with old HTML is suddenly served by new logic and new assets. Skip waiting only if your pages tolerate that, or prompt the visitor to reload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whichever you choose, write it down. When someone asks why the old version is still showing after a release, the answer is almost always in this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage limits and eviction
&lt;/h2&gt;

&lt;p&gt;A cache is borrowed space. As of October 2026, MDN documents these limits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Per-origin limit&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome, Edge and other Chromium browsers&lt;/td&gt;
&lt;td&gt;Up to 60% of total disk size&lt;/td&gt;
&lt;td&gt;Same in best-effort and persistent modes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox&lt;/td&gt;
&lt;td&gt;The smaller of 10% of disk or 10 GiB&lt;/td&gt;
&lt;td&gt;Up to 50% of disk if persistent storage is granted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari, from iOS 17 and macOS 14&lt;/td&gt;
&lt;td&gt;Around 60% of total disk for browser apps&lt;/td&gt;
&lt;td&gt;Around 15% for other apps that embed web content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a device runs short of space, browsers delete data for the least recently used origins first. Safari also deletes script-created data, which includes service worker registrations and caches, for a site with no user interaction in seven days of browser use when cross-site tracking prevention is on. &lt;code&gt;navigator.storage.estimate()&lt;/code&gt; reports approximate usage and &lt;code&gt;navigator.storage.persist()&lt;/code&gt; asks the browser not to evict your origin.&lt;/p&gt;

&lt;p&gt;The practical rule: cache only what can be downloaded again, and never treat the cache as the only copy of user data. iPhone specifics are in &lt;a href="https://www.wavxsolutions.in/blog/pwa-on-ios-what-works" rel="noopener noreferrer"&gt;PWAs on iPhone: what works and what does not&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two newer options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Navigation preload&lt;/strong&gt; lets the browser start the network request for a page while the service worker is still starting up, which removes a delay from network-first navigations. MDN's compatibility data lists it from Chrome 59, Firefox 99 and Safari 15.4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The static routing API&lt;/strong&gt; (&lt;code&gt;InstallEvent.addRoutes()&lt;/code&gt;) lets a worker declare routes that the browser can handle without starting the worker at all. The same data lists it from Chrome 123 and Safari 27, released in September 2026, and not in Firefox, so treat it as an optimisation and not something to depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hand-written or a library
&lt;/h2&gt;

&lt;p&gt;Workbox, described in the Chrome documentation as production-ready service worker libraries and tooling, wraps the same strategies in modules for routing, precaching and expiry. It earns its place when the precache list should be generated by the build, when caches need size or age limits, or when there are many routes. For Next.js projects, the Next.js documentation names Serwist as one option for service-worker-based offline caching.&lt;/p&gt;

&lt;p&gt;For a site with three or four rules, a hand-written file like the one above is easier to reason about, and nothing in it is hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing before release
&lt;/h2&gt;

&lt;p&gt;Chrome DevTools has what you need in the Application panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service Workers&lt;/strong&gt; shows the registered worker and its state, with controls to go offline, update on reload, bypass the worker for network, and unregister it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Storage&lt;/strong&gt; lists every cache and the responses in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear storage&lt;/strong&gt; unregisters workers and wipes caches in one click, so you can repeat a first visit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test four things: a first visit, a repeat visit, a repeat visit with the Offline box ticked, and a release with a new &lt;code&gt;VERSION&lt;/code&gt;. Then repeat on a real Android phone and a real iPhone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A kill switch
&lt;/h2&gt;

&lt;p&gt;A faulty service worker keeps serving whatever it cached, to every returning visitor, until it is replaced. Chrome's documentation on removing buggy service workers recommends deploying a replacement worker that has no &lt;code&gt;fetch&lt;/code&gt; handler, so that the browser handles every request itself. The file below follows that advice and also deletes the caches and unregisters the worker. Keep it ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sw.js (emergency replacement)&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipWaiting&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;activate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unregister&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})()&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy it at the same URL as the worker it replaces. The browser picks it up at its next update check, which is why the fix is a new file at that address and not a change anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you do not need a service worker
&lt;/h2&gt;

&lt;p&gt;A service worker adds a second cache with its own rules, and a new class of bug. It is not worth it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The site is a brochure or a blog that visitors read once. Correct HTTP cache headers and a CDN give most of the speed benefit with none of the risk.&lt;/li&gt;
&lt;li&gt;Nobody will own it. A worker written at launch and never revisited is how sites end up serving a months-old home page.&lt;/li&gt;
&lt;li&gt;The real problem is a slow first load. A service worker cannot help a first visit, because it is not in control yet. Fix images, scripts and server response time first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is worth it when visitors return often, when connections drop in the middle of a task, or when the site should be installable and usable offline. That is the case for a &lt;a href="https://www.wavxsolutions.in/pwa-development" rel="noopener noreferrer"&gt;progressive web app&lt;/a&gt;, and it is the work WavX takes on under &lt;a href="https://www.wavxsolutions.in/services/web/pwa-development" rel="noopener noreferrer"&gt;PWA development&lt;/a&gt;, including the caching rules, the update plan and the testing described here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers" rel="noopener noreferrer"&gt;MDN: Using Service Workers&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching" rel="noopener noreferrer"&gt;MDN: Caching (Progressive web apps guide)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register" rel="noopener noreferrer"&gt;MDN: ServiceWorkerContainer.register()&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Cache/addAll" rel="noopener noreferrer"&gt;MDN: Cache.addAll()&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria" rel="noopener noreferrer"&gt;MDN: Storage quotas and eviction criteria&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mdn/browser-compat-data" rel="noopener noreferrer"&gt;MDN browser-compat-data, the data behind MDN's compatibility tables (NavigationPreloadManager, InstallEvent.addRoutes)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/articles/offline-cookbook" rel="noopener noreferrer"&gt;web.dev: The Offline Cookbook&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/articles/service-worker-lifecycle" rel="noopener noreferrer"&gt;web.dev: The service worker lifecycle&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/workbox/caching-strategies-overview" rel="noopener noreferrer"&gt;Chrome for Developers: Strategies for service worker caching&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/workbox" rel="noopener noreferrer"&gt;Chrome for Developers: Workbox&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/workbox/remove-buggy-service-workers" rel="noopener noreferrer"&gt;Chrome for Developers: Removing buggy service workers&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/devtools/progressive-web-apps" rel="noopener noreferrer"&gt;Chrome for Developers: Debug Progressive Web Apps&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/" rel="noopener noreferrer"&gt;WebKit: Full Third-Party Cookie Blocking and More (seven-day cap on script-writable storage)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/guides/progressive-web-apps" rel="noopener noreferrer"&gt;Next.js documentation: Progressive Web Applications&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pwa</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>PWAs on iPhone: What Works and What Does Not (October 2026)</title>
      <dc:creator>WAVX solutions</dc:creator>
      <pubDate>Sun, 04 Oct 2026 05:48:41 +0000</pubDate>
      <link>https://dev.to/wavx_solutions_1c671328ad/pwas-on-iphone-what-works-and-what-does-not-october-2026-5cgf</link>
      <guid>https://dev.to/wavx_solutions_1c671328ad/pwas-on-iphone-what-works-and-what-does-not-october-2026-5cgf</guid>
      <description>&lt;p&gt;As of October 2026, per the sources below, a progressive web app on iPhone can be added to the Home Screen, open in its own window without browser controls, work offline through a service worker, send push notifications and show a badge on its icon. It cannot show an automatic install prompt, sync data in the background, or reach Bluetooth, NFC or USB devices. Push notifications work only after the user has added the app to the Home Screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this page was compiled.&lt;/strong&gt; Every row below comes from WebKit's release posts, Apple's developer documentation or MDN's browser compatibility data, all read on 2 October 2026. Safari 27, which ships with iOS 27, was released in September 2026 and is the current version. This is a reading of documentation, not a device test. A checklist for confirming the items that matter to your product is at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support table: installing and launching
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;On iPhone&lt;/th&gt;
&lt;th&gt;Since&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add to Home Screen from Safari&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Long-standing&lt;/td&gt;
&lt;td&gt;From iOS 26, every site added to the Home Screen opens as a web app by default. The user can switch off "Open as Web App".&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add to Home Screen from Chrome, Edge, Firefox&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 16.4&lt;/td&gt;
&lt;td&gt;Through the Share menu of those browsers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic install prompt (&lt;code&gt;beforeinstallprompt&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;The event exists only in Chromium browsers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Own window (&lt;code&gt;display: standalone&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 11.3&lt;/td&gt;
&lt;td&gt;MDN's data lists &lt;code&gt;fullscreen&lt;/code&gt; and &lt;code&gt;minimal-ui&lt;/code&gt; as unsupported in Safari&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Icons from the manifest&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 15.4&lt;/td&gt;
&lt;td&gt;Used only when the page has no &lt;code&gt;apple-touch-icon&lt;/code&gt; and the icon's &lt;code&gt;purpose&lt;/code&gt; is &lt;code&gt;any&lt;/code&gt; or unset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest &lt;code&gt;id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 16.4&lt;/td&gt;
&lt;td&gt;Lets the same app be installed more than once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest &lt;code&gt;shortcuts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Listed for Safari on Mac from 17.4, not for iOS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Support table: offline, storage and background work
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;On iPhone&lt;/th&gt;
&lt;th&gt;Since&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service worker and Cache API&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 11.3&lt;/td&gt;
&lt;td&gt;The basis of offline support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage quota&lt;/td&gt;
&lt;td&gt;Around 60% of disk per origin&lt;/td&gt;
&lt;td&gt;iOS 17&lt;/td&gt;
&lt;td&gt;A Home Screen web app has the same quota as the site in the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent storage request&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 15.2&lt;/td&gt;
&lt;td&gt;Safari decides using heuristics, including whether the site is opened as a Home Screen web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigation preload&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 15.4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service worker static routing&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 27&lt;/td&gt;
&lt;td&gt;New in Safari 27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background Sync&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Periodic Background Sync&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background Fetch&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Support table: notifications
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;On iPhone&lt;/th&gt;
&lt;th&gt;Since&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web Push&lt;/td&gt;
&lt;td&gt;Works, with conditions&lt;/td&gt;
&lt;td&gt;iOS 16.4&lt;/td&gt;
&lt;td&gt;Home Screen web apps only; permission must follow a tap; every push must show a notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declarative Web Push&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 18.4&lt;/td&gt;
&lt;td&gt;Home Screen web apps; no service worker needed to display the notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Badge on the app icon (Badging API)&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 16.4&lt;/td&gt;
&lt;td&gt;Home Screen web apps only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Focus modes&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 16.4&lt;/td&gt;
&lt;td&gt;Web app notifications follow the user's Focus settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action buttons, image and vibration pattern on a notification&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;MDN's data lists these notification options as unsupported in Safari&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Support table: device features
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;On iPhone&lt;/th&gt;
&lt;th&gt;Since&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Camera and microphone (&lt;code&gt;navigator.mediaDevices&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 11&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geolocation&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Long-standing&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Share&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 12.2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen Wake Lock&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;iOS 16.4 in Safari, iOS 18.4 in Home Screen web apps&lt;/td&gt;
&lt;td&gt;Did not work in Home Screen web apps before 18.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Bluetooth&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web NFC&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebUSB&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vibration API&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact Picker&lt;/td&gt;
&lt;td&gt;Off by default&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Behind a preference flag from iOS 14.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect an installed native app (&lt;code&gt;getInstalledRelatedApps&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Installing: there is no prompt, so explain it
&lt;/h2&gt;

&lt;p&gt;On Android, Chrome fires &lt;code&gt;beforeinstallprompt&lt;/code&gt; and a site can show its own install button. Safari does not support that event, and MDN's installability guide states plainly that it is not supported on iOS. On iPhone the user installs by hand:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the Share menu in the browser.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Add to Home Screen&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Confirm the name and tap &lt;strong&gt;Add&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;WebKit's notes for Safari 26 describe what changed last year. Before iOS 26, a site opened as a web app only if it had a manifest with a suitable &lt;code&gt;display&lt;/code&gt; value or the older Apple meta tag; otherwise the Home Screen icon opened a browser tab. From iOS 26, every site added to the Home Screen opens as a web app by default, and WebKit describes this as zero requirements for installability in Safari.&lt;/p&gt;

&lt;p&gt;Keep the manifest anyway. It still sets the name, icon and start address, and visitors on older iOS versions need &lt;code&gt;"display": "standalone"&lt;/code&gt; to get the app window at all.&lt;/p&gt;

&lt;p&gt;Since most people do not know the Share menu route, show a hint to iPhone visitors who have not installed yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ios-install-hint"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  To install this app: tap the Share button, then "Add to Home Screen".
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isIPhone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/iPhone/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isInstalled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(display-mode: standalone)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isIPhone&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isInstalled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ios-install-hint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If install is not working at all, the checks are in &lt;a href="https://www.wavxsolutions.in/problems/pwa-not-installing" rel="noopener noreferrer"&gt;PWA not installing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push notifications on iPhone: the exact conditions
&lt;/h2&gt;

&lt;p&gt;Web Push arrived on iPhone in iOS 16.4. It uses the same standards as other browsers: the Push API, the Notifications API and a service worker. The conditions are what catch people out.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app must be on the Home Screen.&lt;/strong&gt; MDN's data notes that on iOS the &lt;code&gt;Notification&lt;/code&gt; interface is undefined unless the page is a web app saved to the Home Screen. In a Safari tab there is nothing to call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission must follow a tap.&lt;/strong&gt; Apple's documentation says to give the user a control such as a button and to call the subscription method immediately from that gesture's event handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every push must be shown.&lt;/strong&gt; Apple states that Safari does not support invisible push notifications, and that if you do not present a notification Safari revokes the push permission for your site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No developer account is needed.&lt;/strong&gt; Apple's documentation says you do not need to join the Apple Developer Program to send web push notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your server must reach Apple's push service.&lt;/strong&gt; If outbound traffic is restricted, Apple says to allow &lt;code&gt;https://*.push.apple.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once granted, WebKit says these notifications behave like those from other apps: they show on the Lock Screen, in Notification Center and on a paired Apple Watch.&lt;/p&gt;

&lt;p&gt;The subscription code is the same as for any browser. The service worker registration is fetched before the click so that &lt;code&gt;subscribe()&lt;/code&gt; runs directly inside the tap handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Page script (type="module"). VAPID_PUBLIC_KEY is your server's public key.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;urlBase64ToUint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64String&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#enable-alerts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pushSupported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PushManager&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pushSupported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// On iPhone this is the case in a Safari tab. Ask the user to install first.&lt;/span&gt;
  &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;userVisibleOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;applicationServerKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;urlBase64ToUint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VAPID_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/push/subscribe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Push subscription failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Declarative Web Push, from iOS 18.4
&lt;/h3&gt;

&lt;p&gt;WebKit added Declarative Web Push in iOS 18.4 for web apps on the Home Screen. The push message itself is a JSON description of the notification, and the browser displays it without running any JavaScript. The format in WebKit's announcement looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"web_push"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8030&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your order has shipped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tap to see tracking details."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"navigate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.example.com/orders/1042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"silent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"app_badge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebKit designed it to be backwards compatible: a browser that understands the format handles it itself, and an older browser passes the same message to your service worker. A &lt;code&gt;push&lt;/code&gt; handler that reads the same shape covers both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In sw.js&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notificationclick&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Offline and storage on iPhone
&lt;/h2&gt;

&lt;p&gt;Service workers have been in Safari on iOS since 11.3, so the caching rules in &lt;a href="https://www.wavxsolutions.in/blog/service-workers-caching-strategies" rel="noopener noreferrer"&gt;service workers and caching strategies&lt;/a&gt; apply. Three iPhone-specific points:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quota is generous, eviction is the risk.&lt;/strong&gt; WebKit's storage policy, in force from iOS 17, allows an origin up to around 60% of total disk in a browser app, and a Home Screen web app gets the same. WebKit may still evict data when the overall quota is exceeded, when the device is under storage pressure, or when the user has not interacted with the site for some time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The seven-day rule is aimed at browser tabs.&lt;/strong&gt; Safari deletes a site's script-writable storage, including service worker registrations and caches, after seven days of Safari use without interaction on that site. WebKit's post introducing the rule says web apps added to the Home Screen have their own counter based on actual use of the app, and that it does not expect their first-party data to be deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An installed app has its own storage.&lt;/strong&gt; web.dev's installation guide says each installation on Apple devices has isolated storage. Plan for a visitor who was signed in on your site in Safari to sign in again inside the installed app.&lt;/p&gt;

&lt;p&gt;Because Background Sync is not available, anything queued offline has to be sent while the app is open. Detect the feature and fall back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Page script (type="module"). flushOutbox() sends whatever is queued.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send-outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// iPhone: retry when the app is open and the connection returns.&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flushOutbox&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;flushOutbox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the gaps rule out
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your product needs&lt;/th&gt;
&lt;th&gt;On iPhone as a PWA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Uploads or sync that finish after the app is closed&lt;/td&gt;
&lt;td&gt;Not possible. Work completes only while the app is open.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluetooth devices, NFC tags or USB hardware&lt;/td&gt;
&lt;td&gt;Not possible. This needs a native app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A one-tap install button&lt;/td&gt;
&lt;td&gt;Not possible. The user must use the Share menu.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notification buttons such as "Reply" or "Mark done"&lt;/td&gt;
&lt;td&gt;Not available. A tap can only open the app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A listing people can find in the App Store&lt;/td&gt;
&lt;td&gt;A PWA is not listed there. This needs a native app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content, catalogue, booking, ordering, account pages&lt;/td&gt;
&lt;td&gt;Works.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline reading of pages already visited&lt;/td&gt;
&lt;td&gt;Works.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order and reminder notifications for installed users&lt;/td&gt;
&lt;td&gt;Works, from iOS 16.4.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How iPhone support has changed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;iOS version&lt;/th&gt;
&lt;th&gt;What arrived&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;11.3&lt;/td&gt;
&lt;td&gt;Service workers; &lt;code&gt;display: standalone&lt;/code&gt; from the manifest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12.2&lt;/td&gt;
&lt;td&gt;Web Share&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15.4&lt;/td&gt;
&lt;td&gt;Manifest icons; navigation preload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16.4&lt;/td&gt;
&lt;td&gt;Web Push, Badging API, Add to Home Screen from third-party browsers, manifest &lt;code&gt;id&lt;/code&gt;, Screen Wake Lock in Safari&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;New storage quotas; Add to Home Screen from in-app Safari views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18.4&lt;/td&gt;
&lt;td&gt;Declarative Web Push; Screen Wake Lock inside Home Screen web apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;Every site added to the Home Screen opens as a web app by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;Service worker static routing API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Support has moved in one direction since 2023, but the missing items in the tables above have stayed missing through several releases. Plan around what exists today.&lt;/p&gt;

&lt;h2&gt;
  
  
  PWA or native app for iPhone users
&lt;/h2&gt;

&lt;p&gt;Start with how many of your users this affects. Statcounter puts iOS at 6.05% of mobile web usage in India in September 2026, against 93.89% for Android. For a product aimed at the Indian mass market, the iPhone gaps touch a small share of users. For a premium, urban or export audience the share can be far higher, so check your own analytics before deciding.&lt;/p&gt;

&lt;p&gt;A PWA is usually enough on iPhone when the product is content, commerce, booking or an internal tool, and when notifications are useful but not critical. WavX builds these under &lt;a href="https://www.wavxsolutions.in/services/web/pwa-development" rel="noopener noreferrer"&gt;PWA development&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A native app is the better choice when the product depends on background work, hardware over Bluetooth or NFC, App Store discovery, or notifications that must reach every user and not only those who installed from the Share menu. That is &lt;a href="https://www.wavxsolutions.in/services/mobile" rel="noopener noreferrer"&gt;mobile app development&lt;/a&gt;, and it costs more to build and maintain.&lt;/p&gt;

&lt;p&gt;Many products do both in sequence: a PWA first, a native app later if usage justifies it. The broader comparison is in &lt;a href="https://www.wavxsolutions.in/blog/pwa-vs-native-app-2026" rel="noopener noreferrer"&gt;PWA vs native app&lt;/a&gt; and &lt;a href="https://www.wavxsolutions.in/blog/website-vs-app-vs-pwa-what-to-build" rel="noopener noreferrer"&gt;website vs app vs PWA&lt;/a&gt;. If you already have a site, turning it into a PWA is the smaller first step. The full set of PWA guides is on the &lt;a href="https://www.wavxsolutions.in/pwa-development" rel="noopener noreferrer"&gt;PWA development hub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check it on your own devices
&lt;/h2&gt;

&lt;p&gt;Documentation and behaviour can differ, and iOS changes several times a year. Before you commit to a plan, run these on a real iPhone and write down the iOS version and the date:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the site to the Home Screen from Safari and from one other browser. Does it open in its own window with the right name and icon?&lt;/li&gt;
&lt;li&gt;Turn on aeroplane mode and open it. Does the offline page or cached content appear?&lt;/li&gt;
&lt;li&gt;Subscribe to push from inside the installed app, lock the phone and send a test message.&lt;/li&gt;
&lt;li&gt;Set and clear the icon badge.&lt;/li&gt;
&lt;li&gt;Sign in, close the app, wait a week and open it again. Are you still signed in?&lt;/li&gt;
&lt;li&gt;Repeat on the oldest iOS version you intend to support.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/" rel="noopener noreferrer"&gt;WebKit: Web Push for Web Apps on iOS and iPadOS&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/13966/webkit-features-in-safari-16-4/" rel="noopener noreferrer"&gt;WebKit: WebKit Features in Safari 16.4&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/14445/webkit-features-in-safari-17-0/" rel="noopener noreferrer"&gt;WebKit: WebKit Features in Safari 17.0&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/14403/updates-to-storage-policy/" rel="noopener noreferrer"&gt;WebKit: Updates to Storage Policy&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/" rel="noopener noreferrer"&gt;WebKit: Full Third-Party Cookie Blocking and More&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/16535/meet-declarative-web-push/" rel="noopener noreferrer"&gt;WebKit: Meet Declarative Web Push&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/16574/webkit-features-in-safari-18-4/" rel="noopener noreferrer"&gt;WebKit: WebKit Features in Safari 18.4&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/16993/news-from-wwdc25-web-technology-coming-this-fall-in-safari-26-beta/" rel="noopener noreferrer"&gt;WebKit: News from WWDC25, WebKit in Safari 26 beta&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/17333/webkit-features-in-safari-26-0/" rel="noopener noreferrer"&gt;WebKit: WebKit Features in Safari 26.0&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webkit.org/blog/18325/webkit-features-for-safari-27-0/" rel="noopener noreferrer"&gt;WebKit: WebKit Features for Safari 27.0&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers" rel="noopener noreferrer"&gt;Apple Developer Documentation: Sending web push notifications in web apps and browsers&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mdn/browser-compat-data" rel="noopener noreferrer"&gt;MDN browser-compat-data, the data behind MDN's compatibility tables&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable" rel="noopener noreferrer"&gt;MDN: Making PWAs installable&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeinstallprompt_event" rel="noopener noreferrer"&gt;MDN: Window beforeinstallprompt event&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Offline_and_background_operation" rel="noopener noreferrer"&gt;MDN: Offline and background operation&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/learn/pwa/installation" rel="noopener noreferrer"&gt;web.dev: Learn PWA, Installation&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gs.statcounter.com/os-market-share/mobile/india" rel="noopener noreferrer"&gt;Statcounter Global Stats: Mobile Operating System Market Share in India&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pwa</category>
      <category>webdev</category>
      <category>mobileapps</category>
      <category>appdevelopment</category>
    </item>
    <item>
      <title>How AI answer engines choose what to cite: the evidence, graded</title>
      <dc:creator>WAVX solutions</dc:creator>
      <pubDate>Sat, 03 Oct 2026 17:32:16 +0000</pubDate>
      <link>https://dev.to/wavx_solutions_1c671328ad/how-ai-answer-engines-choose-what-to-cite-the-evidence-graded-5cin</link>
      <guid>https://dev.to/wavx_solutions_1c671328ad/how-ai-answer-engines-choose-what-to-cite-the-evidence-graded-5cin</guid>
      <description>&lt;p&gt;AI answer engines search first and write second. ChatGPT, Google's AI Overviews and AI Mode, Perplexity, Copilot and Claude each run one or more searches, read the pages that come back, and write an answer that links to some of them. So the first condition is plain: the engine's search crawler must be able to fetch your page. Beyond that, no vendor publishes its selection rules, and the published research supports far fewer conclusions than most articles on this subject claim. This review lists each common claim, the source behind it, the date, and how strong the evidence is.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the evidence is graded
&lt;/h2&gt;

&lt;p&gt;Not all evidence is equal. A vendor's documentation tells you its rules but not its ranking. A correlation across thousands of sites tells you what cited pages have in common but not what caused the citation. Each claim below carries one of five grades.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What it can support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Controlled experiment&lt;/td&gt;
&lt;td&gt;One thing was changed and the result compared with an unchanged baseline&lt;/td&gt;
&lt;td&gt;A causal claim, within the conditions tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matched-control study&lt;/td&gt;
&lt;td&gt;Pages that changed are compared with similar pages that did not, on live engines&lt;/td&gt;
&lt;td&gt;A causal claim with caveats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large observational dataset&lt;/td&gt;
&lt;td&gt;Many pages or prompts were measured, with no intervention&lt;/td&gt;
&lt;td&gt;A pattern or correlation, not a cause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor documentation&lt;/td&gt;
&lt;td&gt;The company that runs the engine states how its product works&lt;/td&gt;
&lt;td&gt;The rules for eligibility. Says little about ranking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small sample or anecdote&lt;/td&gt;
&lt;td&gt;A few prompts, one site, or a report from a third party&lt;/td&gt;
&lt;td&gt;A reason to test, nothing more&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most of the independent research comes from companies that sell search and AI visibility software: Ahrefs, Semrush and Profound. Their datasets are large and their methods are published, but they are not neutral parties. That is noted here once and applies throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the engines themselves document
&lt;/h2&gt;

&lt;p&gt;Every engine documents how a page becomes eligible. None documents how it picks between eligible pages.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;What its own documentation states&lt;/th&gt;
&lt;th&gt;Source and date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google AI Overviews and AI Mode&lt;/td&gt;
&lt;td&gt;A page must be indexed and eligible to be shown with a snippet. There are "no additional requirements" and no special optimisation. Both features may use "query fan-out", issuing several related searches. Responses are grounded in pages retrieved by Google's core ranking systems&lt;/td&gt;
&lt;td&gt;Google Search Central, AI features page (last updated 10 December 2025) and generative AI optimisation guide (last updated 10 July 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT search&lt;/td&gt;
&lt;td&gt;OAI-SearchBot is the crawler used to surface sites in ChatGPT search. Sites that opt out of it "will not be shown in ChatGPT search answers", though they may still appear as navigational links. A robots.txt change takes about 24 hours to apply&lt;/td&gt;
&lt;td&gt;OpenAI crawler documentation, read 2 October 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;Claude-SearchBot indexes content for search, and Claude-User fetches pages when a user asks. Blocking either "may reduce your site's visibility" in user search results&lt;/td&gt;
&lt;td&gt;Anthropic help centre, read 2 October 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;PerplexityBot is "designed to surface and link websites in search results on Perplexity" and should be allowed in robots.txt for a site to appear. Perplexity-User fetches pages on a user's request and generally ignores robots.txt&lt;/td&gt;
&lt;td&gt;Perplexity documentation, read 2 October 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot and Bing&lt;/td&gt;
&lt;td&gt;Copilot breaks pages into smaller pieces and assembles answers from several sources. Crawlability, metadata, internal linking and backlinks "remain essential". Microsoft adds that no practice ensures selection&lt;/td&gt;
&lt;td&gt;Microsoft Advertising blog, 8 October 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Grade for all of the above: vendor documentation. It is reliable on eligibility and should be treated as the starting checklist. The crawler names and what each one does are collected in the AI crawler directory.&lt;/p&gt;

&lt;p&gt;One more vendor fact matters. Since 2026 Google has offered a Search Console setting, the Search generative AI control, that includes or excludes a site from AI Overviews and AI Mode. Inclusion is the default. A site that has been excluded there cannot appear, whatever else it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claims, graded
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Source and date&lt;/th&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The engine's search crawler must be able to fetch the page&lt;/td&gt;
&lt;td&gt;Stated by every vendor&lt;/td&gt;
&lt;td&gt;Google, OpenAI, Anthropic, Perplexity documentation&lt;/td&gt;
&lt;td&gt;Vendor documentation. Treat as a requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages cited in AI Overviews usually rank in Google's top 10&lt;/td&gt;
&lt;td&gt;76% of cited pages ranked in the top 10 in July 2025. An updated study found about 38% in March 2026&lt;/td&gt;
&lt;td&gt;Ahrefs, 21 July 2025 and 2 March 2026&lt;/td&gt;
&lt;td&gt;Large observational dataset. The overlap is real and has fallen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT, Gemini and Copilot cite what Google ranks&lt;/td&gt;
&lt;td&gt;Across 15,000 prompts, about 12% of cited links were in Google's top 10 for the same prompt. Perplexity was the exception at 28.6%&lt;/td&gt;
&lt;td&gt;Ahrefs, 11 August 2025&lt;/td&gt;
&lt;td&gt;Large observational dataset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brands mentioned on other sites are mentioned more in AI Overviews&lt;/td&gt;
&lt;td&gt;Across 75,000 brands, branded web mentions had the strongest correlation with AI Overview mentions (0.664)&lt;/td&gt;
&lt;td&gt;Ahrefs, 26 May 2025&lt;/td&gt;
&lt;td&gt;Large observational dataset. Correlation only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding statistics, quotations and cited sources raises visibility&lt;/td&gt;
&lt;td&gt;30 to 40% relative improvement on one visibility measure, in a simulated engine&lt;/td&gt;
&lt;td&gt;Aggarwal and others, KDD 2024 (arXiv 2311.09735)&lt;/td&gt;
&lt;td&gt;Controlled experiment, not run on today's engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Best X" list articles are what ChatGPT cites for recommendations&lt;/td&gt;
&lt;td&gt;Across 750 prompts, such lists were 43.8% of cited page types&lt;/td&gt;
&lt;td&gt;Ahrefs, 4 December 2025&lt;/td&gt;
&lt;td&gt;Observational, with manual categorisation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each engine prefers different sources, and the mix shifts&lt;/td&gt;
&lt;td&gt;Reddit and Wikipedia shares differ by engine and moved sharply within weeks&lt;/td&gt;
&lt;td&gt;Profound, June 2025; Semrush, 10 November 2025&lt;/td&gt;
&lt;td&gt;Large observational datasets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema markup gets pages cited&lt;/td&gt;
&lt;td&gt;No meaningful uplift after 1,885 pages added JSON-LD, against 4,000 controls&lt;/td&gt;
&lt;td&gt;Ahrefs, 11 May 2026&lt;/td&gt;
&lt;td&gt;Matched-control study. The best evidence on this question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An llms.txt file gets pages cited&lt;/td&gt;
&lt;td&gt;97% of published files received no requests in a month. Google states Search ignores the file&lt;/td&gt;
&lt;td&gt;Ahrefs, 15 June 2026; Google, 10 July 2026&lt;/td&gt;
&lt;td&gt;Large log study plus vendor documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An AI "ranking" can be tracked like a search ranking&lt;/td&gt;
&lt;td&gt;The same prompt almost never returned the same list of brands twice&lt;/td&gt;
&lt;td&gt;SparkToro and Gumshoe, 27 January 2026&lt;/td&gt;
&lt;td&gt;Crowd experiment, not peer reviewed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sections below explain what each of these does and does not show.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval comes before everything
&lt;/h2&gt;

&lt;p&gt;Google describes its AI features as retrieval-augmented generation: its systems retrieve pages from the Search index using the core ranking systems, then generate a response from them, with links. It also describes query fan-out, where the model issues a set of related queries to gather more results. OpenAI, Anthropic and Perplexity describe separate crawlers for search and for user-requested fetches.&lt;/p&gt;

&lt;p&gt;Two practical consequences follow, and both are well supported.&lt;/p&gt;

&lt;p&gt;First, a page that a search crawler cannot fetch cannot be cited from search. Check robots.txt, any firewall or bot-protection rule, and whether the content is present in the HTML the server returns. Microsoft's guidance makes a related point: content hidden in tabs or expandable menus may not be rendered and can be skipped.&lt;/p&gt;

&lt;p&gt;Second, the page competes on several queries, not one. Fan-out means the engine may retrieve your page for a sub-question the user never typed. Google's guide warns against reacting to this by publishing a page for every variation: doing so mainly to influence rankings or AI responses falls under its scaled content abuse policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much does ranking in search matter?
&lt;/h2&gt;

&lt;p&gt;Less than it did, and it depends on the engine.&lt;/p&gt;

&lt;p&gt;For Google AI Overviews, Ahrefs looked at 1.9 million citations from 1 million AI Overviews in July 2025 and found 76.1% of cited pages ranking in the top 10 for the same query. It repeated the exercise on 863,000 keyword results pages and 4 million cited URLs, published on 2 March 2026. This time 37.9% of cited URLs were in the first ten results, 31.2% were in positions 11 to 100, and 31.0% were beyond 100. Ahrefs attributes the drop to greater reliance on fan-out queries. The two studies did not use identical methods (the first looked only at the three most visible citations), so the size of the fall should be read with care. The direction is consistent with Google's own description of fan-out.&lt;/p&gt;

&lt;p&gt;For the assistants, the August 2025 Ahrefs study of 15,000 prompts found that on average 12% of links cited by ChatGPT, Gemini and Copilot appeared in Google's top 10 for the same prompt, and about 80% did not rank in Google for that prompt at all. Perplexity overlapped most, at 28.6%.&lt;/p&gt;

&lt;p&gt;What this supports: ranking well in Google is still associated with being cited in AI Overviews, and it is a weak predictor for the assistants. What it does not support: the idea that search ranking no longer matters. The assistants still retrieve from search indexes, but for queries they write themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Brand mentions: the strongest correlation, and only a correlation
&lt;/h2&gt;

&lt;p&gt;Ahrefs' May 2025 study of 75,000 brands used Spearman correlation to compare several factors with how often a brand was mentioned in AI Overviews. Branded web mentions came first at 0.664, then branded anchor text (0.527) and branded search volume (0.392). Domain Rating was 0.326, the number of backlinks 0.218, and the number of pages on the site 0.17. The study also reports that 26% of brands had no AI Overview mentions at all.&lt;/p&gt;

&lt;p&gt;The authors state the limit themselves: correlation is not causation. Well-known brands are mentioned more everywhere, including in AI answers. The sample was also restricted to domains with a Domain Rating above 40, so it describes established brands, not new ones.&lt;/p&gt;

&lt;p&gt;Google's guide addresses the obvious shortcut. Seeking inauthentic mentions across the web "isn't as helpful as it might seem", because the AI features depend on the same ranking and spam systems as the rest of Search. Google's spam policies, updated on 28 August 2026, now name "attempting to manipulate generative AI responses" as an example of spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Statistics, quotations and cited sources: a controlled result with limits
&lt;/h2&gt;

&lt;p&gt;The one controlled experiment in this field is the paper that coined the term generative engine optimisation, published at KDD 2024. The researchers built a benchmark of 10,000 queries and tested nine ways of rewriting a source page. Their engine retrieved the top five Google results for each query and generated an answer with GPT-3.5.&lt;/p&gt;

&lt;p&gt;Three methods stood out. Adding citations from credible sources, adding quotations and adding statistics each improved a page's share of the answer by 30 to 40% on a position-adjusted word count measure, and by 15 to 30% on a subjective impression measure. Keyword stuffing did not help. In a smaller check on Perplexity, adding quotations improved the first measure by 22% and keyword stuffing performed 10% worse than the baseline.&lt;/p&gt;

&lt;p&gt;One table in the paper deserves more attention than it gets. The gains went mostly to lower-ranked sources. Adding citations raised visibility by 115.1% for the source ranked fifth and lowered it by 30.3% for the source ranked first.&lt;/p&gt;

&lt;p&gt;The limits are significant. The pages tested had already been retrieved, so the paper says nothing about getting retrieved in the first place. The main engine was built by the researchers on a model that is now several generations old. The measure is share of words in an answer, not clicks or enquiries. The authors themselves note that methods may need to change as engines evolve. It is fair to say that sourced, specific content was favoured in a controlled setting. It is not fair to quote "40% more visibility" as a result anyone should expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  List articles and the recommendation question
&lt;/h2&gt;

&lt;p&gt;For prompts that ask which product, tool or agency to use, Ahrefs analysed ChatGPT's responses to 750 prompts and categorised 26,283 source URLs. "Best X" list articles were 43.8% of all page types cited. Of 1,100 such lists with a clear date, 79.1% had been updated in 2025, and 35% of the cited lists sat on low-authority domains, many of which the author describes as questionable.&lt;/p&gt;

&lt;p&gt;This is descriptive. It shows what ChatGPT drew on in late 2025 for one kind of prompt. The author does not claim that publishing a list causes recommendations, and notes that many recommendations come from multi-step conversations the study did not cover. A business reading this should also weigh Google's spam policy before publishing a list that ranks itself first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each engine cites differently, and the mix moves
&lt;/h2&gt;

&lt;p&gt;Profound analysed 680 million citations between August 2024 and June 2025. Wikipedia was ChatGPT's most cited source at 7.8% of its citations. Reddit led on Perplexity at 6.6% and on Google AI Overviews at 2.2%.&lt;/p&gt;

&lt;p&gt;Semrush tracked more than 230,000 prompts weekly from 14 July to 12 October 2025 across ChatGPT search, AI Mode and Perplexity. ChatGPT cited Reddit in close to 60% of responses in early August and around 10% by mid-September. Wikipedia fell from roughly 55% to under 20% over the same period, while both stayed steady on the other two engines.&lt;/p&gt;

&lt;p&gt;The lesson is about stability, not about Reddit. A tactic built on one engine's current habits can stop working in a month, for reasons no outsider can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Widely repeated claims the evidence does not support
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Add an llms.txt file and AI tools will cite you."&lt;/strong&gt; Google's guide says Search ignores the file and that it will "neither harm nor help". Ahrefs' log study of 137,210 domains found that 97% of published files were not requested once in May 2026, and that search-retrieval bots made up 1.1% of the requests that did occur. Details are in what llms.txt does and does not do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Schema markup makes AI cite your page."&lt;/strong&gt; Ahrefs found that cited pages are far more likely to carry schema, then tested whether adding it changes anything. Across 1,885 pages and 4,000 matched controls, citations moved by +2.4% on AI Mode and +2.2% on ChatGPT, both indistinguishable from zero, and by -4.6% on AI Overviews. The study covered pages that were already heavily cited, so it does not rule out an effect on pages that are not yet seen at all. Google says structured data is not required for its AI features. Microsoft says schema helps its systems understand content. Keep schema for rich results. Do not buy it as an AI citation tactic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Content must be split into small chunks for AI."&lt;/strong&gt; Google lists this as something site owners can ignore. Microsoft recommends clear headings, lists and tables. These are compatible: organise pages for readers. No independent test shows that chunking beyond that changes citations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"More pages means more AI visibility."&lt;/strong&gt; Page count had the weakest correlation of any factor in the Ahrefs brand study (0.17), and Google's guide says a high quantity of pages does not make a site more relevant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"You must rank first on Google to be cited."&lt;/strong&gt; The overlap figures above contradict this for every engine measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"We can track your position in ChatGPT."&lt;/strong&gt; In the SparkToro and Gumshoe experiment, 600 volunteers ran 12 prompts 2,961 times across ChatGPT, Claude and Google's AI features. The chance of getting the same list of brands twice in 100 runs was under 1 in 100. The same data showed something more useful: how often a brand appears across many runs is fairly stable. One agency appeared in 85 of 95 responses to a single prompt. Appearance rate over repeated runs is a defensible measure. A position from a single run is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This method gets you cited."&lt;/strong&gt; No vendor offers that, and no study shows it. Google states that meeting every requirement does not mean a page will be crawled, indexed or served.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a small business can reasonably do
&lt;/h2&gt;

&lt;p&gt;In order of how well each step is supported:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm that the search crawlers can reach your pages and that the content is in the server's HTML. This is the only step every vendor documents. An &lt;a href="https://www.wavxsolutions.in/services/geo/ai-visibility-audit" rel="noopener noreferrer"&gt;AI visibility audit&lt;/a&gt; starts here.&lt;/li&gt;
&lt;li&gt;Check that the site is included in Google's Search generative AI control and is indexed with snippets allowed.&lt;/li&gt;
&lt;li&gt;Publish pages that state specific, dated, sourced facts a model can lift. The controlled evidence, with its limits, points this way, and so does Google's advice on non-commodity content. This is the work described under &lt;a href="https://www.wavxsolutions.in/services/geo/answer-engine-optimization" rel="noopener noreferrer"&gt;answer engine optimisation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Earn real mentions: directory listings, reviews, partner pages, press. It is the strongest correlate and the slowest to build.&lt;/li&gt;
&lt;li&gt;Measure with the tools the engines provide and with repeated prompt runs, not single screenshots. See how to measure AI referral traffic in GA4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the immediate problem is that an assistant does not name your business, the checks are in why ChatGPT does not mention your business. WavX's wider offer is described under &lt;a href="https://www.wavxsolutions.in/services/geo" rel="noopener noreferrer"&gt;generative engine optimisation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is not worth paying for
&lt;/h2&gt;

&lt;p&gt;If your site has little search visibility and few mentions elsewhere, the evidence says the gap is in ordinary SEO and reputation, not in an AI-specific tactic. Google's guide says as much: optimising for its AI features is still SEO. A business that gets most of its work through referrals or marketplaces may see no return from any of this. And a tracking subscription makes little sense before the basic crawler and indexing checks have been done, which cost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this review
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It covers published sources read on 2 October 2026. Engines change faster than studies are published. Two of the findings above reversed or shifted within a year.&lt;/li&gt;
&lt;li&gt;Most independent data comes from companies with products to sell in this area.&lt;/li&gt;
&lt;li&gt;None of the studies was designed around Indian queries or Indian businesses.&lt;/li&gt;
&lt;li&gt;Several findings concern Google AI Overviews only and should not be generalised to ChatGPT or Perplexity.&lt;/li&gt;
&lt;li&gt;No study measures enquiries or revenue. They measure citations and mentions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/search/docs/appearance/ai-features" rel="noopener noreferrer"&gt;Google Search Central: AI features and your website&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/search/docs/fundamentals/ai-optimization-guide" rel="noopener noreferrer"&gt;Google Search Central: Optimizing your website for generative AI features on Google Search&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/search/docs/essentials/spam-policies" rel="noopener noreferrer"&gt;Google Search Central: Spam policies for Google web search&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://support.google.com/webmasters/answer/16908024" rel="noopener noreferrer"&gt;Google Search Console Help: Search generative AI control&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.openai.com/api/docs/bots" rel="noopener noreferrer"&gt;OpenAI: Overview of OpenAI crawlers&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Anthropic: Does Anthropic crawl data from the web, and how can site owners block the crawler?&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.perplexity.ai/docs/resources/perplexity-crawlers" rel="noopener noreferrer"&gt;Perplexity: Perplexity crawlers&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://about.ads.microsoft.com/en/blog/post/october-2025/optimizing-your-content-for-inclusion-in-ai-search-answers" rel="noopener noreferrer"&gt;Microsoft Advertising: Optimizing your content for inclusion in AI search answers (8 October 2025)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ahrefs.com/blog/ai-overview-brand-correlation/" rel="noopener noreferrer"&gt;Ahrefs: An analysis of AI Overview brand visibility factors, 75K brands (26 May 2025)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ahrefs.com/blog/search-rankings-ai-citations/" rel="noopener noreferrer"&gt;Ahrefs: 76% of AI Overview citations pull from the top 10 (21 July 2025)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ahrefs.com/blog/ai-overview-citations-top-10/" rel="noopener noreferrer"&gt;Ahrefs: Update, 38% of AI Overview citations pull from the top 10 (2 March 2026)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ahrefs.com/blog/ai-search-overlap/" rel="noopener noreferrer"&gt;Ahrefs: Only 12% of AI cited URLs rank in Google's top 10 for the original prompt (11 August 2025)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ahrefs.com/blog/best-lists-research/" rel="noopener noreferrer"&gt;Ahrefs: Do self-promotional "best" lists boost ChatGPT visibility? (4 December 2025)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ahrefs.com/blog/schema-ai-citations/" rel="noopener noreferrer"&gt;Ahrefs: We tracked 1,885 pages adding schema. AI citations barely moved (11 May 2026)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ahrefs.com/blog/llmstxt-study/" rel="noopener noreferrer"&gt;Ahrefs: We analyzed 137K sites: 97% of llms.txt files never get read (15 June 2026)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2311.09735" rel="noopener noreferrer"&gt;Aggarwal and others: GEO: Generative Engine Optimization, KDD 2024 (arXiv 2311.09735)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.semrush.com/blog/most-cited-domains-ai/" rel="noopener noreferrer"&gt;Semrush: The most-cited domains in AI, a 3-month study (10 November 2025)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.tryprofound.com/blog/ai-platform-citation-patterns" rel="noopener noreferrer"&gt;Profound: AI platform citation patterns (5 June 2025, updated August 2025)&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sparktoro.com/blog/new-research-ais-are-highly-inconsistent-when-recommending-brands-or-products-marketers-should-take-care-when-tracking-ai-visibility/" rel="noopener noreferrer"&gt;SparkToro: AIs are highly inconsistent when recommending brands or products (27 January 2026)&lt;/a&gt;, read 2 October 2026 from the Internet Archive copy dated 1 October 2026&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>geo</category>
      <category>seostrategy</category>
      <category>digitalmarketing</category>
    </item>
    <item>
      <title>Custom Shopify Theme Development Cost in India (2026)</title>
      <dc:creator>WAVX solutions</dc:creator>
      <pubDate>Sat, 03 Oct 2026 05:27:34 +0000</pubDate>
      <link>https://dev.to/wavx_solutions_1c671328ad/custom-shopify-theme-development-cost-in-india-2026-3b38</link>
      <guid>https://dev.to/wavx_solutions_1c671328ad/custom-shopify-theme-development-cost-in-india-2026-3b38</guid>
      <description>&lt;p&gt;A custom Shopify theme, designed for one brand and coded in Liquid, falls inside the ₹1,50,000 to ₹4,50,000 planning range WavX already publishes for a custom-designed Shopify store. Buying a ready theme is far cheaper: on 2 October 2026 the Shopify Theme Store listed paid themes from US$100 to US$500 as a one-time purchase, and 24 themes free. For most new stores the bought theme is the right first choice. A custom theme earns its cost when the brand, the product page or the buying flow needs something no listed theme does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three routes and what each costs
&lt;/h2&gt;

&lt;p&gt;There are three ways to get a storefront on Shopify, and they differ by an order of magnitude.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;What you pay for the theme&lt;/th&gt;
&lt;th&gt;What you pay a developer&lt;/th&gt;
&lt;th&gt;Source of the figure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free theme, configured&lt;/td&gt;
&lt;td&gt;Nothing. 24 free themes listed&lt;/td&gt;
&lt;td&gt;₹25,000 to ₹75,000 for a basic template setup of the whole store&lt;/td&gt;
&lt;td&gt;Shopify Theme Store, 2 October 2026; WavX store setup guide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid theme, adjusted&lt;/td&gt;
&lt;td&gt;US$100 to US$500, once, for one store&lt;/td&gt;
&lt;td&gt;₹15,000 to ₹40,000 to adjust an off-the-shelf theme&lt;/td&gt;
&lt;td&gt;Shopify Theme Store, 2 October 2026; WavX store setup guide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom theme&lt;/td&gt;
&lt;td&gt;Nothing to Shopify&lt;/td&gt;
&lt;td&gt;₹1,50,000 to ₹4,50,000 for a custom-designed store, of which design and theme development are the largest part&lt;/td&gt;
&lt;td&gt;WavX store setup guide&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rupee figures are planning ranges that WavX has already published in its &lt;a href="https://www.wavxsolutions.in/blog/shopify-store-setup-cost-in-india" rel="noopener noreferrer"&gt;Shopify store setup cost guide&lt;/a&gt;. They are quoted from that page so the two agree. WavX's cost model has no separate theme line items, so no new rate is introduced here, and a firm figure is quoted after scoping.&lt;/p&gt;

&lt;p&gt;Theme Store prices are in US dollars. At a round ₹100 to the dollar, which is a convenience for arithmetic and not a quoted exchange rate, US$100 to US$500 is about ₹10,000 to ₹50,000. Your card's rate on the day applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Shopify Theme Store charges and what you get
&lt;/h2&gt;

&lt;p&gt;These are the facts as read on Shopify's own pages on 2 October 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paid themes.&lt;/strong&gt; 1,275 paid themes were listed. Sorted by price, the cheapest were US$100 and the most expensive US$500.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free themes.&lt;/strong&gt; 24 free themes were listed, including Dawn, Horizon, Refresh, Craft and Sense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who makes them.&lt;/strong&gt; Shopify's help centre says free themes are developed by Shopify, and paid themes come from third-party developers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who supports them.&lt;/strong&gt; Help with customising a free theme is supported by Shopify. For a paid theme, general support and updates come from the theme's developer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trying before buying.&lt;/strong&gt; You can preview up to 19 paid themes at a time, customise a previewed theme in the theme editor, and keep those customisations if you buy it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licence.&lt;/strong&gt; A theme bought from the Theme Store is licensed exclusively to the store where the purchase was made.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates.&lt;/strong&gt; Shopify applies automated updates to Theme Store themes for bugs and security issues. A new theme version can be added as a draft and tested before it is published. If the theme's code has been edited, the update either includes the edits or reports that they could not be included, and Shopify advises saving a copy of customised code before updating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point is the hidden cost of a heavily edited paid theme. Every code edit is something that may have to be reapplied by hand when the theme's developer ships a new version. A theme changed only through its settings has no code edits to carry over. A theme with a rewritten product page does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a custom theme build includes
&lt;/h2&gt;

&lt;p&gt;A Shopify theme is a folder of files with a fixed structure. Shopify's theme architecture documentation lists the directories: &lt;code&gt;layout&lt;/code&gt;, &lt;code&gt;templates&lt;/code&gt;, &lt;code&gt;sections&lt;/code&gt;, &lt;code&gt;blocks&lt;/code&gt;, &lt;code&gt;snippets&lt;/code&gt;, &lt;code&gt;assets&lt;/code&gt;, &lt;code&gt;config&lt;/code&gt; and &lt;code&gt;locales&lt;/code&gt;. Templates decide what each type of page shows. Sections and blocks are the pieces a merchant can add, remove and reorder in the theme editor. The code is Liquid, JSON, CSS and JavaScript.&lt;/p&gt;

&lt;p&gt;A custom theme means writing those files for one store instead of buying them. In practice the work is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wireframes&lt;/strong&gt; for each unique page: home, collection, product, cart, and any landing or content pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual design&lt;/strong&gt; in Figma, for mobile first and then desktop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme development&lt;/strong&gt;: sections and blocks for every design element, with settings so the team can edit content without a developer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product page logic&lt;/strong&gt;: variants, size guides, bundles, delivery estimates, whatever the catalogue needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App blocks&lt;/strong&gt;: space in the templates for the apps the store uses, such as reviews. Shopify's theme app extensions work with Online Store 2.0 themes, so a custom theme has to support them properly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt; on real phones and browsers, then launch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the work described on WavX's &lt;a href="https://www.wavxsolutions.in/services/shopify/theme-development" rel="noopener noreferrer"&gt;Shopify theme development&lt;/a&gt; page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the money goes
&lt;/h2&gt;

&lt;p&gt;The store setup guide breaks a ₹3,50,000 mid-tier Shopify build into eight steps. Four of them are the theme.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step in the published example&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Part of the theme?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Discovery and wireframing&lt;/td&gt;
&lt;td&gt;₹25,000&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI/UX design&lt;/td&gt;
&lt;td&gt;₹60,000&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme development and Liquid customisation&lt;/td&gt;
&lt;td&gt;₹1,20,000&lt;/td&gt;
&lt;td&gt;3 weeks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App integration and configuration&lt;/td&gt;
&lt;td&gt;₹40,000&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment and logistics integration&lt;/td&gt;
&lt;td&gt;₹20,000&lt;/td&gt;
&lt;td&gt;0.5 week&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data migration and catalogue setup&lt;/td&gt;
&lt;td&gt;₹35,000&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UAT and bug fixing&lt;/td&gt;
&lt;td&gt;₹35,000&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;Yes, mostly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment and go-live&lt;/td&gt;
&lt;td&gt;₹15,000&lt;/td&gt;
&lt;td&gt;0.5 week&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;₹3,50,000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 weeks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The theme-related lines come to ₹2,40,000 over about 7 weeks. That is one worked example for a mid-sized store, and it should be read as a guide to proportions: in this example design, theme code and testing are about two-thirds of the build, and the rest is setup that a bought theme needs too.&lt;/p&gt;

&lt;p&gt;The same guide gives ₹50,000 to ₹2,00,000 for custom design on its own. The &lt;a href="https://www.wavxsolutions.in/blog/ecommerce-website-cost-in-india" rel="noopener noreferrer"&gt;ecommerce website cost guide&lt;/a&gt; lists a Shopify custom theme or Plus build at ₹1,50,000 to ₹5,00,000 and above. That row also covers Plus builds, which is why its upper end is higher.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes a theme build cost more
&lt;/h2&gt;

&lt;p&gt;The planning range is wide because themes differ in size. These decide where in the range a project lands.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Number of unique templates.&lt;/strong&gt; A store with one product template and one collection template is small. A store where gift sets, made-to-order items and subscriptions each need their own product page has three product pages to design, build and test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sections the team can edit.&lt;/strong&gt; Every section with settings takes longer to build than a fixed block of HTML, and saves a developer call each time the content changes. More editable sections means more build cost and lower running cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product page logic.&lt;/strong&gt; Variant pickers with swatches, size charts per category, pin-code delivery estimates, bundles and personalisation fields are each a piece of work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motion and media.&lt;/strong&gt; Video backgrounds, scroll effects and 3D viewers cost time to build and time to keep fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages and markets.&lt;/strong&gt; Each language needs translated strings in the theme's locale files and a design that survives longer text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content readiness.&lt;/strong&gt; A design built around real photography and real copy is finished once. A design built around placeholders is usually adjusted again when the real content arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moving from another platform.&lt;/strong&gt; URL redirects, imported content and old page designs to match all add work that has nothing to do with the theme's code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paid theme or custom theme
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;A paid theme is enough when&lt;/th&gt;
&lt;th&gt;A custom theme is worth pricing when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brand&lt;/td&gt;
&lt;td&gt;The brand can live inside a good theme's layout with its own colours, type and photography&lt;/td&gt;
&lt;td&gt;The layout itself is part of the brand, and no listed theme comes close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product page&lt;/td&gt;
&lt;td&gt;Products are simple: a few variants, standard images, a description&lt;/td&gt;
&lt;td&gt;The product needs configuration, bundles, fitting guidance or content that standard templates cannot hold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team&lt;/td&gt;
&lt;td&gt;Nobody on the team will edit code&lt;/td&gt;
&lt;td&gt;You have, or will pay for, someone to maintain the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stage&lt;/td&gt;
&lt;td&gt;The store is new and the offer is still being tested&lt;/td&gt;
&lt;td&gt;The store has steady sales and you know from data which pages lose buyers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget&lt;/td&gt;
&lt;td&gt;The build budget is under about ₹1,50,000&lt;/td&gt;
&lt;td&gt;The budget covers the build and yearly maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apps&lt;/td&gt;
&lt;td&gt;The apps you need provide blocks that fit the theme&lt;/td&gt;
&lt;td&gt;Apps need to sit inside a custom layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;The theme you chose performs well in Shopify's data&lt;/td&gt;
&lt;td&gt;A measured problem remains after apps and media have been cleaned up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A middle route exists and is often the best value: buy a paid theme whose structure is close, and pay for a small number of custom sections built as additions and kept separate from the theme's own files. Additions kept in their own section files are much easier to carry through a theme update than edits to the theme's own files, and the bill is usually closer to the ₹15,000 to ₹40,000 adjustment band than to a custom build. This works when the changes are additions. It stops working when the request is to rebuild the theme's product page or cart.&lt;/p&gt;

&lt;h2&gt;
  
  
  A custom theme is not automatically faster
&lt;/h2&gt;

&lt;p&gt;Speed is the most common reason given for going custom, so it is worth checking against public data.&lt;/p&gt;

&lt;p&gt;Shopify publishes aggregated Core Web Vitals results by theme. The table, last updated on 29 September 2026, covers 127 themes and reports the share of stores on each theme where at least 75% of visitors had a good experience on all three measures. It counts active stores on a custom domain with at least 100 daily events, on one of the theme's three latest versions, over the previous seven days.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Stores passing all three Core Web Vitals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dawn (free)&lt;/td&gt;
&lt;td&gt;89.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizon (free)&lt;/td&gt;
&lt;td&gt;87.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh (free)&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Craft (free)&lt;/td&gt;
&lt;td&gt;93.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impulse (paid, US$500)&lt;/td&gt;
&lt;td&gt;92.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Highest theme in the table&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lowest theme in the table&lt;/td&gt;
&lt;td&gt;37.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two conclusions follow. First, a well-chosen bought theme already performs well on most stores that use it, including the free ones. Second, the spread between themes is large, so checking this table before buying a theme is worth ten minutes.&lt;/p&gt;

&lt;p&gt;What usually slows a Shopify store is what gets added afterwards. Shopify's help centre lists theme code, apps, images, videos, carousels, social media feeds and analytics as things that affect performance. A custom theme can help because it carries only the features the store uses. It cannot fix twelve app scripts and uncompressed images. If the store is slow today, read &lt;a href="https://www.wavxsolutions.in/problems/shopify-store-slow" rel="noopener noreferrer"&gt;why a Shopify store is slow&lt;/a&gt; and look at &lt;a href="https://www.wavxsolutions.in/services/shopify/speed-optimization" rel="noopener noreferrer"&gt;speed optimisation&lt;/a&gt; before commissioning a new theme. No developer can promise a score before the store is built and carrying real content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs after the build
&lt;/h2&gt;

&lt;p&gt;A bought theme is maintained by its developer. A custom theme is maintained by you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform changes.&lt;/strong&gt; Shopify keeps adding to the theme platform. A custom theme only gains new platform features when someone builds them in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New pages and campaigns.&lt;/strong&gt; If the theme was built with editable sections, most campaign pages need no developer. If not, each one is a small project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App changes.&lt;/strong&gt; Adding or removing an app can need template changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixes.&lt;/strong&gt; Browsers and devices change, and a theme needs occasional repair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WavX publishes 15% to 25% of build cost per year as its maintenance band for software it builds, explained in the &lt;a href="https://www.wavxsolutions.in/blog/app-maintenance-cost-per-year-india" rel="noopener noreferrer"&gt;app maintenance cost guide&lt;/a&gt;. Applied to the ₹1,50,000 to ₹4,50,000 range, that is ₹22,500 to ₹1,12,500 a year. A theme with little custom logic sits at the low end.&lt;/p&gt;

&lt;p&gt;Over three years, with maintenance in years two and three, the model gives ₹1,95,000 to ₹6,75,000 for a custom-designed store. The paid-theme route over the same period is the theme price once, the adjustment fee once, and whatever further changes you ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should not commission a custom theme
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The store has not launched.&lt;/strong&gt; You do not yet know which pages matter. Launch on a good theme, learn from real orders, and spend on design once the data says where.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The problem is traffic or product, not design.&lt;/strong&gt; A new theme does not create demand. If few people reach the product page, the theme is not the constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The request is "make it look premium".&lt;/strong&gt; That is usually photography, typography and copy, all of which work inside a bought theme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody will maintain it.&lt;/strong&gt; A custom theme with no owner ages badly. A paid theme keeps receiving its developer's updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless is being considered for speed.&lt;/strong&gt; That is a bigger decision with a higher bill, covered in when headless Shopify is worth it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to ask before you pay for a theme build
&lt;/h2&gt;

&lt;p&gt;These questions separate a real custom theme from a bought theme with a new colour scheme, and they apply to any developer, WavX included.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is this built from a starter theme or from a purchased theme? Either is acceptable, as long as you are told which.&lt;/li&gt;
&lt;li&gt;Which sections will the team be able to edit in the theme editor, and which are fixed?&lt;/li&gt;
&lt;li&gt;How will apps be placed: through app blocks, or by pasting code into templates?&lt;/li&gt;
&lt;li&gt;Where is the code kept, and will it be handed over in a repository you own?&lt;/li&gt;
&lt;li&gt;What was tested, on which phones, and what were the Core Web Vitals results on the finished store with real content?&lt;/li&gt;
&lt;li&gt;What is included after launch, for how long, and what does a change cost after that?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A developer who answers these plainly is giving you a custom theme you can live with. One who cannot is selling a theme you will be afraid to touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://themes.shopify.com/themes?price%5B%5D=paid&amp;amp;sort_by=price_low_to_high" rel="noopener noreferrer"&gt;Shopify Theme Store: paid themes sorted by price, low to high&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://themes.shopify.com/themes?price%5B%5D=paid&amp;amp;sort_by=price_high_to_low" rel="noopener noreferrer"&gt;Shopify Theme Store: paid themes sorted by price, high to low&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://themes.shopify.com/themes?price%5B%5D=free" rel="noopener noreferrer"&gt;Shopify Theme Store: free themes&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.shopify.com/en/manual/online-store/themes/adding-themes" rel="noopener noreferrer"&gt;Shopify Help Center: adding themes, licensing, previews and support&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.shopify.com/en/manual/online-store/themes/managing-themes/updating-themes" rel="noopener noreferrer"&gt;Shopify Help Center: updating themes&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/storefronts/themes/architecture" rel="noopener noreferrer"&gt;Shopify developer documentation: theme architecture&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.dev/docs/apps/build/online-store/theme-app-extensions" rel="noopener noreferrer"&gt;Shopify developer documentation: theme app extensions&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://performance.shopify.com/pages/theme-performance-data-table" rel="noopener noreferrer"&gt;Shopify: aggregated Core Web Vitals performance by theme, updated 29 September 2026&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.shopify.com/en/manual/online-store/web-performance" rel="noopener noreferrer"&gt;Shopify Help Center: online store web performance&lt;/a&gt;, read 2 October 2026&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>costguide</category>
      <category>india</category>
    </item>
  </channel>
</rss>
