A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Google Analytics: A luxury your users are paying for (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f0f0ea">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f0f0ea">
  26. <!-- Documented, feel free to shoot an email. -->
  27. <link rel="stylesheet" href="/static/david/css/style_2020-06-19.css">
  28. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  29. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  30. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  31. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  32. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <script type="text/javascript">
  36. function toggleTheme(themeName) {
  37. document.documentElement.classList.toggle(
  38. 'forced-dark',
  39. themeName === 'dark'
  40. )
  41. document.documentElement.classList.toggle(
  42. 'forced-light',
  43. themeName === 'light'
  44. )
  45. }
  46. const selectedTheme = localStorage.getItem('theme')
  47. if (selectedTheme !== 'undefined') {
  48. toggleTheme(selectedTheme)
  49. }
  50. </script>
  51. <meta name="robots" content="noindex, nofollow">
  52. <meta content="origin-when-cross-origin" name="referrer">
  53. <!-- Canonical URL for SEO purposes -->
  54. <link rel="canonical" href="https://iainbean.com/posts/2020/google-analytics-a-luxury-your-users-are-paying-for/">
  55. <body class="remarkdown h1-underline h2-underline h3-underline hr-center ul-star pre-tick">
  56. <article>
  57. <header>
  58. <h1>Google Analytics: A luxury your users are paying for</h1>
  59. </header>
  60. <nav>
  61. <p class="center">
  62. <a href="/david/" title="Aller à l’accueil">🏠</a> •
  63. <a href="https://iainbean.com/posts/2020/google-analytics-a-luxury-your-users-are-paying-for/" title="Lien vers le contenu original">Source originale</a>
  64. </p>
  65. </nav>
  66. <hr>
  67. <main>
  68. <p>Since its launch in 2005, Google Analytics (GA) has become so widely used that it now appears on 86% of the top 100,000 websites in the United States. How did it become so popular? For two main reasons: its convenience and its cost. Once you’ve signed up (for free), you can install it by pasting a script tag and four lines of JavaScript into your site’s <span class="small-caps lowercase">HTML</span>. You now have the power to track whenever anyone visits your website, where they came from (both virtually and geographically), what pages they looked at, and for how long they stuck around. But this power doesn’t come for free: you may not be spending any money, but your users are paying in other ways.</p>
  69. <p><picture>
  70. <source srcset="https://iainbean.com/they-know-what-you-clicked-last-summer_200.webp 200w,https://iainbean.com/they-know-what-you-clicked-last-summer_400.webp 400w,https://iainbean.com/they-know-what-you-clicked-last-summer_800.webp 800w," type="image/webp" sizes="calc(100vw - 2rem)">
  71. <img src="https://iainbean.com/images/posts/they-know-what-you-clicked-last-summer.jpg" srcset="https://iainbean.com/they-know-what-you-clicked-last-summer_200.jpg 200w,https://iainbean.com/they-know-what-you-clicked-last-summer_400.jpg 400w,https://iainbean.com/they-know-what-you-clicked-last-summer_800.jpg 800w," alt="Graph showing percentage of global page loads tracked by certain companies: Google 64.4%, Facebook 28.8%, Comscore 12.2%, Twitter 11%, Amazon 10.5%, Yandex 8%, Criteo 6.5%, New Relic 5.9%" falsefalsefalsefalse=""/>
  72. </source></picture></p>
  73. <p>Of course, Google isn’t the only company involved in the analytics business, but they collect more data from more users than anyone else. According to this graphic from 2017, Google were tracking 64.4% of all page loads across the whole web; three years later, that figure is now probably even higher.</p>
  74. <p>After recently deciding to remove all the client-side JavaScript from my own website, I had a decision to make: either stop tracking visitor numbers entirely, or look for an alternative to GA. Fortunately, there are a number of other ways to measure the popularity of my site that have far less (or even zero) impact on end users. In this post, I’ll cover the main problems with GA and explore some alternatives.</p>
  75. <h2>The downsides</h2>
  76. <p>Let’s look at some of the frequently mentioned downsides of using GA and determine their impact:</p>
  77. <h3>It’s not accurate</h3>
  78. <p>A quick search gives me a figure from 2018 of 27% of web users using an ad blocker. A more recent study suggests that this figure is 40% on desktop and 22% on mobile. Some ad blockers such as uBlock origin block all tracking JS by default (including GA) and even those that don’t block GA by default can often be configured to do so. There are now also privacy controls built into browsers like Safari and Firefox which break certain tracking features by disabling <a href="https://blog.mozilla.org/firefox/cross-site-tracking-lets-unpack-that/">cross-site-tracking cookies</a> (these are what allow ads to follow you around the web after you’ve looked at a product). What does this mean for your analytics numbers? It means you’re going to miss a significant amount of visitors and if your site is one with a tech-savvy audience (e.g. a blog about web development), that number is probably even higher.</p>
  79. <p>This one is a definite <strong>yes</strong>: the numbers you get from GA are wrong and it’s difficult to say exactly how wrong they are.</p>
  80. <h3>It’s bad for performance</h3>
  81. <p>If you’re using the method currently recommended by Google to install GA on your site, each of your users will be downloading the gtag.js library which is 34.72kB of (compressed) JavaScript <em>and</em> analytics.js which is 18.4kB. For comparison, that’s larger than many popular JavaScript libraries including Vue (22.8kB) or jQuery (30.4kB), and over twice as large as the previously recommended method of loading analytics.js on its own.</p>
  82. <p>I’ve seen it argued that this isn’t a big deal because of the way browsers cache files between requests. To confirm whether this assumption is correct, I opened a new incognito window in Chrome, opened the network tab and then navigated to five different websites which I knew used GA: while gtag.js appeared as a 34.72kB download on the first site, and analytics.js showed up as being 18.4kB, for the next four sites both files showed up as '(disk cache)', meaning the locally downloaded version was being used instead. However, Google sets a <code>Cache-Control max-age</code> header of only two hours for analytics.js and only fifteen minutes for gtag.js, meaning that after that time the version in the cache is invalid and the file must be downloaded again.</p>
  83. <p>There was a time when websites using Google Analytics were unable to score 100/100 on Google’s own PageSpeed benchmark (the predecessor to Lighthouse) because of this short cache period, leading certain performance-minded developers to host the analytics.js library on their own server with a longer cache period. However, this negates the benefit mentioned above, where the file can be cached between different sites.</p>
  84. <p>I’ll mark this one as a <strong>yes, but with the caveat that for some visitors it won’t be an issue</strong>: you’re still forcing those visitors without a cached version to download multiple libraries which provide them with zero perceptible benefits. It’s also worth mentioning that GA often isn’t the only third-party tracking JavaScript installed on a site. A <a href="https://www.pingdom.com/blog/trackers-impact-performance/">2018 study by Pingdom</a>, showed that the average news site loads over <strong>40 trackers</strong> and the impact of all those trackers is an increase in the average page load time of <strong>6.77 seconds</strong>.</p>
  85. <h3>It’s bad for privacy</h3>
  86. <p>By default, GA harvests a wide variety of data from users, including IP addresses, regardless of whether you have your users’ permission to do so. Efforts by governments, like the EU’s General Data Protection Regulation (<span class="small-caps lowercase">GDPR</span>), have attempted to reduce this kind of harvesting of personal information without good reason; but big tech companies are always looking for loopholes which allow them to carry on these practices. If you’ve ever been overwhelmed by the options on a cookie consent dialog, that’s intentional — these are usually designed to either trick you or bore you into sharing more data than you’re comfortable doing, with as many third parties as possible.</p>
  87. <p>Here’s an excerpt from ‘How Google uses information from sites or apps that use our services’:</p>
  88. <blockquote>
  89. <p>Google uses the information shared by sites and apps to deliver our services, maintain and improve them, develop new services, measure the effectiveness of advertising, protect against fraud and abuse, and personalize content and ads you see on Google and on our partners’ sites and apps.</p>
  90. </blockquote>
  91. <p>When you install GA on your site, you’re basically giving Google free rein to use any data they collect for whatever purpose they choose. One of these purposes Google is somewhat open about is 'Ad personalization' — this is where data collected from multiple sources is combined to build an ‘ad profile’ containing your demographic data and interests. This is then used ‘to make your ads more useful for you’. These personalised, ‘more useful’ ads are shown with the aim of increasing conversions (i.e. clicks and sales), based on the idea you’re more likely to engage with something that’s relevant to your interests. A higher conversion rate means Google can charge higher fees to advertisers for the same ad space.</p>
  92. <p>This is another definite <strong>yes</strong>: the negative impact on privacy caused by GA is undeniable and is something you should be open with your visitors about. The Google Analytics Terms of Service include the following line: “You must post a Privacy Policy and that Privacy Policy must provide notice of Your use of cookies…” — if you’re using GA without making this clear in your privacy policy (you <em>do</em> have a privacy policy, right?), you’re not only violating GA’s terms and privacy regulations, but also your users’ trust.</p>
  93. <h2>The alternatives</h2>
  94. <h3>Minimal Analytics</h3>
  95. <p>When Google Analytics runs in the browser it sends tracking events to an <span class="small-caps lowercase">API</span>. This means that you don’t have to use Google’s JavaScript: you can instead write your own or use one of many existing GA-compatible libraries to track only the things you need. A great example of this is the <a href="https://minimalanalytics.com/">Minimal Google Analytics Snippet</a>, a 1.5kB library that is small enough to paste directly into your website’s HTML with no external dependencies. For websites without complex requirements such as AdWords, this is just as effective (if not more effective, as it should load quicker) as gtag.js or analytics.js, but at a fraction of the size.</p>
  96. <p>This mostly solves the potential performance issue, however, even though we’re not using Google’s JavaScript, we’re still sending data to their servers. This does nothing to fix the privacy issue and most ad blockers will be smart enough to intercept these requests.</p>
  97. <h3>Client-side alternatives</h3>
  98. <p>If you want the features of GA but want to make a more ethical choice, there are plenty of offerings which claim to offer better privacy than GA. I won’t cover all of the available options here, but I will recommend <a href="https://nts.strzibny.name/privacy-oriented-alternatives-to-google-analytics/">this post by Josef Strzibny</a> in which he covers both open source and hosted alternatives. These generally have much smaller file sizes and collect far less data than GA, but are still liable to be blocked by ad-blockers.</p>
  99. <p><picture>
  100. <source srcset="https://iainbean.com/plausible-analytics_200.webp 200w,https://iainbean.com/plausible-analytics_400.webp 400w,https://iainbean.com/plausible-analytics_800.webp 800w,https://iainbean.com/plausible-analytics_1000.webp 1000w,https://iainbean.com/plausible-analytics_1600.webp 1600w," type="image/webp" sizes="calc(100vw - 2rem)">
  101. <img src="https://iainbean.com/images/posts/plausible-analytics.png" srcset="https://iainbean.com/plausible-analytics_200.png 200w,https://iainbean.com/plausible-analytics_400.png 400w,https://iainbean.com/plausible-analytics_800.png 800w,https://iainbean.com/plausible-analytics_1000.png 1000w,https://iainbean.com/plausible-analytics_1600.png 1600w," alt="A graph from Plausible analytics, showing the number of visitors during each hour of a single day" falsefalsefalsefalse=""/>
  102. </source></picture></p>
  103. <p>I’ve recently replaced GA with <a href="https://plausible.io/">Plausible</a> on <a href="https://component.gallery/">the Component Gallery</a>: it’s open source, lightweight (835 bytes), it captures the bare minimum of data, and it even comes with a 30 day free trial (after that, it costs $6 a month or $4 if paying annually). I can’t say whether it’s the best privacy-focused client-side analytics service because I haven’t tried them all, but I’d tend to favour those services that have made their code open source over those which haven’t. Open source analytics programs have the benefit that you can choose to host the program yourself without paying a subscription fee. You can also inspect the source code to get a better idea of how much data they’re collecting.</p>
  104. <p>For hosted services I’d be more inclined to choose a service which charges a fee. While no company should be taken at their word without a healthy pinch of scepticism, it’s easier to understand the business model of a service which charges a subscription fee than one that gives it away for free — with a free service, you could be paying with your users’ data. That said, it’s unlikely that any service is doing anything with data on the scale of Google. If you’re strongly against paying a subscription fee for analytics, <a href="https://www.goatcounter.com">GoatCounter</a> offers a free tier (donations recommended, non-commercial use) for up to 100,000 pageviews per month.</p>
  105. <h3>Other sources of analytics data</h3>
  106. <p>Even without client-side analytics, users leave a trail when using the internet: if you make a search using Google, or click a link in a tweet, those platforms are recording that action. No single tool can give you a definitive measurement of how users arrive on your website, but there are still ways you can build up a picture from data that already exists: sites including Google search and Twitter provide a subset of the data they collect for you to use for your own purposes.</p>
  107. <p><picture>
  108. <source srcset="https://iainbean.com/google-search-console_200.webp 200w,https://iainbean.com/google-search-console_400.webp 400w,https://iainbean.com/google-search-console_800.webp 800w,https://iainbean.com/google-search-console_1000.webp 1000w,https://iainbean.com/google-search-console_1600.webp 1600w," type="image/webp" sizes="calc(100vw - 2rem)">
  109. <img src="https://iainbean.com/images/posts/google-search-console.png" srcset="https://iainbean.com/google-search-console_200.png 200w,https://iainbean.com/google-search-console_400.png 400w,https://iainbean.com/google-search-console_800.png 800w,https://iainbean.com/google-search-console_1000.png 1000w,https://iainbean.com/google-search-console_1600.png 1600w," alt="A graph from Google Search console showing the number of clicks and impressions for the last 3 months" falsefalsefalsefalse=""/>
  110. </source></picture></p>
  111. <p><strong>Google Search Console</strong> (shown above) allows you to track the terms used when visitors see your website in search results as well as the proportion of users who saw those links and went on to click them. It also lets you know which pages have been successfully crawled and which caused crawler errors. Unlike with GA, Google already has this data, so I personally don’t see this as anywhere near as harmful as actively collecting more data on Google’s behalf.</p>
  112. <p>Google Search Console isn’t a replacement for analytics, in fact Google push you to link it with your GA account so the datasets from each platform can be combined. But if you don’t want to feed more data into Google’s ad revenue generating machine, using Search Console on its own may be enough to give you a good idea of how users are arriving on your site and what they’re looking for.</p>
  113. <h3>Netlify analytics</h3>
  114. <p>If you’re hosting your website on Netlify you can enable <a href="https://www.netlify.com/products/analytics/">Netlify Analytics</a> for $9 per site, per month. That might sound steep for a service you can get elsewhere for free, but Netlify’s server-side tracking is an entirely different offering from the client-side services previously mentioned:</p>
  115. <ol>
  116. <li>It doesn’t impact <strong>privacy</strong>. If you’ve ever explored the GA sidebar, you’ll have seen that GA collects a baffling array of location, demographic and acquisition data from your site users. GA can collect this because it runs custom JavaScript in the client’s browser. Netlify Analytics data is compiled from server request logs: it can’t use anything other than the data provided to it by regular HTTP requests from the browser.</li>
  117. <li>It doesn’t impact <strong>performance</strong>. All client-side analytics libraries mean forcing extra JavaScript on users when there’s no benefit to them, whereas code running only on the server will have zero impact on users.</li>
  118. </ol>
  119. <p>Compared to GA, the set of features is fairly minimal, but for small sites like my own personal blog, I’ve found it refreshingly simple to find the information I want. All the data for your site is shown on a single dashboard screen including graphs of visitor numbers over time, a list of top pages, and something I’ve found especially useful: a list of resources which returned the most 404 errors (something that server-side analytics is able to handle far better than client-side analytics).</p>
  120. <p><picture>
  121. <source srcset="https://iainbean.com/netlify-analytics-not-found_200.webp 200w,https://iainbean.com/netlify-analytics-not-found_400.webp 400w,https://iainbean.com/netlify-analytics-not-found_800.webp 800w,https://iainbean.com/netlify-analytics-not-found_1000.webp 1000w,https://iainbean.com/netlify-analytics-not-found_1600.webp 1600w," type="image/webp" sizes="calc(100vw - 2rem)">
  122. <img src="https://iainbean.com/images/posts/netlify-analytics-not-found.png" srcset="https://iainbean.com/netlify-analytics-not-found_200.png 200w,https://iainbean.com/netlify-analytics-not-found_400.png 400w,https://iainbean.com/netlify-analytics-not-found_800.png 800w,https://iainbean.com/netlify-analytics-not-found_1000.png 1000w,https://iainbean.com/netlify-analytics-not-found_1600.png 1600w," alt="A screenshot from Netlify Analytics showing a list of resources and the number of 404 errors caused by each" falsefalsefalsefalse=""/>
  123. </source></picture></p>
  124. <p>There are some issues worth pointing out, perhaps the biggest of which is Netlify’s accuracy: compared to GA which seems to generally underestimate visitor numbers, because Netlify uses raw access logs it treats traffic from bots and aggregators (which don’t tend to run JavaScript) the same as genuine users, meaning your visitor numbers are artificially inflated with non-human visitors. While I appreciate how clean and free of configuration Netlify Analytics is, there are some glaring omissions: firstly, I’d like an option to filter out known bots based on user-agent string; I’d also like to see a method for viewing or exporting data older than 30 days — the lack of historical data makes it hard to do any serious data analysis.</p>
  125. <p>If you aren’t using Netlify, there are other server-side analytics but most require some form of subscription fee. If you’re not afraid of some manual set up you could try <a href="https://awstats.sourceforge.io/">AWStats</a>, an open-source program written in Perl, which parses your server log files and builds an interface which you can use to explore your data.</p>
  126. <h2>What if I don’t have a choice?</h2>
  127. <p>Google Analytics is now so common that for many it’s become synonymous with the word, ‘analytics’. If you build websites for clients, chances are they will expect to be able to log in to ‘analytics’ using their Google account and see the familiar reporting interface. You may have other, more pressing battles you’d rather fight with a client: maybe they insist on using animated <span class="small-caps lowercase">GIF</span>s everywhere, or have a brand palette with <em>only</em> inaccessible colour combinations.</p>
  128. <p>I’m not saying this data can’t be used for good: there are plenty of "top 10 benefits of Google Analytics" articles out there explaining how you can use GA data to make targeted improvements to your site. For example, you could try to improve the content on your most visited pages, or if the majority of your visitors are on mobile devices, you can focus your design on smaller screen sizes. If you’ve got to have it, at least use it to make your site better, but if you aren’t using it to drive improvements to your site, you’re better off without it.</p>
  129. <p>I've found that for my own blog, a combination of Google Search Console, Twitter analytics and Netlify analytics provide me with enough information that I haven’t needed to look further. I’m lucky enough to be able to afford the $9/month fee but I understand that many people, including clients, will be hesitant to pay for something they can get for free elsewhere. It isn’t up to me to tell you whether the privacy of your website users is worth money out of your pocket, but I’d encourage you to ask yourself that question.</p>
  130. <p>Google don’t give away analytics for free as an act of kindness: they’re still, primarily, an advertising company and the more data they can feed into their ad-targeting algorithms, the more money they can make from selling ads. By giving up your users’ data <em>voluntarily</em>, you’re doing Google a favour, but doing your users a disservice.</p>
  131. </main>
  132. </article>
  133. <hr>
  134. <footer>
  135. <p>
  136. <a href="/david/" title="Aller à l’accueil">🏠</a> •
  137. <a href="/david/log/" title="Accès au flux RSS">🤖</a> •
  138. <a href="http://larlet.com" title="Go to my English profile" data-instant>🇨🇦</a> •
  139. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">📮</a> •
  140. <abbr title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340">🧚</abbr>
  141. </p>
  142. <template id="theme-selector">
  143. <form>
  144. <fieldset>
  145. <legend>Thème</legend>
  146. <label>
  147. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  148. </label>
  149. <label>
  150. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  151. </label>
  152. <label>
  153. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  154. </label>
  155. </fieldset>
  156. </form>
  157. </template>
  158. </footer>
  159. <script type="text/javascript">
  160. function loadThemeForm(templateName) {
  161. const themeSelectorTemplate = document.querySelector(templateName)
  162. const form = themeSelectorTemplate.content.firstElementChild
  163. themeSelectorTemplate.replaceWith(form)
  164. form.addEventListener('change', (e) => {
  165. const chosenColorScheme = e.target.value
  166. localStorage.setItem('theme', chosenColorScheme)
  167. toggleTheme(chosenColorScheme)
  168. })
  169. const selectedTheme = localStorage.getItem('theme')
  170. if (selectedTheme && selectedTheme !== 'undefined') {
  171. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  172. }
  173. }
  174. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  175. window.addEventListener('load', () => {
  176. let hasDarkRules = false
  177. for (const styleSheet of Array.from(document.styleSheets)) {
  178. let mediaRules = []
  179. for (const cssRule of styleSheet.cssRules) {
  180. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  181. continue
  182. }
  183. // WARNING: Safari does not have/supports `conditionText`.
  184. if (cssRule.conditionText) {
  185. if (cssRule.conditionText !== prefersColorSchemeDark) {
  186. continue
  187. }
  188. } else {
  189. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  190. continue
  191. }
  192. }
  193. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  194. }
  195. // WARNING: do not try to insert a Rule to a styleSheet you are
  196. // currently iterating on, otherwise the browser will be stuck
  197. // in a infinite loop…
  198. for (const mediaRule of mediaRules) {
  199. styleSheet.insertRule(mediaRule.cssText)
  200. hasDarkRules = true
  201. }
  202. }
  203. if (hasDarkRules) {
  204. loadThemeForm('#theme-selector')
  205. }
  206. })
  207. </script>
  208. </body>
  209. </html>