<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="pretty-atom-feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  
  <title>Full Atom archive feed for my blog by Matt Hobbs (Nooshu.com)</title>
  <subtitle>This is the website of Matt Hobbs, who is a Frontend Engineering Manager from Oxfordshire, UK.</subtitle>
  <link href="https://nooshu.com/feed/full-atom.xml" rel="self" />
  <link href="https://nooshu.com/" />
  <updated>2026-06-20T00:00:00Z</updated>
  <id>https://nooshu.com/</id>
  <author>
    <name>Matt Hobbs</name>
  </author>
  <entry>
    <title>How a Simple CSP Tweak Turned into an AI Project</title>
    <link href="https://nooshu.com/blog/2026/06/20/how-a-simple-csp-tweak-turned-into-an-ai-project/" />
    <updated>2026-06-20T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2026/06/20/how-a-simple-csp-tweak-turned-into-an-ai-project/</id>
    <content type="html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;&lt;p&gt;In this post, I’ll tell the story of how a stray semicolon led me to build an AI-powered tool to make creating Content Security Policies (CSPs) a bit less painful for developers.&lt;/p&gt;&lt;p&gt;I’ve written about CSPs on this blog before:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/&quot;&gt;&lt;strong&gt;December 2024&lt;/strong&gt;: Securing your static website with HTTP response headers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/&quot;&gt;&lt;strong&gt;February 2025&lt;/strong&gt;: Configuring your Content-Security-Policy on your development environment in 11ty&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To quote from my 2024 post:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The Content Security Policy (CSP) header is a security feature that protects web applications from attacks like Cross-Site Scripting (XSS) and code injection by controlling the sources from which browsers can load and execute content. If there&#39;s only a single response header you implement today, then make it the CSP Response header.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The post has a few more details, so if you are interested you can &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#content-security-policy-csp&quot;&gt;find the Content-Security-Policy (CSP) section here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;As mentioned in the post, there are already a huge number of tools available on the web for creating CSP’s, but none of them really fit my needs. So, I decided to write my own tool using &lt;a href=&quot;https://cursor.com/referral?code=XDKDHWAJX4RJ&quot;&gt;cursor.ai&lt;/a&gt; and host it on the web for everyone to evaluate and use. &lt;strong&gt;Note&lt;/strong&gt;: I&#39;ve included my referral link so any readers can get 50% off their first month subscription.&lt;/p&gt;&lt;p&gt;It is called &lt;a href=&quot;https://csp-playground.dev/&quot;&gt;CSP Playground&lt;/a&gt;⁠. Despite the name, writing a CSP is rarely described as “fun”, and “playground” may be overselling the experience somewhat.&lt;/p&gt;&lt;p&gt;It seemed as sensible a name as any, though. Besides, the domain wasn’t taken and only cost £9, which is cheap enough to make almost any naming decision seem justified!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using Cloudflare Registrar I got a quote for csp-playground.secuity, only $2000.20 per year!&quot; decoding=&quot;async&quot; height=&quot;108&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/06/20/how-a-simple-csp-tweak-turned-into-an-ai-project/eRJUnGcXhx-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/06/20/how-a-simple-csp-tweak-turned-into-an-ai-project/eRJUnGcXhx-300.webp 300w, https://nooshu.com/blog/2026/06/20/how-a-simple-csp-tweak-turned-into-an-ai-project/eRJUnGcXhx-600.webp 600w, https://nooshu.com/blog/2026/06/20/how-a-simple-csp-tweak-turned-into-an-ai-project/eRJUnGcXhx-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I briefly considered splashing out on a &lt;code&gt;.security&lt;/code&gt; top-level domain (TLD), until &lt;a href=&quot;https://www.cloudflare.com/en-gb/products/registrar/&quot;&gt;Cloudflare Registrar&lt;/a&gt; reminded me that the privilege would cost &lt;strong&gt;only £1,515 per year&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;I suspect telling my wife that we’ve cancelled our summer holiday plans so I can own a particularly niche bit of the internet would have tested the “for richer or poorer” part of our vows rather more thoroughly than intended. 😬&lt;/p&gt;&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;/h2&gt;&lt;p&gt;If any readers have tried to write a CSP “manually”, you will understand how painful it really is! Understanding the very particular syntax is challenging. Although CSP itself does not have to be written on a single line conceptually, when the header is sent over the network, it must not contain any arbitrary line breaks, else it will be classed as invalid. And don’t even get me started on the use of &lt;code&gt;;&lt;/code&gt;, one misplaced semicolon and your browser console will throw a very red looking hissy fit!&lt;/p&gt;&lt;p&gt;There are whole RFC’s dedicated to the specifics of HTTP header fields, that I won’t even pretend to understand! Linked below for a little light reading, if you are struggling to sleep!&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7231&quot;&gt;RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/info/rfc9110/&quot;&gt;RFC 9110 - HTTP Semantics&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9112&quot;&gt;RFC 9112 - HTTP/1.1&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Anyway, as I always do, I’ve wandered off on a bit of a tangent.&lt;/p&gt;&lt;p&gt;What I’m basically saying is writing CSP’s is complicated and frustrating (especially for a terrible typist like myself!), So why not make life easier and spend a couple of days in &quot;AI world” and build a tool to make your life easier? You never know, it may even help others too!&lt;/p&gt;&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Form-based editor for every standard CSP directive&lt;/strong&gt;: No hand-editing of semicolon syntax&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Import from URL&lt;/strong&gt;: Fetch a policy from headers or meta tags, or detect when none exists&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Paste headers or policy&lt;/strong&gt;: Paste a full HTTP response header block or a single &lt;code&gt;Content-Security-Policy&lt;/code&gt; line; the CSP is extracted in the browser and used to pre-fill the form&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Real-time security score&lt;/strong&gt;: Including actionable recommendations that navigate you instantly to the exact directive&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Nonce and hash helpers&lt;/strong&gt;: Generate values and copy ready-to-paste HTML snippets&lt;/li&gt;&lt;li&gt;&lt;strong&gt;One-click export&lt;/strong&gt;: Policy, header line, or server config for Apache, Nginx, Caddy, Cloudflare, Netlify, Vercel, and more&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Report-only mode&lt;/strong&gt;: Option to output the CSP using &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt;. Allowing for testing before enforcement&lt;/li&gt;&lt;li&gt;&lt;strong&gt;MDN links and inline help&lt;/strong&gt;: Every directive and sandbox flag links directly to the relevant MDN content&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Privacy focused by design&lt;/strong&gt;: No accounts, no tracking, and no database. Nothing is stored because there’s nowhere to store it&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Runs mostly in the browser&lt;/strong&gt;: Policy editing and paste import stay local; the only server-side processing happens when you explicitly import a URL to fetch your site’s CSP&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Free and open-source&lt;/strong&gt;: &lt;a href=&quot;https://choosealicense.com/licenses/mit/&quot;&gt;MIT licensed&lt;/a&gt; at &lt;a href=&quot;https://csp-playground.dev/&quot;&gt;https://csp-playground.dev/&lt;/a&gt;, &lt;a href=&quot;https://github.com/Nooshu/CSP-Playground&quot;&gt;code available on GitHub&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Clean design&lt;/strong&gt; Built on the fantastic &lt;a href=&quot;https://utopia.fyi&quot;&gt;utopia.fyi&lt;/a&gt; Fluid Design CSS grid built by &lt;a href=&quot;https://clearleft.com/&quot;&gt;Clearleft&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;audit&quot;&gt;Audit&lt;/h3&gt;&lt;p&gt;Start from a real policy instead of a blank form. Enter a URL and CSP Playground will retrieve the CSP from the response headers or a &lt;code&gt;&amp;lt;meta&gt;&lt;/code&gt; tag. Alternatively, you can paste the headers or the policy string directly. Everything stays in the browser, with the exception of URL lookups.&lt;/p&gt;&lt;p&gt;Whichever approach you choose, the editor is pre-populated so you can quickly see what is being enforced.&lt;/p&gt;&lt;p&gt;If the lookup finds problems (duplicates, deprecated directives, odd formatting), it flags them and suggests fixes.&lt;/p&gt;&lt;p&gt;For sites where there is no CSP at all, the tool does not leave a user at in the lurch: it points you to a short guide on why a policy matters and how to start safely, so auditing either an existing policy or a blank slate becomes the first step toward a stronger CSP.&lt;/p&gt;&lt;h3 id=&quot;build&quot;&gt;Build&lt;/h3&gt;&lt;p&gt;Whether you are starting from scratch or updating an existing policy, you can work through each CSP directive using a structured form. Add sources without worrying about syntax, generate (placeholder) nonces and hashes for inline scripts and styles, and copy ready-to-use HTML snippets.&lt;/p&gt;&lt;p&gt;Every directive links directly to &lt;a href=&quot;https://developer.mozilla.org/&quot;&gt;MDN&lt;/a&gt;, so the documentation is always one click away when you need it.&lt;/p&gt;&lt;h3 id=&quot;improve&quot;&gt;Improve&lt;/h3&gt;&lt;p&gt;As you edit, CSP Playground scores your policy in real time and highlights improvements you can make. Recommendations are practical, and show the expected score increase, simply click on the recommendation, and it takes you straight to the relevant directive. You can also see your potential score if you applied every suggestion. This makes it a whole lot easier to focus on the changes that matter most, even if you’ve never written a CSP before.&lt;/p&gt;&lt;h3 id=&quot;deploy&quot;&gt;Deploy&lt;/h3&gt;&lt;p&gt;When you are happy with your shiny new Content Security Policy, CSP Playground generates the full header and provides config snippets for web servers and hosting platforms:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://httpd.apache.org/&quot;&gt;Apache&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nginx.org/en/&quot;&gt;Nginx&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://caddyserver.com/&quot;&gt;Caddy&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.litespeedtech.com/products/litespeed-web-server&quot;&gt;LiteSpeed&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.iis.net/&quot;&gt;Microsoft IIS&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://pages.cloudflare.com/&quot;&gt;Cloudflare Pages&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://vercel.com/&quot;&gt;Vercel&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://traefik.io/traefik&quot;&gt;Traefik&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.envoyproxy.io/&quot;&gt;Envoy&lt;/a&gt;&lt;/li&gt;&lt;li&gt;more can be added on request, just &lt;a href=&quot;https://github.com/Nooshu/CSP-Playground/issues&quot;&gt;create an issue&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You can also toggle a setting so that the server setup only attaches the CSP header to HTML responses, cutting down on repeated headers across all static assets, where they aren’t required.&lt;/p&gt;&lt;p&gt;Lastly, you can switch to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy-Report-Only&quot;&gt;Report-Only mode&lt;/a&gt; to test changes before enforcement, with links to MDN guidance to help you roll out your new (or updated) CSP safely.&lt;/p&gt;&lt;h2 id=&quot;result&quot;&gt;Result&lt;/h2&gt;&lt;p&gt;So, the first test for the tool was the CSP for this very website, after all it’s the reason I built it! As mentioned at the start of the post, the first result of my initial iteration of the tool caught a stray. It must have been live for quite a while, as I haven’t touched the CSP since February 2025. Thankfully, it wasn’t serious, hence why it went unnoticed. The validator picked it up and changed this:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;Content-Security-Policy: base-uri &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;child-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;connect-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;default-src &lt;span class=&quot;token string&quot;&gt;&#39;none&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;img-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://v1.indieweb-avatar.11ty.dev/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;font-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;form-action &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://webmention.io&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;frame-ancestors &lt;span class=&quot;token string&quot;&gt;&#39;none&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;frame-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://player.vimeo.com/ https://www.slideshare.net/ https://www.youtube.com/ https://giscus.app/ https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;manifest-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;media-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;object-src &lt;span class=&quot;token string&quot;&gt;&#39;none&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;script-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;inline-speculation-rules&#39;&lt;/span&gt; https://ajax.cloudflare.com https://giscus.app/ https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;script-src-elem &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;inline-speculation-rules&#39;&lt;/span&gt; https://ajax.cloudflare.com https://giscus.app/ https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;style-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;unsafe-inline&#39;&lt;/span&gt; https://giscus.app/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;worker-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To this:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;Content-Security-Policy: base-uri &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;child-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;connect-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;default-src &lt;span class=&quot;token string&quot;&gt;&#39;none&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;img-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://v1.indieweb-avatar.11ty.dev/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;font-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;form-action &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://webmention.io&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;frame-ancestors &lt;span class=&quot;token string&quot;&gt;&#39;none&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;frame-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; https://player.vimeo.com/ https://www.slideshare.net/ https://www.youtube.com/ https://giscus.app/ https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;manifest-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;media-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;object-src &lt;span class=&quot;token string&quot;&gt;&#39;none&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;script-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;inline-speculation-rules&#39;&lt;/span&gt; https://ajax.cloudflare.com https://giscus.app/ https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;script-src-elem &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;inline-speculation-rules&#39;&lt;/span&gt; https://ajax.cloudflare.com https://giscus.app/ https://challenges.cloudflare.com/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;style-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;unsafe-inline&#39;&lt;/span&gt; https://giscus.app/&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;worker-src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It took me a little while to spot the difference because the validator simply reported a &quot;semicolon change&quot;. If you are able to notice these small changes straight away, congratulations, you’re more observant than I am!&lt;/p&gt;&lt;p&gt;For any readers wondering the only difference is that the trailing semicolon in the second version has been removed. It’s actually entirely optional and has no effect on how the CSP behaves.&lt;/p&gt;&lt;p&gt;I admit it’s hardly a groundbreaking discovery, but it was reassuring to see that the validation functionality was doing exactly what it should.&lt;/p&gt;&lt;p&gt;The next changes it recommended made more of an impact and improved my CSP score. The initial score was &lt;strong&gt;79%&lt;/strong&gt;. Not bad, but we can do better with only 2 minor changes:&lt;/p&gt;&lt;h3 id=&quot;trusted-types-tt&quot;&gt;trusted-types (TT)&lt;/h3&gt;&lt;p&gt;I must admit I’d never heard of this directive until the tool recommended it.&lt;/p&gt;&lt;p&gt;It&#39;s only very recently (February) that it has been marked as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API&quot;&gt;Baseline in the MDN documentation&lt;/a&gt; due to broad cross browser support across all modern browsers.&lt;/p&gt;&lt;p&gt;It was actually Mozilla who were dragging their feet on this directive, but thankfully changed their stance on it &lt;a href=&quot;https://www.theregister.com/security/2023/12/21/mozilla-decides-trusted-types-is-a-worthy-security-feature/1141670&quot;&gt;back in 2023&lt;/a&gt;. Full support in Firefox was released in version 148 (released 24 February 2026).&lt;/p&gt;&lt;p&gt;If you’re curious about this directive, here’s an &lt;a href=&quot;https://en.wiktionary.org/wiki/ELI5&quot;&gt;ELI5&lt;/a&gt; explanation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Think of an Aeroplane at an airport.&lt;/li&gt;&lt;li&gt;Before TT: Anybody could walk straight onto the plane.&lt;/li&gt;&lt;li&gt;After TT: Everybody must:&lt;ol&gt;&lt;li&gt;Go through security.&lt;/li&gt;&lt;li&gt;Have their passport checked.&lt;/li&gt;&lt;li&gt;Receive an approved boarding pass.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Only passengers (or potentially executable content in this analogy) with a valid boarding pass are allowed onto the plane.&lt;/p&gt;&lt;p&gt;Trusted Types are the boarding pass.&lt;/p&gt;&lt;p&gt;For readers who are older than 5, you can read all about the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API&quot;&gt;Trusted Types API in great depth on MDN&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;upgrade-insecure-requests&quot;&gt;upgrade-insecure-requests&lt;/h3&gt;&lt;p&gt;This directive is pretty self-explanatory given the name. &lt;code&gt;upgrade-insecure-requests&lt;/code&gt; instructs a browser to automatically upgrade HTTP requests to HTTPS before loading resources. Resources that do not support HTTPS will fail to load, although this is &lt;a href=&quot;https://almanac.httparchive.org/en/2025/security#transport-security&quot;&gt;very rare on the modern web&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;By simply adding:&lt;/p&gt;&lt;p&gt;&lt;code&gt;trusted-types &#39;none&#39;; upgrade-insecure-requests&lt;/code&gt;&lt;/p&gt;&lt;p&gt;to my CSP, it bumped the score from &lt;strong&gt;79% up to 87%&lt;/strong&gt;!&lt;/p&gt;&lt;p&gt;A respectable gain for what was, in the grand scheme of things, barely any work!&lt;/p&gt;&lt;p&gt;Adding &lt;code&gt;upgrade-insecure-requests&lt;/code&gt; improves protection against mixed content by upgrading HTTP resources to HTTPS. Meanwhile, &lt;code&gt;trusted-types &#39;none&#39;&lt;/code&gt; does not actually enforce Trusted Types, but it does make the decision to not use them very explicit.&lt;/p&gt;&lt;p&gt;For the moment I have simply disabled the use of Trusted Types in the CSP, but in the future I will likely enable enforcement via the use of:&lt;/p&gt;&lt;p&gt;&lt;code&gt;require-trusted-types-for &#39;script&#39;; trusted-types default;&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Once enforced this will bring the sites CSP score up to 95%! 🎉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update 1&lt;/strong&gt;: I tested &lt;code&gt;require-trusted-types-for &#39;script&#39;;&lt;/code&gt; with &lt;code&gt;&#39;trusted-types default;&lt;/code&gt;, but it breaks a few scripts across the site, so have reverted to &lt;code&gt;trusted-types &#39;none&#39;;&lt;/code&gt;. I will update once I have resolved the issues. It just goes to show how much testing you need to do when you change your site&#39;s CSP! Use &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; if in doubt!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update 2&lt;/strong&gt;: It turns out these errors occur because some of these scripts use legacy code, such as:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// or element.innerHTML = `&amp;lt;p&gt;${text}&amp;lt;/p&gt;`;&lt;/span&gt;
element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;innerHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;p&gt;&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; text &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;/p&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To properly resolve these issues, you&#39;d need to refactor the scripts to use more standard DOM manipulation methods, like:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; p &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;p&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The problem comes when you don&#39;t &quot;own&quot; these scripts, e.g. a 3rd-party script. It&#39;s just not practical to update these scripts without the original author(s) being involved, as any new version of the script will overwrite the changes.&lt;/p&gt;&lt;p&gt;There is a workaround:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// run this code immediately using an Immediately Invoked Function Expression (IIFE)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// check see if the browser supports the trustedTypes API&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;trustedTypes&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;createPolicy&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Create a Trusted Types policy called &quot;default&quot; policy for the browser to use e.g. `trusted-types default;`&lt;/span&gt;
		trustedTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;default&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// allow all HTML through e.g. I trust every HTML string. (this is bad!)&lt;/span&gt;
			&lt;span class=&quot;token function function-variable&quot;&gt;createHTML&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is critical that this code executes &lt;strong&gt;before&lt;/strong&gt; the browser tries to execute &lt;code&gt;innerHTML&lt;/code&gt;, as without the policy the code will fail. Unfortunately, this code has a major security implication: XSS vulnerabilities are no longer prevented.&lt;/p&gt;&lt;p&gt;Given this implication, I&#39;m going to stick to &lt;code&gt;trusted-types &#39;none&#39;;&lt;/code&gt; for the moment, or at least until I look into using &lt;a href=&quot;https://github.com/cure53/DOMPurify&quot;&gt;DOMPurify&lt;/a&gt; to &quot;enable&quot; XSS protection.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update 3&lt;/strong&gt;: I have tried a solution using ) on my preview environments, and I&#39;m still seeing a host of little issues like missing images in some functionality, so I&#39;ll stick with &lt;code&gt;trusted-types &#39;none&#39;;&lt;/code&gt; for the moment as it&#39;s the only otion that works at the moment. It&#39;s unfortunate that it really isn&#39;t adding much in terms of site security, and it&#39;s only there for the &quot;CSP Score&quot;, but on the positive side it&#39;s been another CSP learning curve for me! So when using the &lt;code&gt;trusted-types&lt;/code&gt; directive, you should definetly expect issues with JavaScript that uses legacy code! It could be a while before anyone gets to roll out this directive without issues!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update 4&lt;/strong&gt;: I have tried a solution using &lt;a href=&quot;https://github.com/cure53/DOMPurify&quot;&gt;DOMPurify&lt;/a&gt; on my preview environments, and I&#39;m still seeing a host of little issues like missing images in some functionality. So, I&#39;ll stick with for the moment, as it&#39;s the only option that works at the moment. It&#39;s unfortunate that it really isn&#39;t adding much in terms of site security, and it&#39;s only there for the &quot;CSP Score&quot;, but on the positive side it&#39;s been another CSP learning curve for me! So when using the &lt;code&gt;trusted-types&lt;/code&gt; directive, you should definitely expect issues with JavaScript that uses legacy code! It could be a while before anyone gets to roll out this directive without issues!&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;I promised myself that I wouldn’t make this blog post an epic read this time, so fingers crossed I have kept my promise! If you happen to be interested in my “strategy” and opinion (for what it&#39;s worth) on AI assisted coding off the back of this post, I’ve wrote all about it here in my &lt;a href=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/&quot;&gt;FractalAI: Generating Infinity in the Browser&lt;/a&gt; post from April.&lt;/p&gt;&lt;p&gt;Lastly, you can find &lt;a href=&quot;https://csp-playground.dev/&quot;&gt;CSP Playground here&lt;/a&gt;, and the &lt;a href=&quot;https://github.com/Nooshu/CSP-Playground&quot;&gt;GitHub repo for the code here&lt;/a&gt;. &lt;a href=&quot;https://github.com/Nooshu/CSP-Playground/pulls&quot;&gt;PRs&lt;/a&gt; and &lt;a href=&quot;https://github.com/Nooshu/CSP-Playground/issues&quot;&gt;Issues&lt;/a&gt; welcome!&lt;/p&gt;&lt;p&gt;I hope you find this little tool useful! I’ve enjoyed building it and learning a little more about the world of CSPs, and Trusted Types.&lt;/p&gt;&lt;p&gt;As always, thanks for reading, feedback and comments are welcome. You can &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;contact me here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;20/06/26: Initial post published.&lt;/li&gt;&lt;li&gt;20/06/26: Reverted &lt;code&gt;require-trusted-types-for &#39;script&#39;;&lt;/code&gt; with &lt;code&gt;trusted-types default;&lt;/code&gt; to &lt;code&gt;trusted-types &#39;none&#39;;&lt;/code&gt; because it broke a number of scripts on the site!&lt;/li&gt;&lt;li&gt;20/06/26: Added information about the &lt;code&gt;require-trusted-types-for &#39;script&#39;;&lt;/code&gt; workaround before 3rd-party scripts are updated to use non-legacy DOM methods. Sticking with &lt;code&gt;trusted-types &#39;none&#39;;&lt;/code&gt; for the moment.&lt;/li&gt;&lt;li&gt;21/06/26: I tried to resolve issues with &lt;code&gt;require-trusted-types-for &#39;script&#39;;&lt;/code&gt; with &lt;code&gt;trusted-types default;&lt;/code&gt; by using &lt;a href=&quot;https://github.com/cure53/DOMPurify&quot;&gt;DOMPurify&lt;/a&gt;, but there are still other problems that pop up, so still sticking with &lt;code&gt;trusted-types &#39;none&#39;;&lt;/code&gt;. I&#39;ll chalk this one up to a learning exercise, and leave it as is for now! After all, this post was only supposed to be about &lt;a href=&quot;https://csp-playground.dev/&quot;&gt;CSP Playground&lt;/a&gt;, not the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/trusted-types&quot;&gt;Trusted Types directive&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>FractalAI: Generating Infinity in the Browser</title>
    <link href="https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/" />
    <updated>2026-04-21T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/</id>
    <content type="html">&lt;h2 id=&quot;why-i-built-fractalai&quot;&gt;Why I Built FractalAI&lt;/h2&gt;&lt;p&gt;Although AI has been a concept that has been discussed for the past 80 years (Thanks to Alan Turing, and &lt;a href=&quot;https://en.wikipedia.org/wiki/Artificial_intelligence#History&quot;&gt;many others&lt;/a&gt;), it’s only in the past 5 to 10 years that it has become vastly more powerful. It is currently the hot topic in pretty much every industry on the planet! Software development in particular has already changed drastically, with new tools and AI Models popping up every week. You only have to look at the &lt;a href=&quot;https://survey.stackoverflow.co/2025/ai&quot;&gt;Stack Overflow Developer Survey 2025&lt;/a&gt; to see how common its usage is in Software development:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;84% of developers are using or planning to use AI&lt;/li&gt;&lt;li&gt;51% of professional developers use it daily&lt;/li&gt;&lt;li&gt;61% of professional developers look at it favourably&lt;/li&gt;&lt;li&gt;35% of developers visit Stack Overflow due to AI-related issues&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So like it or not, AI is here, and it is here to stay. So to keep up with &quot;the pack” it’s probably a good idea to start learning how to use it, (and more importantly) where &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/when-ai-writes-almost-all-code-what&quot;&gt;to be cautious&lt;/a&gt;!&lt;/p&gt;&lt;h2 id=&quot;the-constraint-100-ai-generated-code&quot;&gt;The Constraint: 100% AI-Generated Code&lt;/h2&gt;&lt;p&gt;To get to grips with AI and its current limits, I decided to set myself a task: build a new hobby project using only AI. All code, all functionality, and even all the design would be generated by me prompting the AI to tweak and generate the frontend code. This is an excellent way to see how AI handles CSS and general usability. For this, I use an AI Tool called &lt;a href=&quot;https://cursor.com/&quot;&gt;Cursor&lt;/a&gt;. There are many tools available, but since subscribing to their pro plan, I’ve seen no real reason to change. Some of the key advantages I see over other AI tools are:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Project-wide context awareness&lt;/strong&gt;: Cursor understands the entire codebase rather than a single file, which leads to more accurate suggestions and safer large-scale changes across interconnected systems.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Multi-file editing and refactoring&lt;/strong&gt;: Cursor can apply consistent updates across multiple files in one action, making large refactors and cross-cutting changes significantly more efficient.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;AI-driven workflows&lt;/strong&gt;: Cursor acts as an AI agent that can plan and execute multistep development tasks rather than just offering inline code suggestions.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Context-aware integrated chat&lt;/strong&gt;: Cursor’s built-in chat has full awareness of the repository and can directly modify code, reducing the need for manual context sharing.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Model flexibility&lt;/strong&gt;: Cursor allows developers to switch between different AI models, enabling optimisation for performance, cost, or task-specific quality.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Optimised for complex systems&lt;/strong&gt;: Cursor performs particularly well in large and complex codebases where deeper reasoning and architectural understanding are required.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Faster end-to-end task completion&lt;/strong&gt;: Cursor often reduces overall development time by completing entire tasks in fewer steps, even if individual code suggestions are not the fastest.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;AI-native IDE design&lt;/strong&gt;: Cursor is built from the ground up as an AI-first environment, enabling tighter integration and more advanced workflows than plugin-based alternatives.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;FYI&lt;/strong&gt;: I’m not sponsored by Cursor, I just love how clean and simple it is too pickup and use! There are &lt;a href=&quot;https://www.gartner.com/reviews/market/ai-code-assistants&quot;&gt;many others on the web&lt;/a&gt; that you can choose instead!&lt;/p&gt;&lt;h2 id=&quot;what-fractalai-actually-does&quot;&gt;What FractalAI Actually Does&lt;/h2&gt;&lt;p&gt;Well, as you may have guessed already from the name of the blog post, the project I chose to build what a fractal generator 100% built on modern browser technology. The code is all &lt;a href=&quot;https://github.com/Nooshu/FractalAI?tab=License-1-ov-file&quot;&gt;open-source&lt;/a&gt; and free for anyone to clone, modify and use as they wish! The code repository is &lt;a href=&quot;https://github.com/Nooshu/FractalAI&quot;&gt;on GitHub here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The project currently comes with the following features:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Fractal Selection&lt;/strong&gt;: Choose from 100+ fractal types via dropdown menu.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Adjustable Iterations&lt;/strong&gt;: Control detail level (10 to 400 iterations).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;35+ Colour Schemes&lt;/strong&gt;: Classic, Fire, Ocean, Rainbow variants, Monochrome, Forest, Sunset, Purple, Cyan, Gold, Ice, Neon, Cosmic, Aurora, Coral, Autumn, Midnight, Emerald, Rose Gold, Electric, Vintage, Tropical, Galaxy, Lava, Arctic, Sakura, Volcanic, Mint, Sunrise, Steel, Prism, Mystic, Amber, and more.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Zoom and Pan&lt;/strong&gt;: Click and drag to pan, scroll or double-click to zoom.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Real-time Parameter Adjustment&lt;/strong&gt;: Adjust Julia set constants, scales, and other parameters in real-time.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Screenshot Capture&lt;/strong&gt;: Save current view as PNG with EXIF metadata.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;FPS Monitoring&lt;/strong&gt;: Real-time frame rate display.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Coordinate Display&lt;/strong&gt;: View and copy exact fractal coordinates.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Presets System&lt;/strong&gt;: Quick-load pre-configured fractal views.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Favourites System&lt;/strong&gt;: Save and manage favourite fractal configurations.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Share &amp; URL State&lt;/strong&gt;: Share fractals via URL with encoded state.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This was just a copy / paste from the &lt;code&gt;README.md&lt;/code&gt; file in the repository, it actually comes with more features that you can read about like &lt;a href=&quot;https://github.com/Nooshu/FractalAI?tab=readme-ov-file#machine-learning-powered-discovery&quot;&gt;Machine Learning-Powered Discovery&lt;/a&gt;, &lt;a href=&quot;https://github.com/Nooshu/FractalAI?tab=readme-ov-file#performance-optimizations&quot;&gt;Performance Optimizations&lt;/a&gt;, and &lt;a href=&quot;https://github.com/Nooshu/FractalAI?tab=readme-ov-file#advanced-features&quot;&gt;Advanced Features&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I’ve been fascinated with Fractals for many years, I actually dabbled with fractal generation and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API&quot;&gt;Web Workers&lt;/a&gt; back in &lt;a href=&quot;https://nooshu.com/lab/mandelbrot-set-renderer-using-javascript-and-the-canvas-api/&quot;&gt;January 2010&lt;/a&gt;, scarily that’s a whole 16-years ago! That makes me feel very 👴!&lt;/p&gt;&lt;p&gt;The reason fractals have always fascinated me is perfectly summed up in this quote from the godfather of fractals &lt;a href=&quot;https://en.wikipedia.org/wiki/Benoit_Mandelbrot&quot;&gt;Benoît Mandelbrot&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Bottomless wonders spring from simple rules, which are repeated without end.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I find it simply astounding that a whole infinite world can be rendered by a computer using such &quot;simple&quot; mathematics. For example, the iconic Mandelbrot fractal is all generated via this neat little formula:&lt;/p&gt;&lt;section&gt;&lt;eqn&gt;&lt;span class=&quot;katex-display&quot;&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.6389em;vertical-align:-.2083em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.044em&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.3011em&quot;&gt;&lt;span style=&quot;top:-2.55em;margin-left:-.044em;margin-right:.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;&lt;span class=&quot;mord mathnormal mtight&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mtight mbin&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mord mtight&quot;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.2083em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:1.1111em;vertical-align:-.247em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.044em&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.8641em&quot;&gt;&lt;span style=&quot;top:-2.453em;margin-left:-.044em;margin-right:.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mathnormal mtight&quot;&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-3.113em;margin-right:.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.247em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2222em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mbin&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2222em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.4306em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/eqn&gt;&lt;/section&gt;&lt;p&gt;Where:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;eq&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.5806em;vertical-align:-.15em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.044em&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.3011em&quot;&gt;&lt;span style=&quot;top:-2.55em;margin-left:-.044em;margin-right:.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.15em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.6444em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/eq&gt;&lt;/li&gt;&lt;li&gt;&lt;eq&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.5782em;vertical-align:-.0391em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;∈&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.6889em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathbb&quot;&gt;C&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/eq&gt;(a complex number)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A point&lt;eq&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.4306em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/eq&gt;belongs to the Mandelbrot set if the sequence does not diverge:&lt;/p&gt;&lt;section&gt;&lt;eqn&gt;&lt;span class=&quot;katex-display&quot;&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:1.45em;vertical-align:-.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mop op-limits&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.6944em&quot;&gt;&lt;span style=&quot;top:-2.4em;margin-left:0&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;&lt;span class=&quot;mord mathnormal mtight&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mtight mrel&quot;&gt;→&lt;/span&gt;&lt;span class=&quot;mord mtight&quot;&gt;∞&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-3em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span&gt;&lt;span class=&quot;mop&quot;&gt;lim&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.7em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;∣&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.044em&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.1514em&quot;&gt;&lt;span style=&quot;top:-2.55em;margin-left:-.044em;margin-right:.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mathnormal mtight&quot;&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.15em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;∣&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;&lt;span class=&quot;mord katex-vbox&quot;&gt;&lt;span class=&quot;katex-thinbox&quot;&gt;&lt;span class=&quot;rlap&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.8889em;vertical-align:-.1944em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;katex-inner&quot;&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mrel&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;katex-fix&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace nobreak&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;→&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.4306em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;∞&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/eqn&gt;&lt;/section&gt;&lt;p&gt;Which generates this absolutely stunning image that you can literally zoom in to forever!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The classic shape of the Mandelbrot fractal using my FractalAI generator with the Lava colour scheme.&quot; decoding=&quot;async&quot; height=&quot;508&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/GZqDDv3k3D-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/GZqDDv3k3D-300.webp 300w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/GZqDDv3k3D-600.webp 600w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/GZqDDv3k3D-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The image above really doesn’t do the details of the Mandelbrot justice! So why not take a look around the &lt;a href=&quot;https://fractals.world/?seed=eyJ0IjoibWFuZGVsYnJvdCIsImMiOiJjb3JhbCIsInoiOjIsImkiOjQwMCwib3giOi0wLjUzNSwib3kiOi0wLjAxNSwieHMiOjEsInlzIjoxfQ&quot;&gt;original setting I used to render it here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;why-the-browser-is-surprisingly-good-at-rendering-fractals&quot;&gt;Why the Browser Is Surprisingly Good at Rendering Fractals&lt;/h2&gt;&lt;p&gt;What makes the browser surprisingly good at rendering fractals is not just raw capability, but accessibility and reach. A fractal renderer written in the browser can run anywhere instantly with no installation, no setup, and no platform constraints. That is a powerful starting point. Additionaly there&#39;s JavaScript, as the language behind this project, it is embedded in every modern browser, making it one of the most widely available execution environments in the world.&lt;/p&gt;&lt;p&gt;This accessibility isn&#39;t theoretical, it shows up in everyday life. Just last month, while walking my 8-year-old son home from primary school, he told me about what he had been learning in &quot;Coding Club&quot; using his &lt;a href=&quot;https://microbit.org/new-microbit/&quot;&gt;BBC micro:bit&lt;/a&gt;. He was genuinely excited when he said:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Daddy, I’ve been learning all about something called JavaScript!&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;That moment really highlighted the scale of JavaScript’s reach. It is not just a professional tool, it is something being introduced at a very young age. When I explained that it forms a large part of my own work, it reinforced how deeply embedded it is in both education and industry.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The BBC micro:bit kit I purchased for my son contains: BBC micro:bit V2 board, Micro USB cable, Battery pack (holder), and 2× AAA batteries&quot; decoding=&quot;async&quot; height=&quot;600&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/OetDnwH4Om-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/OetDnwH4Om-300.webp 300w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/OetDnwH4Om-600.webp 600w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/OetDnwH4Om-800.webp 800w&quot; width=&quot;800&quot;&gt;&lt;/p&gt;&lt;p&gt;It is remarkable to think that a language first created by &lt;a href=&quot;https://en.wikipedia.org/wiki/Brendan_Eich&quot;&gt;Brendan Eich&lt;/a&gt; in just 10-days has evolved into a platform capable of driving complex visualisations like fractal rendering!&lt;/p&gt;&lt;p&gt;This, combined with modern browser features such as high-performance &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Engine/JavaScript&quot;&gt;JavaScript engines&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API&quot;&gt;GPU acceleration&lt;/a&gt; through &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API&quot;&gt;WebGL&lt;/a&gt;, and efficient &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API&quot;&gt;canvas APIs&lt;/a&gt;, the browser has become an unexpectedly capable environment for this kind of computationally intensive work.&lt;/p&gt;&lt;h2 id=&quot;ai-as-a-design-partner-not-just-a-tool&quot;&gt;AI as a Design Partner, Not Just a Tool&lt;/h2&gt;&lt;p&gt;I’ve found that AI is most powerful when it is treated as a design partner rather than just a tool for generating code. While it is clearly effective at implementation, its real strength lies in collaboration. You can ask it to plan a feature, and it will respond with probing questions that help clarify intent, constraints, and direction. Those questions are not incidental, they are often the key to shaping a better outcome, giving you the opportunity to guide and refine the approach early in a project.&lt;/p&gt;&lt;p&gt;Because of the breadth of knowledge it draws on, the interaction feels less like issuing instructions and more like a back and forth design &quot;conversation&quot;. It becomes a space for exploring ideas, testing assumptions, and iterating quickly before committing to an ideal solution.&lt;/p&gt;&lt;p&gt;This became particularly valuable in areas where I would not usually feel confident, such as design. Rather than treating that as a limitation, I leaned into AI to generate an initial direction. From there, my role shifted to refinement. Once a baseline design was in place, I could make targeted adjustments to the CSS and HTML, either through follow-up prompts or by working directly with insights gathered from the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools&quot;&gt;Browser DevTools&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Although I had originally set myself the constraint of not writing any code, I found that introducing precise, real-world context from DevTools significantly improved the quality of the AI’s output (more on this later in the post). By feeding in specific observations rather than abstract instructions, the responses became more relevant, more predictable, and easier to iterate on.&lt;/p&gt;&lt;p&gt;This is where AI moves beyond being a simple tool. It becomes a collaborator that helps shape both the design thinking and the implementation, with you firmly guiding the direction at each step.&lt;/p&gt;&lt;h2 id=&quot;the-architecture-without-writing-code-myself&quot;&gt;The Architecture (Without Writing Code Myself)&lt;/h2&gt;&lt;p&gt;This was easily the most engaging part of the process and where the most learning happened. Having the freedom to choose technologies and then observe how AI composes them into a coherent architecture is genuinely insightful, especially when you examine the reasoning behind those decisions.&lt;/p&gt;&lt;p&gt;When you are unsure whether a technology is a good fit for a project, you can quickly create a &quot;throwaway&quot; prototype and evaluate it. That shifts the role of AI from a coding assistant to a decision support tool. You are not just generating output, you are generating evidence.&lt;/p&gt;&lt;p&gt;Over time, this creates a feedback loop where each experiment improves your judgement. You learn what works, what to avoid, and why.&lt;/p&gt;&lt;p&gt;This aligns closely with core Agile principles:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Fail early to surface risk&lt;/li&gt;&lt;li&gt;Iterate quickly to explore options&lt;/li&gt;&lt;li&gt;Learn continuously to improve decisions&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you use AI for anything, use it to accelerate learning. Speed only matters when it leads to better decisions.&lt;/p&gt;&lt;h2 id=&quot;machine-learning-inside-the-browser&quot;&gt;Machine Learning Inside the Browser&lt;/h2&gt;&lt;p&gt;For many years, I’d wanted to have a play with machine learning (ML), but had never really had the opportunity. With the arrival of browser-based libraries like TensorFlow.js, the idea of running ML directly in the browser suddenly became far more accessible.&lt;/p&gt;&lt;p&gt;In this project, I saw an interesting opportunity to apply it to the “random fractal position” feature. The goal was simple on the surface. A user selects a fractal and a colour scheme, presses a button, and gets a random set of x, y, and z values to explore.&lt;/p&gt;&lt;p&gt;In practice, it was not that simple.&lt;/p&gt;&lt;p&gt;When the AI first implemented this in plain JavaScript, I ran into a fundamental problem. A large proportion of fractal space is effectively empty, which meant most “random” positions resulted in a blank or uninteresting visual. I initially tried constraining the co-ordinates to specific regions, but with over 100 fractals available, this quickly became unmanageable.&lt;/p&gt;&lt;p&gt;So I started thinking differently. Instead of hard-coding “good” regions, what if the system could learn what a good fractal view looks like?&lt;/p&gt;&lt;p&gt;That is where machine learning came in. The idea was to train a lightweight model to recognise visually interesting outputs, effectively teaching the random feature to make better choices over time. Using AI to help generate and iterate on this approach made the experimentation surprisingly fast.&lt;/p&gt;&lt;p&gt;Originally, I explored using &lt;a href=&quot;https://www.tensorflow.org/js&quot;&gt;TensorFlow.js&lt;/a&gt;, but it turned out to be far too heavy for this use case. Even compressed, it added around 450 KB of JavaScript, which is significant given the existing bundle size for all the fractal rendering functionality.&lt;/p&gt;&lt;p&gt;Instead, I opted for &lt;a href=&quot;https://caza.la/synaptic&quot;&gt;Synaptic&lt;/a&gt;. It is much smaller at around 45 KB when Brotli compressed, and while it has not seen recent updates, it proved more than capable for what I needed. It also keeps the overall performance profile of the application in check, which was a key consideration.&lt;/p&gt;&lt;p&gt;If required, this approach leaves the door open to swap Synaptic out for a more modern ML library in the future. But for now, it demonstrates something powerful. You can run meaningful machine learning directly in the browser, and use it to enhance user experience in ways that would be difficult to achieve with traditional logic alone.&lt;/p&gt;&lt;p&gt;As mentioned in the &lt;a href=&quot;https://github.com/Nooshu/FractalAI?tab=readme-ov-file#machine-learning-powered-discovery&quot;&gt;README.md&lt;/a&gt; file in the GitHub repository it is used for the following functionality:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;ML-Based Scoring&lt;/strong&gt;: Uses Synaptic.js neural network to score fractal configurations.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Hybrid Algorithm&lt;/strong&gt;: Combines fast heuristic screening with ML refinement.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Personalised Learning&lt;/strong&gt;: Trains on your favourites to learn your preferences.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&quot;Surprise Me&quot; Feature&lt;/strong&gt;: Discover interesting new fractal zooms automatically.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Background Training&lt;/strong&gt;: Model retrains automatically as you add favourites.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Local Storage&lt;/strong&gt;: ML model and favourites persist in browser storage.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You will find all of this functionality in the “Full-screen view” of the fractal viewer. The standard UI was starting to feel quite crowded, so that ended up being the most sensible place for it.&lt;/p&gt;&lt;h2 id=&quot;what-worked-surprisingly-well&quot;&gt;What Worked Surprisingly Well&lt;/h2&gt;&lt;h3 id=&quot;planning&quot;&gt;Planning&lt;/h3&gt;&lt;p&gt;Planning from the AI was excellent. This may be a Cursor-specific feature, but the follow-up questions it asked about how I wanted to approach a problem were a really nice touch. When you are unsure which direction the AI will take, this gives you useful insights early on.&lt;/p&gt;&lt;h3 id=&quot;crafted-prompts&quot;&gt;Crafted Prompts&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Top AI Tip&lt;/strong&gt;: Detailed prompts made a huge difference. The more specific I was, the more reliable the output became. In some cases, I even included images or copied exact code from DevTools just to make sure the AI fully understood the context.&lt;/p&gt;&lt;p&gt;I really liked how &lt;a href=&quot;https://bsky.app/profile/tonyalicea.dev&quot;&gt;Anthony Alicea&lt;/a&gt; explains AI in his &lt;a href=&quot;https://dontimitate.dev/courses/ai-assisted-development/&quot;&gt;Understanding AI-Assisted Software Development course&lt;/a&gt;. He visualises the AI “brain” as a cube, where each smaller blue cube represents a piece of knowledge the AI has learned. It’s a simple idea, but it makes how AI works much easier to grasp.&lt;/p&gt;&lt;p&gt;A visualisation from his course is shown below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A graphic of a cube filled with blue squares. Each of the squares represents some knowledge that the AI has learned during its training.&quot; decoding=&quot;async&quot; height=&quot;455&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/jsnmyNTWd2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/jsnmyNTWd2-300.webp 300w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/jsnmyNTWd2-600.webp 600w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/jsnmyNTWd2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This “knowledge” could include things like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Documents&lt;/li&gt;&lt;li&gt;Images&lt;/li&gt;&lt;li&gt;External data&lt;/li&gt;&lt;li&gt;Specifications&lt;/li&gt;&lt;li&gt;Examples&lt;/li&gt;&lt;li&gt;Instructions&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Your job when prompting the AI is to “sail” across this ocean of knowledge and get to a favourable output on the opposite side. To get better results, you need to give the AI waypoints or &quot;lighthouses&quot; to direct the AI through this ocean of knowledge, your prompts are these &quot;lighthouses&quot;. That way, you guide the path it takes, rather than leaving it to figure one out on its own.&lt;/p&gt;&lt;p&gt;So rather than saying:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Build me a website that renders fractals&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Be more specific:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Build me a website that renders fractals. Start with a simple Mandelbrot set and use the Canvas API to draw it. I’d also like you to leverage the WebGL API in the visualisation. Make sure the code is well documented and uses modern linting tools like Stylelint and ESLint to keep it maintainable. I also want to use Vite as the local development server…&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Now, you might be thinking, that’s a long prompt! It is. But if you want reliable output, you need to give the AI clear instructions to follow. Otherwise, it will just “set sail” across that vast corpus of information and produce something unfocused because it has too much freedom.&lt;/p&gt;&lt;p&gt;If there’s one thing I learned from this project, it’s this: spend time crafting detailed prompts. That’s how you steer the ship across the ocean of information and actually reach the result you want.&lt;/p&gt;&lt;h3 id=&quot;rules&quot;&gt;Rules&lt;/h3&gt;&lt;p&gt;Another useful thing I learned is that you can give the AI a consistent set of rules to follow across every prompt. In Cursor, these live in the &lt;code&gt;.cursor/rules&lt;/code&gt; directory in your repository.&lt;/p&gt;&lt;p&gt;In the FractalAI project, I ended up with four sets of rules:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Nooshu/FractalAI/blob/main/.cursor/rules/00-core-standards.mdc&quot;&gt;00-core-standards.mdc&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Nooshu/FractalAI/blob/main/.cursor/rules/10-security-dependencies.mdc&quot;&gt;0-security-dependencies.mdc&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Nooshu/FractalAI/blob/main/.cursor/rules/20-performance-frontend.mdc&quot;&gt;20-performance-frontend.mdc&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Nooshu/FractalAI/blob/main/.cursor/rules/30-testing-quality.mdc&quot;&gt;30-testing-quality.mdc&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The AI refers to these rules for every prompt, giving it consistent context and guidance. You might notice they look AI-generated, and you’d be right. It’s a bit meta, but I asked Cursor to write the rules it would follow in future prompts.&lt;/p&gt;&lt;p&gt;The thinking behind this was simple: it is far more likely to understand its own instructions than my rambling ones!&lt;/p&gt;&lt;p&gt;I’m not sure if this is specific to Cursor, or if other AIs follow a similar approach. It would be great to see some standardisation here to allow these rules to be portable across multiple AI tools in the future.&lt;/p&gt;&lt;p&gt;If this is already happening, I’d love to know. Please feel free to &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt; if you have any information!&lt;/p&gt;&lt;h3 id=&quot;writing-tests&quot;&gt;Writing Tests&lt;/h3&gt;&lt;p&gt;If you are not a fan of writing tests, even though you know you should, AI helps a lot. If it can write the code, it can just as easily write the tests too.&lt;/p&gt;&lt;p&gt;Where a human developer might get tired of writing tests, AI doesn&#39;t. That makes it much easier to push towards high coverage and keep things consistent.&lt;/p&gt;&lt;p&gt;And if you enjoy that warm feeling of seeing a wall of green in your terminal&#39;s test output, this definitely helps. I’m one of those people:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The code coverage report from my terminal for this very blog where every line of code is covered with a corresponding test, so it’s just a black and green image with columns of “100”’s.&quot; decoding=&quot;async&quot; height=&quot;768&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/TVF_JBKYTg-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/TVF_JBKYTg-300.webp 300w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/TVF_JBKYTg-600.webp 600w, https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/TVF_JBKYTg-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Can you imagine how long it would take to get 100% &lt;a href=&quot;https://en.wikipedia.org/wiki/Code_coverage&quot;&gt;coverage&lt;/a&gt; across all files, &lt;a href=&quot;https://en.wikipedia.org/wiki/Code_coverage#Statement_coverage&quot;&gt;Statements&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/Code_coverage#Branch_coverage&quot;&gt;Branches&lt;/a&gt;, &lt;a href=&quot;https://web.dev/articles/ta-code-coverage#function_coverage&quot;&gt;Functions&lt;/a&gt;, and &lt;a href=&quot;https://web.dev/articles/ta-code-coverage#line_coverage&quot;&gt;Lines&lt;/a&gt;? That&#39;s a lot of work for one (obsessive) person.&lt;/p&gt;&lt;p&gt;Or, a perfect job for AI!&lt;/p&gt;&lt;h3 id=&quot;refactoring&quot;&gt;Refactoring&lt;/h3&gt;&lt;p&gt;Where AI really excelled in this project was its ability to understand even the smallest details of the codebase. That makes refactoring fast and almost effortless.&lt;/p&gt;&lt;p&gt;For example, I initially used TensorFlow.js, but quickly realised it was too heavy and overly complex for what I needed. So I gave a simple prompt:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Could you swap out TensorFlow.js and use Synaptic instead? The page weight is too high and it’s more complex than required.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The important part here is the added context. By explaining why I wanted the change, I was building up the AI’s understanding of the project.&lt;/p&gt;&lt;p&gt;From just that, it learns that I care about performance and keeping things lightweight, which leads to better, more aligned outputs as the project progresses.&lt;/p&gt;&lt;p&gt;If you combine AI’s ability to refactor quickly with version control like &lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt;, you are onto a winning combination.&lt;/p&gt;&lt;p&gt;You can easily &lt;a href=&quot;https://git-scm.com/docs/user-manual#Documentation/user-manual.txt-branch&quot;&gt;branch&lt;/a&gt; and &lt;a href=&quot;https://git-scm.com/docs/user-manual#def_tag&quot;&gt;tag&lt;/a&gt; different versions of your project to experiment with alternative approaches or technologies. If a refactor does not work out, just roll back to a previous commit, or simply scrap the branch / tag.&lt;/p&gt;&lt;p&gt;One thing to watch out for if you do this is be wary of your &lt;code&gt;node_modules&lt;/code&gt; folder. Projects tend to get a bit cranky if the dependencies are not installed correctly. And no, committing &lt;code&gt;node_modules&lt;/code&gt; to your repo is not the answer! 🤮&lt;/p&gt;&lt;p&gt;An added bonus when using this technique is if you have good &lt;a href=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/#writing-tests&quot;&gt;test coverage&lt;/a&gt;, you can swap out a technology and quickly run your tests to see what has broken. It makes validating a refactor fast and far less risky.&lt;/p&gt;&lt;p&gt;This is where AI and testing really come into their own. The AI can make sweeping changes, and your tests act as the safety net to catch anything that breaks.&lt;/p&gt;&lt;h3 id=&quot;proof-of-concepts&quot;&gt;Proof of concepts&lt;/h3&gt;&lt;p&gt;At the start of my career in digital marketing, there was a familiar pattern. Most of the budget went on UX and design. By the time it reached the build phase, you would give an estimate and hear, “We don’t have the budget for that.”&lt;/p&gt;&lt;p&gt;Classic waterfall. Siloed teams. And not much left for actually building &quot;the thing&quot;.&lt;/p&gt;&lt;p&gt;There was even a running joke that what we really needed was a big “build it” button, so the technology part cost next to nothing.&lt;/p&gt;&lt;p&gt;When it comes to proof of concepts, or spikes as some call them, you are essentially writing throwaway code. This is where that “build it” button starts to feel real.&lt;/p&gt;&lt;p&gt;With a bit of direction on the tech stack and constraints, you can have AI produce a working prototype in hours, not days. Especially when dealing with unfamiliar technologies.&lt;/p&gt;&lt;p&gt;What I’m really saying is that AI is great for testing ideas in code. Going back to the Agile principles I mentioned &lt;a href=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/#the-architecture-without-writing-code-myself&quot;&gt;earlier in the post&lt;/a&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Fail early to surface risk&lt;/li&gt;&lt;li&gt;Iterate quickly to explore options&lt;/li&gt;&lt;li&gt;Learn continuously to improve decisions&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These all map perfectly to AI-generated proof of concepts.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Word of warning&lt;/strong&gt;: be careful using AI-generated code in live / production without proper validation. Make sure it goes through the usual checks like Pull Requests (PRs), Continuous Integration, and Deployment (CI/CD) gates.&lt;/p&gt;&lt;p&gt;If something goes wrong, you can&#39;t blame the AI (well you could try, but I doubt it would go down well!)&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt;: There should &lt;strong&gt;always&lt;/strong&gt; be a human reviewing AI-generated code, especially when sensitive data is involved.&lt;/p&gt;&lt;h3 id=&quot;making-unfamiliar-things-make-sense&quot;&gt;Making unfamiliar things make sense&lt;/h3&gt;&lt;p&gt;I’ve touched on this a little in the previous sections, one of AI’s superpowers is being able to explain complex concepts to the user when required. This is especially true when it comes to software development and technologies a user has no experience of before. AI is basically an incredibly patient coach who is always willing to answer your questions, no mater how small or stupid they may be! When combined with a Code UI like &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code&lt;/a&gt;, you are really working with a powerful workflow. Being able to highlight individual rows and blocks of code and get AI to explain exactly what is happening is a dream come true for anyone learning to code in 2026! I know I wish I had it when I was a junior developer, far to many years ago!&lt;/p&gt;&lt;p&gt;Another thing I found really useful is learning through optimisation. You might write a piece of code you understand, then ask the AI:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Are there any edge cases I have missed?&lt;/li&gt;&lt;li&gt;How can I make it more maintainable?&lt;/li&gt;&lt;li&gt;Does this code follow best practices for the framework?&lt;/li&gt;&lt;li&gt;How can I make it more performant?&lt;/li&gt;&lt;li&gt;Will this code perform well if the website is scaled up?&lt;/li&gt;&lt;li&gt;How can I make it easier to read for other developers?&lt;/li&gt;&lt;li&gt;etc…&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These kinds of questions help reinforce your understanding, especially around the less obvious parts of a language.&lt;/p&gt;&lt;h3 id=&quot;building-momentum&quot;&gt;Building momentum&lt;/h3&gt;&lt;p&gt;Because I’m so old, I remember when &lt;a href=&quot;https://stackoverflow.com/&quot;&gt;Stack Overflow&lt;/a&gt; first appeared back in late 2008. It was a game changer for web development and beyond. A place to ask questions and get a range of answers, some useful, some less so.&lt;/p&gt;&lt;p&gt;I’ve never been one to answer loads of questions, but I do try to go back and share solutions when I find them.&lt;/p&gt;&lt;p&gt;The downside is it breaks your flow. One minute you are productive, the next you are stuck searching for answers, sometimes for hours. And if your problem is niche and not already answered, you could be left waiting days for a response.&lt;/p&gt;&lt;p&gt;With AI built into your code editor, you have all that knowledge and reasoning right at your fingertips. No need to leave the comfort of your IDE or go searching the web for that one insight that leads to a “Eureka” moment.&lt;/p&gt;&lt;p&gt;The result of using AI in this way is simple. Higher productivity, less frustration, and faster learning.&lt;/p&gt;&lt;p&gt;A win all round.&lt;/p&gt;&lt;h2 id=&quot;where-ai-fell-short-and-needed-steering&quot;&gt;Where AI Fell Short (and Needed Steering)&lt;/h2&gt;&lt;h3 id=&quot;the-definition-of-insanity&quot;&gt;The definition of insanity&lt;/h3&gt;&lt;p&gt;To quote Albert Einstein:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Insanity is doing the same thing over and over again and expecting different results.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;At times, this is precisely what using AI felt like. It would get stuck on a particular solution and keep coming back to it, no matter how I rephrased the prompt or tried to steer it in a different direction. It was incredibly frustrating.&lt;/p&gt;&lt;p&gt;The only reliable fix was to start a new chat, which meant losing all the project context, or switch to a different model in Cursor. I usually run in “auto&quot; mode to balance cost and capability, but switching models also meant losing that built up understanding within the AI prompt.&lt;/p&gt;&lt;p&gt;There may be a better way to handle this without resetting everything? Again, if you know, please &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;over-engineering-simple-problems&quot;&gt;Over-engineering simple problems&lt;/h3&gt;&lt;p&gt;If there’s one thing you don’t need to ask AI to do, it’s over engineer a solution. I saw this a lot with CSS. Even simple requirements would take longer than expected and result in far more code than needed.&lt;/p&gt;&lt;p&gt;Take something basic like centring a &lt;code&gt;&amp;lt;div&gt;&lt;/code&gt; across all screen sizes.&lt;/p&gt;&lt;p&gt;This is a real example from the Benchmark panel in the top right of the FractalAI UI:&lt;/p&gt;&lt;h4 id=&quot;ai-output&quot;&gt;AI output&lt;/h4&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--center-x&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--center-y&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--translate-center&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;translate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;-50%&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; -50%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--container-display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--container-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--container-justify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; relative&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--container-display&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;align-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--container-align&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;justify-content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--container-justify&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vh&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.container::before&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;inset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;radial-gradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;circle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0.05&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transparent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;pointer-events&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.centered&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--center-y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--center-x&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--translate-center&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;place-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;1rem + 2px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clamp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;200px&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 50%&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 400px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;aspect-ratio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1 / 1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;backdrop-filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;blur&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;8px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;box-shadow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    0 2px 4px &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0.1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    0 8px 16px &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;0.15&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@supports&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100dvh&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100dvh&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 768px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.centered&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;translate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;-50% + 2px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;-50% + 2px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Why use one way to centre a &lt;code&gt;&amp;lt;div&gt;&lt;/code&gt; when you can use three!&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Flexbox (&lt;code&gt;align-items&lt;/code&gt; + &lt;code&gt;justify-content&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Absolute positioning (&lt;code&gt;top&lt;/code&gt; / &lt;code&gt;left&lt;/code&gt; + &lt;code&gt;transform&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Grid (&lt;code&gt;place-items&lt;/code&gt;)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Additionally, it threw in CSS variables that were never reused and &lt;code&gt;@supports&lt;/code&gt; queries where they were not needed.&lt;/p&gt;&lt;p&gt;I’d expect a developer with a good grasp of modern CSS to write something more like this:&lt;/p&gt;&lt;h4 id=&quot;flexbox-modern&quot;&gt;Flexbox (modern)&lt;/h4&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;justify-content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;align-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vh&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;grid-modern&quot;&gt;Grid (modern)&lt;/h4&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;place-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vh&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Look how much more maintainable and easier to debug the simpler approach is.&lt;/p&gt;&lt;p&gt;This is definitely something to watch for when reviewing or generating AI code.&lt;/p&gt;&lt;h3 id=&quot;confidently-wrong-outputs&quot;&gt;Confidently wrong outputs&lt;/h3&gt;&lt;p&gt;The tricky thing with AI is how convincing it sounds. Even when it&#39;s wrong.&lt;/p&gt;&lt;p&gt;I did not see this much in this project, but I ran into it when I worked for Government Digital Service (GDS). I asked Google Gemini for an overview of the changes from WCAG 2.1 to WCAG 2.2. The response sounded accurate, at least from my perspective as someone who understands the basics of accessibility without being an expert.&lt;/p&gt;&lt;p&gt;But when I shared it with our dedicated accessibility team, it did not hold up. It was full of inaccuracies and false claims.&lt;/p&gt;&lt;p&gt;It is a good reminder not to take AI output at face value, especially on topics you are not deeply familiar with.&lt;/p&gt;&lt;h3 id=&quot;inconsistent-outputs-between-prompts&quot;&gt;Inconsistent outputs between prompts&lt;/h3&gt;&lt;p&gt;It’s incredible how much difference a single word in a prompt can make to the output. In this example, I asked the AI to write a test for a hypothetical login function in another project I was working on.&lt;/p&gt;&lt;h4 id=&quot;prompt-1&quot;&gt;Prompt 1&lt;/h4&gt;&lt;blockquote&gt;&lt;p&gt;“Write a test for a login function”&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;AI Output:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;login works&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;user&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pass&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;prompt-2&quot;&gt;Prompt 2&lt;/h4&gt;&lt;blockquote&gt;&lt;p&gt;“Write a comprehensive test for a login function”&lt;/p&gt;&lt;/blockquote&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;login&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;should succeed with correct credentials&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;user&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pass&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;should fail with incorrect password&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;user&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;wrong&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;should fail with empty input&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Just by adding “comprehensive”, the output is vastly different.&lt;/p&gt;&lt;p&gt;This might seem like a negative, and it is something to be aware of, but it links back to the &lt;a href=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/#crafted-prompts&quot;&gt;Crafted Prompts&lt;/a&gt; section earlier. Prompt 1 lacks detail, and even Prompt 2 is still pretty vague.&lt;/p&gt;&lt;p&gt;Neither is a great example of how to get the best from AI. It just reinforces the need to be clear and specific in what you are asking for.&lt;/p&gt;&lt;p&gt;One extra word. An entirely different level of quality.&lt;/p&gt;&lt;h3 id=&quot;performance-blind-spots&quot;&gt;Performance blind spots&lt;/h3&gt;&lt;p&gt;For every project, I work on, web performance is a priority. I see it as the foundation of accessibility, regardless of a user’s device, connection, or location.&lt;/p&gt;&lt;p&gt;During this project, I noticed that unless you explicitly ask for it, performance is not a priority in AI-generated code. In some cases, fractals would not render at all.&lt;/p&gt;&lt;p&gt;As an example, I asked Cursor to find unoptimised code in FractalAI. It highlighted this:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;For non line based fractals, ProgressiveRenderer increases iterations on each requestAnimationFrame tick and calls fractalModule.render again to recreate the draw command&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This is particularly problematic for the Mandelbrot, and other fractals that are &lt;a href=&quot;https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html&quot;&gt;shader&lt;/a&gt; based. Using &lt;a href=&quot;https://regl-project.github.io/&quot;&gt;regl&lt;/a&gt;, which already calls render() on every &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame&quot;&gt;requestAnimationFrame&lt;/a&gt;, this approach creates a new draw command on every frame. That is extremely inefficient for both CPU and memory.&lt;/p&gt;&lt;p&gt;After prompting, the issue was resolved. Thanks to my visual render tests using &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt;, I could confirm that none of the fractals broke during the optimisation. Another win for visual testing!&lt;/p&gt;&lt;p&gt;This is also something I can automate. As mentioned earlier, Cursor’s &lt;a href=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/#rules&quot;&gt;Rules&lt;/a&gt; feature can enforce performance considerations. Writing a rule for this is now high-up on my to-do list.&lt;/p&gt;&lt;h3 id=&quot;forgetting-earlier-constraints&quot;&gt;Forgetting earlier constraints&lt;/h3&gt;&lt;p&gt;This is a surprisingly human like trait of AI. As prompt &quot;conversations&quot; go on, it starts to forget earlier constraints. You often have to repeat yourself or restate key requirements it has quietly ignored.&lt;/p&gt;&lt;p&gt;Over time, parts of the project context seem to drift, much like people forgetting details as time passes.&lt;/p&gt;&lt;p&gt;Going back to the sailing analogy in &lt;a href=&quot;https://nooshu.com/blog/2026/04/21/fractalai-generating-infinity-in-the-browser/#crafted-prompts&quot;&gt;Crafted Prompts&lt;/a&gt;, it is as if the AI loses sight of the waypoint “lighthouses” you set earlier.&lt;/p&gt;&lt;h3 id=&quot;elementary-mistakes&quot;&gt;Elementary mistakes&lt;/h3&gt;&lt;p&gt;This happened more often than I expected. For such an advanced tool, the mistakes were surprisingly basic.&lt;/p&gt;&lt;p&gt;I saw this when asking the AI to update outdated &lt;code&gt;npm&lt;/code&gt; packages. A simple prompt like:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Update all npm packages in this project, and run the coverage tests to ensure nothing has broken.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;In Cursor, you can see what it is “thinking”, and it frequently searched for package versions from 2023 or 2024. It was not using the latest information.&lt;/p&gt;&lt;p&gt;It was only when I explicitly told it the current year was 2026 did it start checking for up-to-date versions!&lt;/p&gt;&lt;p&gt;This is a good reminder to be explicit when you need current information, especially for dependencies, libraries, and documentation.&lt;/p&gt;&lt;h2 id=&quot;performance-challenges-in-the-browser&quot;&gt;Performance Challenges in the Browser&lt;/h2&gt;&lt;p&gt;When I first planned this side project, I was quite naïve. My initial idea was to build a 3D fractal renderer in the browser using &lt;a href=&quot;https://threejs.org/&quot;&gt;Three.js&lt;/a&gt;. It didn’t take long to realise this wasn’t going to be viable. Even on an M1 MacBook, the frame rate was so low it was effectively unusable.&lt;/p&gt;&lt;p&gt;This is where AI proved its value again. By quickly prototyping the idea, it became obvious in the first iteration that 3D rendering wasn’t going to work. That allowed me to pivot early to a 2D approach using an entirely different WebGL library.&lt;/p&gt;&lt;p&gt;Without that early validation, I could easily have spent far longer trying to optimise something that fundamentally isn’t practical right now. There simply isn’t enough available CPU and GPU power in a typical machine to support real-time 3D fractal rendering in the browser at a usable level.&lt;/p&gt;&lt;p&gt;The key thing to understand with fractals is the sheer volume of maths involved in rendering every single pixel. Each pixel can require hundreds, sometimes thousands, of iterations before you get a final value.&lt;/p&gt;&lt;p&gt;So, even at a relatively modest resolution like a 1810px × 1131px image, that is over 2 million pixels to compute for that single image. Multiply that by hundreds or thousands of calculations per pixel, and you quickly see the scale of the problem.&lt;/p&gt;&lt;p&gt;Without highly optimised code, the browser simply does not stand a chance of keeping up. It becomes less about the idea itself and more about the limits of the hardware running it.&lt;/p&gt;&lt;p&gt;Looking ahead, it is hard not to think about how this changes with more advanced GPU capabilities. If compute continues to accelerate, especially with emerging technologies, calculations at this scale could eventually take milliseconds rather than seconds.&lt;/p&gt;&lt;p&gt;As you zoom deeper into a fractal, the cost per pixel increases. To preserve detail and avoid visual artefacts, the renderer needs to run more iterations for each pixel. Beyond a certain point, the amount of computation required makes real-time interaction impractically slow.&lt;/p&gt;&lt;p&gt;There is also a more fundamental limitation around numerical precision. In the browser, JavaScript numbers use &lt;a href=&quot;https://en.wikipedia.org/wiki/Double-precision_floating-point_format&quot;&gt;IEEE-754 64-bit double precision&lt;/a&gt;, which eventually cannot represent the minuscule coordinate differences needed at very high magnifications.&lt;/p&gt;&lt;p&gt;You can push beyond this using higher precision techniques, such as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt&quot;&gt;BigInt-based arithmetic&lt;/a&gt; or &lt;a href=&quot;https://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format&quot;&gt;“double-double” approaches&lt;/a&gt;, but that additional precision comes at a significant cost. Each pixel becomes even more expensive to compute, which further impacts performance.&lt;/p&gt;&lt;p&gt;What this all really comes down to is simple. Rendering fractals efficiently in the browser is hard.&lt;/p&gt;&lt;p&gt;That being said, it &lt;em&gt;is&lt;/em&gt; one of the quickest ways to max out your CPU and GPU, turn your computer into a heater, and simultaneously watch your electricity bill shoot up!&lt;/p&gt;&lt;h2 id=&quot;what-building-this-changed-about-my-view-of-ai&quot;&gt;What Building This Changed About My View of AI&lt;/h2&gt;&lt;p&gt;When I started this project, I was sceptical about how useful AI could be for coding. I had used tools like &lt;a href=&quot;https://chatgpt.com/&quot;&gt;ChatGPT&lt;/a&gt; and &lt;a href=&quot;https://gemini.google.com&quot;&gt;Google Gemini&lt;/a&gt; for research, but never properly explored the coding side.&lt;/p&gt;&lt;p&gt;The term “Vibe coding” didn’t help either. If anything, it put me off. After reading &lt;a href=&quot;https://blog.val.town/vibe-code&quot;&gt;Vibe code is legacy code&lt;/a&gt;, the last thing I wanted was to introduce more legacy code into a project. I also find the term itself a bit meaningless, especially after it was &lt;a href=&quot;https://www.bbc.co.uk/news/articles/cpd2y053nleo&quot;&gt;named word of the year in 2025&lt;/a&gt;. It felt like just another buzzword that would disappear as quickly as it arrived.&lt;/p&gt;&lt;p&gt;We have seen this pattern before. Technologies get hyped as “the future” and then quietly fade away. Anyone remember &lt;a href=&quot;https://en.wikipedia.org/wiki/Google_Glass&quot;&gt;Google Glass&lt;/a&gt; from 2013??&lt;/p&gt;&lt;p&gt;Looking at the defintion of &quot;vibe coding&quot;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Vibe coding is an intuitive, fast paced way of writing code where you prioritise momentum and experimentation over upfront planning, often with the help of tools&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I’m pleased to say this project has thoroughly changed my perspective on AI in software development. I still dislike the term “vibe coding”, but the practice behind it is undoubtedly what I found most useful and genuinely exciting while building FractalAI.&lt;/p&gt;&lt;p&gt;As mentioned earlier, if you put strong guard rails in place such as mandatory human reviews for all production PRs, along with solid performance and security practices, AI becomes a genuinely valuable tool for learning and rapid prototyping.&lt;/p&gt;&lt;p&gt;That said, I openly admit this is an idealistic view. In reality, there will always be individuals, companies, and bad actors willing to use AI in ways that harm the web for their own gain.&lt;/p&gt;&lt;p&gt;When it comes to new and exciting technology on the web, it often follows the same familiar pattern. A few people find ways to exploit it for their own benefit, usually at the expense of everyone else.&lt;/p&gt;&lt;p&gt;As &lt;a href=&quot;https://en.wikipedia.org/wiki/Paula_Poundstone&quot;&gt;Paula Poundstone&lt;/a&gt; put it best:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;This is why we can’t have nice things.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2 id=&quot;the-future-of-ai-generated-software&quot;&gt;The Future of AI-Generated Software&lt;/h2&gt;&lt;p&gt;If there’s one thing I know about AI-generated software, it’s that it’s not going anywhere. The AI sector is projected to reach &lt;a href=&quot;https://www.grandviewresearch.com/industry-analysis/artificial-intelligence-ai-market&quot;&gt;$539 billion in 2026&lt;/a&gt;, up from $391 billion in 2025. That’s a 38% increase in a single year. Longer term, it’s expected to push into the multi-trillion dollar range.&lt;/p&gt;&lt;p&gt;Spending alone tells the story. Global AI investment hit around &lt;a href=&quot;https://www.gartner.com/en/newsroom/press-releases/2025-09-17-gartner-says-worldwide-ai-spending-will-total-1-point-5-trillion-in-2025&quot;&gt;$1.5 trillion in 2025&lt;/a&gt;, and it’s still accelerating. This isn’t hype, it’s momentum.&lt;/p&gt;&lt;p&gt;At the same time, the risks are evolving just as quickly. A new Claude model, Mythos, from &lt;a href=&quot;https://www.anthropic.com/&quot;&gt;Anthropic&lt;/a&gt; has &lt;a href=&quot;https://www.bbc.co.uk/news/articles/c2ev24yx4rmo&quot;&gt;reportedly identified a significant number of vulnerabilities across major operating systems&lt;/a&gt;. Some experts believe it shows an unprecedented ability to detect and potentially exploit security weaknesses. Whether that is true or is just scaremongering remains to be seen, but it’s telling that major financial institutions have already been given early access to the model ahead of any public release.&lt;/p&gt;&lt;p&gt;As I’ve said, AI is here to stay. The genie is well and truly out of the bottle.&lt;/p&gt;&lt;p&gt;My plan is simple. Learn as much as possible over the coming months and years. This is happening whether we like it or not, so it makes far more sense to understand its potential and how it’s being used than to ignore it.&lt;/p&gt;&lt;p&gt;If you don’t, you can be sure others in your respective field will be. As this goes far beyond software development. AI’s reach is expanding fast, so buckle up.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;This project was an experiment in what happens when you step back from writing code and let AI take the lead. &lt;a href=&quot;https://github.com/Nooshu/FractalAI&quot;&gt;FractalAI&lt;/a&gt; is a fully AI-generated fractal renderer running in the browser, built to explore both the potential and the limits of modern AI-assisted development.&lt;/p&gt;&lt;p&gt;Along the way, it became clear that AI is far more than just a code generator. When used properly, it acts as a design partner, a rapid prototyping tool, and a way to accelerate learning. But it&#39;s &lt;strong&gt;not perfect&lt;/strong&gt;. It needs direction, context, and clear constraints to produce reliable results.&lt;/p&gt;&lt;p&gt;The browser turned out to be a surprisingly powerful platform for this kind of work, capable of rendering complex, infinite visuals while even supporting lightweight machine learning. Combined with AI, it creates a feedback loop where ideas can be tested, refined, and understood far faster than traditional approaches.&lt;/p&gt;&lt;p&gt;The biggest takeaway is simple. AI is not replacing developers, it is changing how we work. If you use it well, it becomes a tool for thinking, learning, and making better decisions, not just writing code faster.&lt;/p&gt;&lt;p&gt;Well, there we go, yet another blog post where I’ve somehow written far more than intended! If you’ve made it this far, I’m genuinely impressed. Please accept this virtual gold star as a reward: ⭐&lt;/p&gt;&lt;p&gt;As always, feedback and comments are very welcome. You can &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;contact me here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;fractalai-project&quot;&gt;FractalAI project&lt;/h2&gt;&lt;p&gt;Below, you can find the links to the repository and the live site:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Nooshu/FractalAI&quot;&gt;FractalAI on GitHub&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://fractals.world/&quot;&gt;Live site&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I’m completely open and actively encourage &lt;a href=&quot;https://github.com/Nooshu/FractalAI/pulls&quot;&gt;raising PR&#39;s&lt;/a&gt; and sending me &lt;a href=&quot;https://github.com/Nooshu/FractalAI/issues&quot;&gt;feedback / issues&lt;/a&gt; about the project (with, or without AI!).&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;21/04/26: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Using Cloudflare Workers and reCAPTCHA v3 for a Static Site Contact Form</title>
    <link href="https://nooshu.com/blog/2026/03/09/using-cloudflare-workers-and-recaptcha-v3-for-a-static-site-contact-form/" />
    <updated>2026-03-09T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2026/03/09/using-cloudflare-workers-and-recaptcha-v3-for-a-static-site-contact-form/</id>
    <content type="html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;&lt;p&gt;I recently wrote a blog post that involved using a &lt;a href=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/&quot;&gt;Cloudflare Worker to serve Brotli 11&lt;/a&gt; compressed HTML rather than the standard uncompressed HTML file. Off the back of this post, I’ve decided to write one about my Cloudflare Worker setup that I use for my &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;Contact Page&lt;/a&gt;. In order to stop the spam, I also integrated &lt;a href=&quot;https://cloud.google.com/security/products/recaptcha&quot;&gt;reCAPTCHA v3&lt;/a&gt; into the form functionality. The form is pretty simple, it asks a visitor for their full name, email address and the message they with to send to me. The Worker then pulls together this information and sends an email to a custom email address that points to my personal email address. This allows me to easily filter messages that come from the site, which I then have the option to reply to if I so wish.&lt;/p&gt;&lt;p&gt;It’s worth noting that this post was heavily inspired by &lt;a href=&quot;https://bsky.app/profile/sia.codes&quot;&gt;Sia Karamalegos&lt;/a&gt;’s post from 2024 about &lt;a href=&quot;https://sia.codes/posts/migrating-netlify-to-cloudflare/&quot;&gt;migrating from Netlify to Cloudflare&lt;/a&gt;. I had the absolute please of meeting Sia in Amsterdam when I spoke at &lt;a href=&quot;https://youtu.be/VdBxrZB9V_c?si=dQm4U77NzD9NKOH1&quot;&gt;Performance.now() 2023&lt;/a&gt;, she was MC for my talk and I really appreciated her support when I was (quite frankly) freaking out with nerves before the talk!&lt;/p&gt;&lt;p&gt;So why use a serverless endpoint for the contact form? Well, as I’ve mentioned many times before, this blog is a static site built using &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;11ty&lt;/a&gt; and hosted on the Pro plan of &lt;a href=&quot;https://pages.cloudflare.com/&quot;&gt;Cloudflare Pages&lt;/a&gt;. There simply is no backend to point the POST method too, which is required to send the email.&lt;/p&gt;&lt;h2 id=&quot;previous-solutions-i-ve-used&quot;&gt;Previous solutions I’ve used&lt;/h2&gt;&lt;p&gt;This isn’t the first time I’ve changed the way the contact form works on this blog. I’ve counted 5 alternatives that I have used over the years! Isn’t Git great for tracking history! I certainly wouldn’t be able to remember them all with my terrible memory! So, if you aren’t on Cloudflare, or you would rather not use a Worker for this, then there’s a list of perfectly viable alternatives below:&lt;/p&gt;&lt;h3 id=&quot;netlify-forms&quot;&gt;Netlify Forms&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://docs.netlify.com/manage/forms/setup/&quot;&gt;Netlify Forms&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Free+ (depending on the Netlify tier you choose)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Netlify Forms is a built-in form handling service for sites hosted on Netlify. It allows you to collect and manage form submissions from static websites without building or maintaining a backend. Submissions can be viewed in the Netlify dashboard or forwarded via email or webhook. I still think it’s a real shame that Cloudflare doesn&#39;t offer a form setup that is as easy to integrate and use as Netlify Forms, although, if it did, I wouldn’t need to be writing this blog post!&lt;/p&gt;&lt;h3 id=&quot;cloudflare-pages-functions-mailchannels&quot;&gt;Cloudflare Pages Functions + MailChannels&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://developers.cloudflare.com/pages/functions/&quot;&gt;https://developers.cloudflare.com/pages/functions/&lt;/a&gt; &amp; &lt;a href=&quot;https://www.mailchannels.com/&quot;&gt;https://www.mailchannels.com/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Free plan available ($10+ depending on volume-based pricing)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;When I initially migrated from Netlify to Cloudflare for hosting I used this setup, that was until MailChannels sunset its free email sending service for Cloudflare Workers users (me), at that point I started looking for other solutions. As you will see below.&lt;/p&gt;&lt;h3 id=&quot;formspree-io&quot;&gt;Formspree.io&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://formspree.io/&quot;&gt;https://formspree.io/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Free tier available ($15+ a month for paid plans)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Formspree is a hosted form service that lets you collect form submissions without building your own backend server. You simply point your form at Formspree, and it handles sending submissions to your email address or other API endpoints, it comes with built-in spam protection and integrations for various workflows.&lt;/p&gt;&lt;h3 id=&quot;formspark-io-previously-submit-form&quot;&gt;Formspark.io (previously Submit Form)&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://formspark.io/&quot;&gt;https://formspark.io/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Free tier available ($9+ a month for paid plans)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Formspark is a basic hosted form backend that lets you accept and manage form submissions without the need for your own backend server. You simply submit forms to Formspark, and it stores (or forwards) the data, with options for email notifications, webhooks, and spam filtering.&lt;/p&gt;&lt;h3 id=&quot;botpoison&quot;&gt;Botpoison&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://botpoison.com/&quot;&gt;https://botpoison.com/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Free plan available ($4+ per month depending on bot verifications required)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Botpoison is an invisible anti-spam and bot prevention service for web forms that blocks automated submissions without requiring CAPTCHAs or extra steps from users. It works by analysing form interactions and applying proof-of-work challenges and reputation checks to distinguish human traffic from bots. I started using it when I was still receiving contact form spam from my Formspark.io setup.&lt;/p&gt;&lt;h3 id=&quot;resend&quot;&gt;Resend&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://resend.com/&quot;&gt;https://resend.com/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Free plan available ($20+ for paid plans)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Resend is a developer-focused email delivery service, providing a simple API and Simple Mail Transfer Protocol (SMTP) interface for sending transactional and broadcast emails from your applications or website. It handles features like deliverability, bounce tracking, suppression lists, and email analytics, so a developer doesn&#39;t need to manage email infrastructure themselves and can be integrated with many platforms. Resend is currently part of my email workflow, as I will describe later in the post.&lt;/p&gt;&lt;h2 id=&quot;overview-of-the-setup&quot;&gt;Overview of the setup&lt;/h2&gt;&lt;p&gt;My current setup is: &lt;strong&gt;Cloudflare Pages Functions&lt;/strong&gt; (the Worker) + &lt;strong&gt;Google reCAPTCHA v3&lt;/strong&gt; + &lt;strong&gt;Resend&lt;/strong&gt; for email.&lt;/p&gt;&lt;p&gt;A high-level view of this workflow is:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;User populates the contact form with their details and message&lt;/li&gt;&lt;li&gt;reCAPTCHA v3 runs and adds a short-lived verification token&lt;/li&gt;&lt;li&gt;The form POSTs to &lt;code&gt;/api/contact&lt;/code&gt; (via the Pages Function, that sits in &lt;code&gt;functions/api/contact.js&lt;/code&gt; in the root of my repository&lt;/li&gt;&lt;li&gt;The Pages Function, then:&lt;ul&gt;&lt;li&gt;verifies the token with Google&lt;/li&gt;&lt;li&gt;validates the user inputs&lt;/li&gt;&lt;li&gt;sends the validated inputs via email using &lt;a href=&quot;https://resend.com/&quot;&gt;Resend&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Redirects the user to the “Thank You” page, confirming that the message has been sent.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;img alt=&quot;diagram of the workflow I described in the list above&quot; decoding=&quot;async&quot; height=&quot;444&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/03/09/using-cloudflare-workers-and-recaptcha-v3-for-a-static-site-contact-form/22H4uljaxL-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/03/09/using-cloudflare-workers-and-recaptcha-v3-for-a-static-site-contact-form/22H4uljaxL-300.webp 300w, https://nooshu.com/blog/2026/03/09/using-cloudflare-workers-and-recaptcha-v3-for-a-static-site-contact-form/22H4uljaxL-600.webp 600w, https://nooshu.com/blog/2026/03/09/using-cloudflare-workers-and-recaptcha-v3-for-a-static-site-contact-form/22H4uljaxL-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;google-recaptcha-v3&quot;&gt;Google reCAPTCHA v3&lt;/h2&gt;&lt;p&gt;I have to say, out of all the things written in this post, researching about how exactly Google reCAPTCHA v3 works, was probably the most fascinating! I really love the fact that it is all invisible to the user, and it is based on a complex (but logical) scoring mechanism. There’s no need for a really frustrating visual CAPTCHA’s that don’t prove you are human, “they prove that you are American!” (I can’t take credit for this quote, that I believe goes to &lt;a href=&quot;https://edent.tel/&quot;&gt;Terence Eden&lt;/a&gt;, or at least that&#39;s who I remember reading it from!)&lt;/p&gt;&lt;p&gt;So how exactly does reCAPTCHA v3 decide if you are a human? Well, it turns out it analyses a user&#39;s behavioural and contextual signals rather than setting a user a visual challenge, and based on those signals it creates a risk score. The biggest signal comes from how the user behaves on the page.&lt;/p&gt;&lt;h3 id=&quot;behavioural-analysis-of-a-users-session&quot;&gt;Behavioural analysis of a user&#39;s session&lt;/h3&gt;&lt;p&gt;reCAPTCHA observes a number of patterns and metrics from a user&#39;s interaction with the page, these include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Mouse movement patterns&lt;/li&gt;&lt;li&gt;Typing speed and rhythm&lt;/li&gt;&lt;li&gt;Scroll behaviour&lt;/li&gt;&lt;li&gt;Click timing and placement&lt;/li&gt;&lt;li&gt;Focus changes between fields or tabs&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Us humans are a little messy when using a computer. Our mouse movements curve and wander slightly, we pause without thinking, and our typing comes with the occasional hesitation or uneven rhythm.&lt;/p&gt;&lt;p&gt;Skynet, on the other hand, tends to be a little too perfect. Its mouse paths are often straight, timing is predictable, and its interactions can happen far faster than any real person would manage. Those overly neat patterns are often a strong indicator that automation is at work.&lt;/p&gt;&lt;h3 id=&quot;browser-and-device-fingerprinting&quot;&gt;Browser and device fingerprinting&lt;/h3&gt;&lt;p&gt;Now, when I think of Browser fingerprinting I often associate it with advertising and companies tracking users across the internet. In all fairness, this is Google we are talking about, so it&#39;s very likely that they actually do this! &lt;code&gt;:sad_face:&lt;/code&gt;. Browser and device fingerprinting is the way the reCAPTCHA script also examines the environment and device information of where the request is coming from.&lt;/p&gt;&lt;p&gt;Typical signals include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Browser version and capabilities&lt;/li&gt;&lt;li&gt;Installed fonts and plugins&lt;/li&gt;&lt;li&gt;Screen resolution and device properties&lt;/li&gt;&lt;li&gt;WebGL and Canvas fingerprint signals&lt;/li&gt;&lt;li&gt;Cookie availability&lt;/li&gt;&lt;li&gt;JavaScript execution behaviour&lt;/li&gt;&lt;li&gt;Timezone of client&lt;/li&gt;&lt;li&gt;Language settings&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You’d be amazed at how accurate these fingerprints can be. Each piece of information adds a little more entropy, meaning it becomes easier to distinguish one device from another. They are so accurate in fact I know many financial sectors or government departments use them for fraud detection.&lt;/p&gt;&lt;p&gt;You really don’t need to look very far to find libraries like &lt;a href=&quot;https://fingerprint.com/github/&quot;&gt;Fingerprint&lt;/a&gt; that do this all for you! Look at all the information it captured about you just by &lt;a href=&quot;https://demo.fingerprint.com/playground&quot;&gt;clicking this link&lt;/a&gt;! And you thought cookie tracking was bad!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: There are many other fingerprinting libraries available, I’m not specifically targeting &lt;a href=&quot;https://fingerprint.com/github/&quot;&gt;Fingerprint&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So while I disagree with browser and device fingerprinting from a privacy and tracking perspective, it does have its uses when it comes to proving that a “user” is a human and not a bot. In particular, bots often run in headless browsers or minimal environments, which look very different from real user devices.&lt;/p&gt;&lt;h3 id=&quot;network-and-reputation-signals&quot;&gt;Network and reputation signals&lt;/h3&gt;&lt;p&gt;Due to Google’s global network, it can also delve into global reputation data. It captures:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;IP reputation and history&lt;/li&gt;&lt;li&gt;Known bot networks or proxy usage&lt;/li&gt;&lt;li&gt;Data centre IP ranges&lt;/li&gt;&lt;li&gt;Previous abuse patterns linked to the IP or session&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So much traffic flows through Googles network globally, it can detect suspicious network traffic surprisingly easily.&lt;/p&gt;&lt;h3 id=&quot;action-context&quot;&gt;Action Context&lt;/h3&gt;&lt;p&gt;Google can also score the predicted actions a user is going to make. With reCAPTCHA v3, developers call the library with an action name such as:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;login
checkout
signup
comment
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From its countless trillions of observations over the years, Google already knows the typical login behaviour for a human (it takes approximately 3–10 seconds). Whereas, a bot can submit hundreds of logins per minute, which is a huge red flag for the use of automation.&lt;/p&gt;&lt;h3 id=&quot;scoring-using-machine-learning&quot;&gt;Scoring using Machine learning&lt;/h3&gt;&lt;p&gt;So what does Google do with all these various signals it captures?&lt;/p&gt;&lt;p&gt;It feeds them into a machine learning algorithm for scoring, which will decide if whatever is interacting is a human or a bot.&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Score&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;0.9 to 1.0&lt;/td&gt;&lt;td&gt;Very likely human&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0.5&lt;/td&gt;&lt;td&gt;Uncertain&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0.0 to 0.3&lt;/td&gt;&lt;td&gt;Very likely bot&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;At this point, it is then up to &lt;strong&gt;your&lt;/strong&gt; backend to decide what to do with the score it gets back. For example:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;0.8+  allow request
0.4–0.8 require MFA
&amp;lt;0.4 block or challenge
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In my case, the Cloudflare Worker is my simple serverless backend, and it contains the following code to decide if the form submit is from a human or a bot:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Verify score (reCAPTCHA v3 returns scores between 0.0 and 1.0)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Lower scores indicate bot-like behavior. 0.5 is a common threshold, but 0.3 allows more legitimate traffic&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
		&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CAPTCHA verification failed&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Score too low: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The code above is saying to reject any score that returns from Google that is under 0.3 (Very likely bot), You could of course easily change this threshold if you found it was being too aggressive with the filtering.&lt;/p&gt;&lt;h3 id=&quot;technical-setup&quot;&gt;Technical setup&lt;/h3&gt;&lt;p&gt;To use reCAPTCHA v3, you must first register your site in the &lt;a href=&quot;https://cloud.google.com/security/products/recaptcha&quot;&gt;Google reCAPTCHA admin console&lt;/a&gt; to obtain two keys.&lt;/p&gt;&lt;p&gt;The “site key” is used on the client-side and allows the browser to request a reCAPTCHA verification token for when a user performs an action. The “secret key” must remain server-side and is used to verify that token with Google.&lt;/p&gt;&lt;p&gt;This means storing the secret securely as an environment variable, for my setup this is done via the Cloudflare Pages build dashboard. The Cloudflare Worker then reads it from the environment variables during the build process.&lt;/p&gt;&lt;p&gt;Once all configured, you can also use the Google reCAPTCHA admin console to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;restrict the key to specific hostnames&lt;/li&gt;&lt;li&gt;configure score thresholds to control how strict the bot detection should be&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;the-contact-form-html-and-client-side&quot;&gt;The contact form (HTML and client-side)&lt;/h2&gt;&lt;p&gt;The HTML used for the form is all pretty standard, just remember to make it as accessible as possible! Semantic markup is the key to achieving this! If you are unsure how to do this, there’s an excellent post called &lt;a href=&quot;https://webaim.org/techniques/forms/&quot;&gt;Creating Accessible Forms on the WebAIM.org&lt;/a&gt; website. The HTML for my contact form looks like this:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot; data-gist=&quot;882a6cc0521479659df63e5f442df817&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
  Contact form: POSTs to the Cloudflare Pages Function at /api/contact.
  Uses novalidate so we can run custom client-side validation and show
  accessible error messages before and after submit.
--&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;fs-frm&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/api/contact&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;POST&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;aria-labelledby&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;page-title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;novalidate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
	  Error summary: shown when validation fails (client or server).
	  role=&quot;alert&quot; announces to screen readers when content appears.
	  tabindex=&quot;-1&quot; allows focus to be moved here after submit for accessibility.
	  hidden by default; JS removes the attribute to reveal and populate it.
	--&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-error-summary&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-error-summary&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;alert&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;tabindex&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;-1&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
	  Live region for status updates (e.g. &quot;Sending…&quot;, success message).
	  role=&quot;status&quot; is polite; screen readers announce changes without interrupting.
	--&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-status&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-status&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
	  Honeypot (spam trap): invisible to users, attractive to bots.
	  - Wrapped in a div with a neutral class (e.g. &quot;abc&quot;) so bots don’t skip it.
	  - aria-hidden=&quot;true&quot; keeps it out of the accessibility tree.
	  - tabindex=&quot;-1&quot; and autocomplete=&quot;off&quot; reduce chance a human touches it.
	  - Obscure name so bots don’t recognise it as a honeypot; server rejects if set.
	--&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;abc&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;aria-hidden&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;true&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;checkbox&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;_its-a-trap!&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;tabindex&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;-1&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;autocomplete&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;off&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Server/worker uses these hidden fields for redirect and email behaviour --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;_redirect&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/contact/thanks/&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;_append&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;false&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;_email.subject&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;New Message from Nooshu.com&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;fieldset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Legend is required for fieldset; visually-hidden keeps layout clean --&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;legend&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;visually-hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Contact form&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;legend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-group&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;required&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Full Name:&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;autocomplete&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name-error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-group&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;required&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Email:&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;autocomplete&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hello@example.com&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email-error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-group&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;required&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Message:&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;textarea&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;7&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;textarea&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;message-error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;form-error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;fieldset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
	  reCAPTCHA v3 token: client-side JS runs grecaptcha.execute() and sets
	  this value before submit. Server verifies the token with Google before
	  sending the email.
	--&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;g-recaptcha-response&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Send Message&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with &lt;a href=&quot;https://gist.github.com/Nooshu/882a6cc0521479659df63e5f442df817&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;fewer comments is available in this gist on GitHub&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The contact form I use above has the following features:&lt;/p&gt;&lt;h3 id=&quot;honeypot-and-recaptcha-to-block-spam&quot;&gt;Honeypot and reCAPTCHA to block spam&lt;/h3&gt;&lt;p&gt;I’ve already detailed how Google reCAPTCHA works, so I won’t cover it again here, but a &lt;a href=&quot;https://en.wikipedia.org/wiki/Honeypot_(computing)&quot;&gt;Honeypot&lt;/a&gt; is a simple first layer of protection. The basic theory behind a Honeypot is a simple form field hidden from real users but still present in the page markup. Bots that scan the source code often fill in every field they find, including this hidden one. If that field is completed, it is a strong signal that the submission came from a bot, not a human because it isn’t seen in the User Interface (UI), thus allowing the request to be rejected immediately.&lt;/p&gt;&lt;h3 id=&quot;client-and-server-validation-to-ensure-safe-input&quot;&gt;Client and server validation to ensure safe input&lt;/h3&gt;&lt;p&gt;Form validation is handled in two layers:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;On the client side, the form uses &lt;code&gt;novalidate&lt;/code&gt; with custom JavaScript to check required fields and validate the email format for the name, email, and message inputs.&lt;/li&gt;&lt;li&gt;On the server-side, the API performs its own validation by checking that required fields are present, enforcing max length limits for each field (name: 200 characters, email: 320 characters, message: 5000 characters), and confirming that a valid reCAPTCHA token is included with the submission.&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&quot;accessible-error-handling-and-focus-management&quot;&gt;Accessible error handling and focus management&lt;/h3&gt;&lt;p&gt;Accessibility is built into the form by clearly associating it with the page title using &lt;code&gt;aria-labelledby&lt;/code&gt;, providing an error summary and status regions with &lt;code&gt;role=“alert&quot;&lt;/code&gt; plus &lt;code&gt;role=“status&quot;&lt;/code&gt;, also marking individual fields with &lt;code&gt;aria-invalid&lt;/code&gt; and &lt;code&gt;aria-describedby&lt;/code&gt; so errors are properly announced.&lt;/p&gt;&lt;p&gt;Inline error messages use matching IDs for assistive technologies, required fields are clearly indicated with a &lt;code&gt;*&lt;/code&gt;, and a visually hidden legend describes the &lt;code&gt;&amp;lt;fieldset&gt;&lt;/code&gt;. When errors occur, the browser focus moves to the summary using &lt;code&gt;tabindex=&quot;-1”&lt;/code&gt; so it can be announced immediately by the screen reader.&lt;/p&gt;&lt;h3 id=&quot;ux-updates-to-keep-the-user-informed-during-submission&quot;&gt;UX updates to keep the user informed during submission&lt;/h3&gt;&lt;p&gt;The form includes the &lt;code&gt;autocomplete&lt;/code&gt; attribute for the name and email fields, allowing users browser the option to suggest information already stored in the browser and pre-fill it for them. Additionally, there&#39;s a simple placeholder for the email input, letting a user know what input format is expected.&lt;/p&gt;&lt;p&gt;When the form is submitted, the submit button is temporarily disabled and its label changes to “Sending…” while a status message informs the user that their message is being sent. After the submission is successful, the user is then redirected to the &lt;code&gt;/contact/thanks/&lt;/code&gt; page, as seen in the &lt;code&gt;_redirect&lt;/code&gt; input field, the API then returns a 303 redirect to complete the process.&lt;/p&gt;&lt;h3 id=&quot;secure-server-side-processing&quot;&gt;Secure server side processing&lt;/h3&gt;&lt;p&gt;The form submits a POST request to &lt;code&gt;/api/contact&lt;/code&gt;, handled by a Cloudflare function. Hidden fields such as &lt;code&gt;_redirect&lt;/code&gt;, &lt;code&gt;_append&lt;/code&gt;, and &lt;code&gt;_email.subject&lt;/code&gt; control what happens on submit.&lt;/p&gt;&lt;p&gt;On the server, the submission is sent as an email in both HTML and plain text formats before redirecting the user on success. The endpoint only accepts &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype&quot;&gt;form-encoded&lt;/a&gt; or &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST#multipart_form_submission&quot;&gt;multipart&lt;/a&gt; content types. It also verifies the reCAPTCHA token using the &lt;a href=&quot;https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-connecting-ip&quot;&gt;CF-Connecting-IP header&lt;/a&gt; when available, and validates required fields and length limits, it then &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Safely_inserting_external_content_into_a_page&quot;&gt;escapes HTML in the email body&lt;/a&gt; to ensure the content is safe.&lt;/p&gt;&lt;p&gt;I’d like to think this form is pretty accessible and simple to use, but if you disagree, please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;. How very &lt;a href=&quot;https://www.dictionary.com/browse/meta&quot;&gt;meta&lt;/a&gt;! It’s always fun to learn something new!&lt;/p&gt;&lt;h2 id=&quot;simplified-csp&quot;&gt;Simplified CSP&lt;/h2&gt;&lt;p&gt;The great thing about not having to use a third-party form service for sending contact form emails (besides the slight reduction in cost), is that it simplifies my &lt;code&gt;Content-Security-Policy&lt;/code&gt;. This is because I’m no longer having to establish a connection to an additional third-party. The Cloudflare Worker runs on &lt;strong&gt;my instance&lt;/strong&gt; of Cloudflare pages, so it is embedded in the build. And thankfully there’s no client-side interaction when using &lt;a href=&quot;https://resend.com/&quot;&gt;Resend&lt;/a&gt;, as this all happens via the Cloudflare Worker, no modifications to the CSP required! This may not seem like a big thing, but the less you can rely on a third parties the better! There are numerous examples from the history of the web, where the failure of a third party has had a massive impact on the web:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.fastly.com/blog/summary-of-june-8-outage&quot;&gt;Fastly outage on 8 June 2021&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.theguardian.com/technology/2016/oct/26/ddos-attack-dyn-mirai-botnet&quot;&gt;Dyn DNS outage (2016)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.theguardian.com/technology/2021/oct/05/facebook-outage-what-went-wrong-and-why-did-it-take-so-long-to-fix&quot;&gt;Facebook BGP outage (2021)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Category:2010s_internet_outages&quot;&gt;Wikipedia Category on Internet outages&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;An interesting point about the Wikipedia link above is you can see how prominent these outages have become over the decades, and also the fact that the placeholder category pages for the 2030s, 2040s, 2050s, and 2060s are already listed! Oh well, those outages are for my grandkids to worry about! 👴🏼&lt;/p&gt;&lt;h2 id=&quot;the-cloudflare-worker-pages-function&quot;&gt;The Cloudflare Worker (Pages Function)&lt;/h2&gt;&lt;p&gt;So let’s get onto the final piece of the puzzle: the Cloudflare Worker code. This file sits in the root of my blog under &lt;code&gt;functions/api/contact.js&lt;/code&gt;, this translates to the &lt;code&gt;action=&quot;/api/contact”&lt;/code&gt; in the &lt;a href=&quot;https://nooshu.com/blog/2026/03/09/using-cloudflare-workers-and-recaptcha-v3-for-a-static-site-contact-form/#the-contact-form-html-and-client-side&quot;&gt;form code above&lt;/a&gt;.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;0e1cb510f533e8ae9247f84bf9c724b3&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Import a small helper that escapes HTML characters.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// This prevents user-supplied content (like the message body)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// from breaking HTML or injecting markup when we render it in the email.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; escapeHtml &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../../_helpers/escape-html.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Cloudflare Pages Functions export HTTP handlers as named exports.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// `onRequestPost` will be invoked for POST requests to this function&#39;s route.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Signature:&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// - `request`: the incoming HTTP Request object&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// - `env`: environment bindings (secrets, KV, etc.), configured in Cloudflare&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function function-variable&quot;&gt;onRequestPost&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 1. Accept only standard HTML form submissions&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We support:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - application/x-www-form-urlencoded  (classic HTML &amp;lt;form method=&quot;post&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - multipart/form-data                (forms that include files)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Anything else (JSON, etc.) is rejected with 415 Unsupported Media Type.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; contentType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;contentType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;application/x-www-form-urlencoded&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt;
			&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;contentType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;multipart/form-data&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Unsupported content type&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;415&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Parse the incoming form body. Cloudflare&#39;s Request object&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// exposes a `.formData()` helper which returns a FormData instance.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; form &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;formData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 2. Honeypot anti-spam field&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// A &quot;honeypot&quot; is an extra hidden field that humans never fill in,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// but bots often do. If this field is present and non-empty,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// we treat it as spam and quietly redirect away.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;_its-a-trap!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Redirect back to the homepage. We build the URL from the current request&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// so it works correctly across environments (preview, production, etc.).&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hpUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hpUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;303&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 3. Extract and normalize form fields&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We pull out the core fields we care about:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - name&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - email&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - message&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - _redirect (optional; where to send the user after success)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Every value is coerced to string and trimmed to avoid&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// issues with null/undefined and accidental leading/trailing spaces.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; email &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; redirectUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;_redirect&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/contact/thanks/&quot;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Basic required-field validation. If any are empty,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// respond with a 400 Bad Request and a JSON error.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;email &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Missing required fields&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 4. Length limits (simple validation / abuse mitigation)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// These caps are deliberately generous but help avoid:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - unrealistic payloads&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - abuse where the form is used as a data pipe&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;320&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Invalid field lengths&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 5. CAPTCHA verification (reCAPTCHA v3 or hCaptcha-compatible)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// The form is expected to include a CAPTCHA token:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - reCAPTCHA v3: `g-recaptcha-response`&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - (optionally) hCaptcha-style: `h-captcha-response`&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// If no token is present, we immediately reject the submission.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; recaptchaToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
			form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;g-recaptcha-response&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;h-captcha-response&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;recaptchaToken&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reCAPTCHA token is required&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Select the secret key from Cloudflare environment variables.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// This must be configured in the project/environment:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - RECAPTCHA_SECRET_KEY or&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - RECAPTCHA_SECRET&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; secret &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RECAPTCHA_SECRET_KEY&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RECAPTCHA_SECRET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;secret&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// If the secret is missing, that&#39;s a server misconfiguration,&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// so we return a 500 Internal Server Error.&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
						&lt;span class=&quot;token string&quot;&gt;&quot;Server not configured for CAPTCHA (missing RECAPTCHA_SECRET_KEY/RECAPTCHA_SECRET)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// For better fraud detection, we also send the user&#39;s IP to reCAPTCHA.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Cloudflare exposes client IP via:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - CF-Connecting-IP header&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - request.cf.clientAddress&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; remoteIp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
			request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CF-Connecting-IP&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
			request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cf&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;clientAddress &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Build the request body for the reCAPTCHA verification endpoint.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// It expects application/x-www-form-urlencoded payload.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; verifyParams &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URLSearchParams&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			secret&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; recaptchaToken&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;remoteIp&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			verifyParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;remoteip&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; remoteIp&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// POST the verification request to Google&#39;s reCAPTCHA API.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; verifyResp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			&lt;span class=&quot;token string&quot;&gt;&quot;https://www.google.com/recaptcha/api/siteverify&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/x-www-form-urlencoded&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; verifyParams&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// If Google&#39;s endpoint itself is failing (network issue or 5xx),&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// treat this as a bad gateway (502) and surface a generic error.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;verifyResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Failed to verify reCAPTCHA&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Parse the JSON response from reCAPTCHA.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Example structure:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// {&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//   success: true/false,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//   score: 0.0-1.0,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//   hostname: &quot;example.com&quot;,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//   &quot;error-codes&quot;: [...]&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// }&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; verifyJson &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; verifyResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 5a. Hard failure: verification not successful at all&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We return a 400 with details about why it failed where possible.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errorCodes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; verifyJson&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;error-codes&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CAPTCHA verification failed&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
						errorCodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; errorCodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;, &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Unknown error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 5b. Soft failure: low reCAPTCHA v3 score&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// reCAPTCHA v3 uses scores instead of explicit &quot;I&#39;m not a robot&quot; prompts.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Lower values are more suspicious. Here we reject anything below 0.3.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// This threshold is a trade-off between blocking bots and not annoying users.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CAPTCHA verification failed&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Score too low: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 5c. Optional hostname verification&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// The reCAPTCHA response includes a `hostname` field.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We check that the hostname reCAPTCHA saw matches the hostname of our request.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// This helps prevent token reuse on other domains.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestHost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt;
			verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; requestHost &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt;
			&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;requestHost&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// We allow subdomains (e.g. www.example.com vs example.com),&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// but log any unexpected mismatch to server logs for later inspection.&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// This log will show up in Cloudflare function logs.&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;reCAPTCHA hostname mismatch: expected &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;requestHost&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, got &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 6. Prepare email content for Resend&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// At this point, the submission has passed:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - basic validation&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - anti-spam honeypot&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - CAPTCHA checks&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Now we build an email to send to the site owner via Resend.&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Fixed &quot;from&quot; address used with Resend.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// This should be a verified sender domain for your Resend account.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fromEmail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;contact@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Allow the subject to be overridden via a hidden form field `_email.subject`,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// otherwise fall back to a sensible default.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; subject &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
			form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;_email.subject&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;New Message from Nooshu.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Plain-text body, which is useful for mail clients that don&#39;t render HTML, note the use of JavaScript Template literals.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; textBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;New contact form submission on nooshu.com

Name: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
Email: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;email&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;

Message:
&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;message&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// HTML body with simple markup for better readability.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We escape all user-supplied fields to avoid injecting HTML or scripts.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; htmlBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;h2&gt;New contact form submission on nooshu.com&amp;lt;/h2&gt;&amp;lt;p&gt;&amp;lt;strong&gt;Name:&amp;lt;/strong&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;br/&gt;&amp;lt;strong&gt;Email:&amp;lt;/strong&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;/p&gt;&amp;lt;p&gt;&amp;lt;strong&gt;Message:&amp;lt;/strong&gt;&amp;lt;/p&gt;&amp;lt;pre style=&quot;white-space:pre-wrap&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;/pre&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Pull the Resend API key from Cloudflare environment variables.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// This must be configured as a secret in the Pages project.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; apiKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;apiKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Server not configured for email (missing RESEND_API_KEY)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 7. Call Resend&#39;s email API&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We issue a POST request to Resend&#39;s /emails endpoint with:&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - from: sender (must be a verified domain)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - to: recipient(s) (in this case, the site inbox)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - reply_to: the visitor&#39;s email address, so &quot;Reply&quot; in your mail client goes to them&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// - subject, text, html: the message content&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; resendResp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://api.resend.com/emails&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;apiKey&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Nooshu Contact &amp;lt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;fromEmail&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;email@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;reply_to&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				subject&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; textBody&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; htmlBody&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// If Resend returns a non-2xx status, we surface a 502 to the client&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// and include the raw error body for easier debugging.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;resendResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; text &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; resendResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Email send failed&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; text &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 8. Final redirect on success&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// If everything above succeeds, we redirect the user to a &quot;thank you&quot;&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// page (configurable via the `_redirect` field).&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We resolve `redirectUrl` relative to the current request URL&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// to avoid hard-coding the origin.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalRedirect &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;redirectUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// 303 See Other is the canonical way to redirect after a POST,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// telling the browser to perform a GET to the new URL.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalRedirect&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;303&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 9. Global error handler&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// If anything unexpected blows up (network issues, runtime errors, etc.),&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// we catch it here and return a generic 500 JSON response.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// The error is stringified so it&#39;s at least inspectable in logs / responses,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// but in a real-world scenario you might want to avoid leaking details&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// to the client and instead only log them server-side.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Server error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with &lt;a href=&quot;https://gist.github.com/Nooshu/0e1cb510f533e8ae9247f84bf9c724b3&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;fewer comments is available in this gist on GitHub&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;using-cloudflare-turnstile&quot;&gt;Using Cloudflare Turnstile&lt;/h2&gt;&lt;p&gt;After I posted the blog post on &lt;a href=&quot;https://bsky.app/profile/therealnooshu.bsky.social/post/3mgowaxsask2d&quot;&gt;Bluesky&lt;/a&gt;, &lt;a href=&quot;https://hachyderm.io/@TheRealNooshu/116203868348768808&quot;&gt;Mastodon&lt;/a&gt;, and &lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7437049173461438464/&quot;&gt;LinkedIn&lt;/a&gt;, I had a couple of replies back asking me why I hadn&#39;t used Cloudflare&#39;s CAPTCHA integration rather than Google&#39;s ReCAPTCHA v3. In all honesty I didn&#39;t realise Cloudflare had a competing product! So, thanks to &lt;a href=&quot;https://bsky.app/profile/twnsnd.com&quot;&gt;Ryan Townsend&lt;/a&gt; and &lt;a href=&quot;https://bsky.app/profile/pawelgrzybek.com&quot;&gt;Paweł Grzybek&lt;/a&gt; for letting me know!&lt;/p&gt;&lt;p&gt;I&#39;m not going to be rewriting the whole blog post, but what I will do is share the client-side JavaScript code (&lt;code&gt;turnstile.js&lt;/code&gt;), and the &lt;code&gt;contact.js&lt;/code&gt; file for the API that I am using to integrate with &lt;a href=&quot;https://www.cloudflare.com/en-gb/application-services/products/turnstile/&quot;&gt;Cloudflare Turnstile&lt;/a&gt;. Also, any additional changes I had to make.&lt;/p&gt;&lt;p&gt;First the Code:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;dd608f9df6e21ad5e2ec3a37e1e12264&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Lightweight, dependency-free integration for Cloudflare Turnstile.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// This script:&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// - Validates the contact form on the client (for UX only – server still re-validates).&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// - Requests an invisible Turnstile token just before submit.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// - Injects the token into a hidden field for the server to verify.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// - Tries hard not to block the user if Turnstile is slow to load.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Public Turnstile site key for this widget. This is *not* a secret.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// The matching secret key lives on the server and is used during&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// server-side verification in the Cloudflare Pages Function.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SITE_KEY&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0x4AAAAAACpYvR2v9J0i1tr_&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// The contact form we progressively enhance. If it does not exist&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// (for example, on other pages), bail out early.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; form &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fs-frm&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Hidden token field management&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Turnstile posts its token back to the server in a field that we choose.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// The backend expects this exact name when validating the submission.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tokenInputName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;cf-turnstile-response&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; tokenInput &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;input[name=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;tokenInputName&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;]&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;tokenInput&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// If the hidden field was not rendered server-side, create it here.&lt;/span&gt;
    tokenInput &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;input&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    tokenInput&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hidden&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    tokenInput&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tokenInputName&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInput&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Accessible error + status UI helpers&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errorSummary &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;form-error-summary&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; formStatus &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;form-status&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Show a high-level error message above the form and move focus there so&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// screen-reader users (and keyboard users) are notified.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;showFormError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errorSummary&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      errorSummary&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errorSummary&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hidden &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errorSummary&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Hide and clear the error summary.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clearFormError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errorSummary&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      errorSummary&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errorSummary&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hidden &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Attach error state to a single field (ARIA attributes + inline message).&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;showFieldError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fieldId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; field &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fieldId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errorEl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fieldId &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;aria-invalid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;aria-describedby&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fieldId &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errorEl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      errorEl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errorEl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hidden &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Clear error state from a single field.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clearFieldError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fieldId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; field &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fieldId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errorEl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fieldId &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;removeAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;aria-invalid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;removeAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;aria-describedby&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errorEl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      errorEl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errorEl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hidden &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Clear all per-field and summary errors.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clearAllErrors&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;clearFormError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;clearFieldError&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Set a small, live-updating status message (polite ARIA live region).&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formStatus&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      formStatus&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Client-side validation&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// This mirrors the server’s validation rules, but is *only* for UX.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// The server still validates everything again.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;validateForm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;clearAllErrors&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errors &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nameField &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; emailField &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#email&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; messageField &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;nameField&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;showFieldError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Please enter your full name.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;nameField&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;emailField&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;showFieldError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Please enter your email address.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;emailField&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;^[^&#92;s@]+@[^&#92;s@]+&#92;.[^&#92;s@]+$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;emailField&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;showFieldError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Please enter a valid email address.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;emailField&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;messageField&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;showFieldError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Please enter a message.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messageField&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; summary &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
        errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;There is 1 error in the form.&quot;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;There are &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot; errors in the form.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;showFormError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;summary&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      errors&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Turnstile widget lifecycle&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// `widgetId` is the handle returned by turnstile.render.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// `pendingSubmit` tracks whether we should submit once a token arrives.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// `submitButton` lets us disable / re-enable the call-to-action button.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; widgetId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; pendingSubmit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; submitButton &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Called by Turnstile when a token is successfully generated.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;onTokenReceived&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    tokenInput&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; token&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// If the user was waiting for the challenge to finish, we can now submit.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pendingSubmit&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      pendingSubmit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;submit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Called when Turnstile hits an error or we decide the widget is unavailable.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;onTokenError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;errorCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    tokenInput&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    pendingSubmit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Turnstile error:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; errorCode&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;showFormError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;Verification failed. Please try again. If you use an ad blocker, you may need to allow Cloudflare on this site.&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;submitButton&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      submitButton&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;disabled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      submitButton&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Send Message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// If we have a widget instance, reset it so the user can try again.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;widgetId &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; turnstile &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      turnstile&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;widgetId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Render an invisible Turnstile widget into the placeholder div.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// The widget itself is not visible – it will run when we call execute().&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;renderTurnstile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; container &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;turnstile-container&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Only render once, and only on the contact page where the container exists.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;container &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; widgetId &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; turnstile &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      widgetId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; turnstile&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;container&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property literal-property&quot;&gt;sitekey&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SITE_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// Invisible mode: no explicit checkbox; Cloudflare decides when&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// to prompt for a challenge, if at all.&lt;/span&gt;
        &lt;span class=&quot;token property literal-property&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;invisible&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// We want full control, so we manually call turnstile.execute(widgetId).&lt;/span&gt;
        &lt;span class=&quot;token property literal-property&quot;&gt;execution&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;execute&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property literal-property&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; onTokenReceived&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property string-property&quot;&gt;&quot;error-callback&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; onTokenError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property string-property&quot;&gt;&quot;expired-callback&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// Expired tokens are treated as missing; the server will reject them.&lt;/span&gt;
          tokenInput&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Wait until the Turnstile script has loaded and the global `turnstile`&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// object is available. We poll for a short period so that a slow network&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// does not permanently block the user.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;waitForTurnstile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; turnstile &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; attempts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; checkInterval &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        attempts&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; turnstile &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;checkInterval&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;attempts &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// Give up after ~10s. At this point we will try to submit&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// without a token and let the server return a clear error.&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;checkInterval&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// As soon as the page is ready and Turnstile is (hopefully) loaded,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// render the widget so it is ready by the time the user hits submit.&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;waitForTurnstile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;renderTurnstile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Submit flow&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------------&lt;/span&gt;
  form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;submit&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// We always take over submission so we can validate + get a token first.&lt;/span&gt;
    e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;validateForm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    submitButton &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;button[type=&quot;submit&quot;]&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;submitButton&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      submitButton&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;disabled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      submitButton&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Sending…&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Verifying, please wait.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Make sure the Turnstile library has had a chance to load.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;waitForTurnstile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// If we still do not have a widget, try to render one and give the&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// browser a short moment to paint it.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;widgetId &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;renderTurnstile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// If for some reason we already have a valid token (for example,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Turnstile auto-ran earlier), we can just submit the form.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInput&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;submit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Normal path: ask Turnstile to run in invisible mode. When it finishes,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// it will call onTokenReceived, which will submit the form if&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// `pendingSubmit` is true.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;widgetId &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; turnstile &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      pendingSubmit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      turnstile&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;widgetId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// We could not talk to Turnstile at all – surface a clear error.&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Turnstile widget not available&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;onTokenError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;widget-not-available&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with &lt;a href=&quot;https://gist.github.com/Nooshu/dd608f9df6e21ad5e2ec3a37e1e12264&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;fewer comments is available in this gist on GitHub&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Now the updates to the &lt;code&gt;contact.js&lt;/code&gt; file that sits in &lt;code&gt;functions/api/&lt;/code&gt; directory.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;f4655e77defa1cfa875c376e2f85de24&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; escapeHtml &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;../../_helpers/escape-html.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Cloudflare Pages Function that powers the contact form.
 *
 * High‑level flow (perfect for a blog diagram):
 *
 * 1. Accept a classic `&amp;lt;form&gt;` POST (urlencoded or multipart).
 * 2. Validate the payload on the server (required fields + length limits).
 * 3. Verify a Cloudflare Turnstile token server‑side.
 * 4. Send a notification email via Resend (HTTPS API call).
 * 5. Redirect the user to a static “thanks” page.
 *
 * The matching HTML form does *not* need JavaScript – the worker stands on its
 * own. Client‑side JS (for validation + Turnstile widget) is purely a UX bonus.
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function function-variable&quot;&gt;onRequestPost&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 1. Only accept traditional browser form submissions&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// The contact page posts using `application/x-www-form-urlencoded`, but&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// we also support `multipart/form-data` so the handler works with&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// progressive enhancement and file inputs if they ever appear.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; contentType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;contentType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;application/x-www-form-urlencoded&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;contentType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;multipart/form-data&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Unsupported content type&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;415&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// This gives us a `FormData` instance regardless of which of the two&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// encodings the browser chose.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; form &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;formData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 2. Honeypot – cheap, early bot filter&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// The form includes a visually hidden checkbox with this wonderfully&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ugly name. Real users never see or tick it, but naive bots often&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// will. If it has a value, we quietly redirect back to the homepage.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;_its-a-trap!&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hpUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hpUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;303&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;name&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; email &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;email&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;message&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; redirectUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;_redirect&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/contact/thanks/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 3. Field‑level validation (server‑side, regardless of JS on client)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;email &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Missing required fields&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Upper bounds on field length keep logs and emails sane and protect&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// downstream services from unexpectedly huge payloads.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;320&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Invalid field lengths&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 4. Verify Cloudflare Turnstile token (server‑side)&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// The front‑end Turnstile widget places its token into&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// `cf-turnstile-response`. Without a token, we *always* reject the&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// submission – even if the rest of the payload looks valid.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; turnstileToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;cf-turnstile-response&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;turnstileToken&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Turnstile token is required&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// The Turnstile secret is never exposed to the client – it lives in&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Cloudflare Pages environment variables.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; secret &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;TURNSTILE_SECRET_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;secret&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Server not configured for CAPTCHA (missing TURNSTILE_SECRET_KEY)&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Including the remote IP is optional but recommended; it gives&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Turnstile a little more context when scoring the request.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; remoteIp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;CF-Connecting-IP&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cf&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;clientAddress &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; verifyBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			secret&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; turnstileToken&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;remoteIp&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			verifyBody&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;remoteip &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; remoteIp&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; verifyResp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;https://challenges.cloudflare.com/turnstile/v0/siteverify&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;verifyBody&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;verifyResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Failed to verify Turnstile&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; verifyJson &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; verifyResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// If Turnstile says “no”, do not send an email – fail fast here.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errorCodes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; verifyJson&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;error-codes&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;CAPTCHA verification failed&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; errorCodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; errorCodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;, &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Unknown error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Optional defence‑in‑depth check: ensure the token we received was&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// minted for this hostname (or one of its subdomains).&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestHost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt;
			verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; requestHost &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt;
			&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;requestHost&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Turnstile hostname mismatch: expected &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;requestHost&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, got &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;verifyJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 5. Build notification email payload for Resend&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// `fromEmail` must be a sender you control and have verified with Resend.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fromEmail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;email@example.com&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; subject &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;_email.subject&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;New Message from Nooshu.com&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; textBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;New contact form submission on nooshu.com&#92;n&#92;nName: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;nEmail: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;email&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&#92;nMessage:&#92;n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;message&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; htmlBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;h2&gt;New contact form submission on nooshu.com&amp;lt;/h2&gt;&amp;lt;p&gt;&amp;lt;strong&gt;Name:&amp;lt;/strong&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;br/&gt;&amp;lt;strong&gt;Email:&amp;lt;/strong&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;/p&gt;&amp;lt;p&gt;&amp;lt;strong&gt;Message:&amp;lt;/strong&gt;&amp;lt;/p&gt;&amp;lt;pre style=&quot;white-space:pre-wrap&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;/pre&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Resend API key comes from Cloudflare Pages environment variables,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// never from the client or build‑time `.env`.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; apiKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;apiKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Server not configured for email (missing RESEND_API_KEY)&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Call the Resend REST API. In this project we make a single,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// straightforward `emails` call – no templates, CC/BCC, or attachments.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; resendResp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;https://api.resend.com/emails&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;apiKey&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Nooshu Contact &amp;lt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;fromEmail&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;email@example.com&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;reply_to&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				subject&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; textBody&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; htmlBody&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;resendResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; text &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; resendResp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Email send failed&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; text &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 6. Redirect to “thanks” page&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// ---------------------------------------------------------------------&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Using a 303 ensures the follow‑up request is a GET, which means&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// browser refreshes do not re‑POST the form.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalRedirect &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;redirectUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalRedirect&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;303&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Server error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property literal-property&quot;&gt;details&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property literal-property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property string-property&quot;&gt;&#39;content-type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with &lt;a href=&quot;https://gist.github.com/Nooshu/f4655e77defa1cfa875c376e2f85de24&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;fewer comments is available in this gist on GitHub&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Amazingly, that&#39;s really the only two major changes!&lt;/p&gt;&lt;p&gt;The minor changes were:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Footer template&lt;/strong&gt;: Update my footer with a link to the JavaScript above and the Turnstile JavaScript, e.g. &lt;code&gt;https://challenges.cloudflare.com/turnstile/v0/api.js&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;CSP&lt;/strong&gt;: Modified my &lt;code&gt;_headers&lt;/code&gt; file to remove Google from my CSP and add in the challenges URL (https://challenges.cloudflare.com/) to the &lt;code&gt;connect-src&lt;/code&gt;, &lt;code&gt;script-src-elem&lt;/code&gt;, and &lt;code&gt;frame-src&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;CSS&lt;/strong&gt;: Slightly modify the CSS to remove Google ReCAPTCHA v3 specific styling (&lt;code&gt;.grecaptcha-badge&lt;/code&gt;) and add Turnstile CSS (&lt;code&gt;.turnstile-widget&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Secret Keys&lt;/strong&gt;: RECAPTCHA_SECRET_KEY swapped out for the TURNSTILE_SECRET_KEY in the Cloudflare Pages environment variables settings page.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Contact Form HTML&lt;/strong&gt;: HTML changes to the contact form added cf-turnstile-response hidden &lt;code&gt;&amp;lt;input&gt;&lt;/code&gt; and #turnstile-container &lt;code&gt;&amp;lt;div&gt;&lt;/code&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&quot;why-change&quot;&gt;Why change?&lt;/h3&gt;&lt;p&gt;So I&#39;ve literally done a 180 from Google ReCAPTCHA v3 to Cloudflare Turnstile in less than a week. So why change and what are the differences? The top of my priority list that I admittedly should have considered when integrating Google ReCAPTCHA v3 is privacy! I would rather not subject any readers of the blog to any additional tracking from Google! But there are other differences too:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Area&lt;/th&gt;&lt;th&gt;Cloudflare Turnstile advantage over reCAPTCHA v3&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Privacy&lt;/td&gt;&lt;td&gt;Turnstile is positioned as a privacy preserving alternative, and it does not rely on tracking user data across sites for ad retargeting. It also offers Ephemeral IDs, which are short-lived device identifiers that work without cookies or client-side storage.&lt;/td&gt;&lt;td&gt;Better fit for teams with strong privacy, GDPR, or public sector concerns.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Less operational tuning&lt;/td&gt;&lt;td&gt;Turnstile’s server validation is a straightforward success or failure check. By contrast, reCAPTCHA v3 returns a score and Google recommends that you review scores in the admin console and tune thresholds based on your own traffic, starting from 0.5.&lt;/td&gt;&lt;td&gt;Usually simpler to ship and maintain, especially for smaller teams that do not want to spend time calibrating fraud thresholds.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;No CAPTCHA experience by default&lt;/td&gt;&lt;td&gt;Turnstile works without showing visitors a CAPTCHA and supports invisible, non-interactive, and managed modes. reCAPTCHA v3 is also frictionless, but Turnstile is explicitly built as a CAPTCHA replacement with more presentation modes for the widget itself.&lt;/td&gt;&lt;td&gt;More control over UX and easier to keep forms feeling clean while still validating traffic.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Can be used on any site without Cloudflare CDN&lt;/td&gt;&lt;td&gt;Turnstile can be embedded into any website, it does not require your traffic to go through Cloudflare or use Cloudflare’s CDN.&lt;/td&gt;&lt;td&gt;Easier adoption if you want the bot check without changing your hosting or edge setup.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Developer-friendly testing&lt;/td&gt;&lt;td&gt;Turnstile provides dummy site keys and secret keys for testing, and Cloudflare documents that these work on localhost and any development domain. Google’s reCAPTCHA FAQ says for v3 you should create a separate key for testing environments, and scores may not be accurate because v3 relies on real traffic.&lt;/td&gt;&lt;td&gt;Usually a smoother local dev and automated test setup.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Free plan positioning&lt;/td&gt;&lt;td&gt;Cloudflare documents Turnstile has a Free plan intended for personal sites, SMBs, dev and test, and most production applications. Google’s FAQ notes quota limits for non-Enterprise reCAPTCHA, and if a v3 key exceeds a monthly quota it may fail open with a static score of 0.9 for the remainder of the month.&lt;/td&gt;&lt;td&gt;Attractive when cost certainty matters or when you want fewer surprises as usage grows.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Extra integration option with Cloudflare security stack&lt;/td&gt;&lt;td&gt;Turnstile supports pre-clearance, letting it issue clearance cookies that can be used across Cloudflare protected domains.&lt;/td&gt;&lt;td&gt;Helpful if you already use Cloudflare WAF or bot controls and want tighter integration.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Well, that brings us to the end of another blog post, I only went off on a tangent a couple of times! So well done for sticking with my ramblings!&lt;/p&gt;&lt;p&gt;In the end, the goal was to keep the architecture simple while still covering the essentials. The site itself remains fully static, with a lightweight Cloudflare Worker handling form submissions. Spam protection is provided through &lt;del&gt;Google reCAPTCHA v3&lt;/del&gt;, Cloudflare Turnstile, while validation and accessibility patterns ensure the form behaves reliably for real users. It is a small piece of functionality, but implemented carefully it can be both robust and easy to maintain.&lt;/p&gt;&lt;p&gt;As always, thanks for reading, and I’d love to hear your feedback. You are welcome to contact me either via the &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;contact form&lt;/a&gt; (again, very meta! 😏), or via any of the various social channels listed on the site.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;09/03/26: Initial post published.&lt;/li&gt;&lt;li&gt;13/03/26: Updates to give instructions on how to use Cloudflare Turnstile as an alternative to Google reCAPTCHA v3. Thanks again to &lt;a href=&quot;https://bsky.app/profile/twnsnd.com&quot;&gt;Ryan Townsend&lt;/a&gt; and &lt;a href=&quot;https://bsky.app/profile/pawelgrzybek.com&quot;&gt;Paweł Grzybek&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Precompressed HTML at the Edge: Eleventy Meets Cloudflare Workers</title>
    <link href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/" />
    <updated>2026-02-21T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/</id>
    <content type="html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;&lt;p&gt;In 2025, I wrote a series of web performance optimisation blog posts focussing on some of the key fundamental&#39;s of Frontend Web Performance:&lt;/p&gt;&lt;h3 id=&quot;caching&quot;&gt;Caching&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/09/02/asset-fingerprinting-and-the-preload-response-header-in-11ty/&quot;&gt;Asset fingerprinting and the preload response header in 11ty&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The blog post describes how the I enhanced web performance on my 11ty-built site by combining asset fingerprinting with the HTTP preload hint.&lt;/p&gt;&lt;p&gt;It explains that preload tells the browser to fetch critical resources earlier, but hashed filenames make this difficult to manage manually.&lt;/p&gt;&lt;p&gt;The solution was to generate preload Link headers automatically during the 11ty build. A custom script locates the fingerprinted CSS file and injects the correct preload header into the Cloudflare Pages &lt;code&gt;_headers&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;This speeds up CSS delivery, removes the need for manual updates, and allows the use of long-lived &lt;code&gt;Cache-Control&lt;/code&gt; header values such as &lt;code&gt;max-age=31536000&lt;/code&gt; and &lt;code&gt;immutable&lt;/code&gt;.&lt;/p&gt;&lt;h3 id=&quot;compression&quot;&gt;Compression&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/&quot;&gt;Cranking Brotli up to 11 with Cloudflare Pro and 11ty&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The blog post explains how I improved the performance of my 11ty-powered blog after migrating to Cloudflare Pages by using Brotli compression at the highest level (11) for static assets. I also describes the difference between Brotli and gzip, outline how Cloudflare’s Pro plan typically applies a moderate Brotli level (4), and then show how to pre-compress JavaScript files to Brotli level 11. Lastly, serve them correctly both locally and via Cloudflare, and configure Cloudflare’s compression rules so that all assets benefit from the stronger compression to reduce file sizes and improve load performance.&lt;/p&gt;&lt;h3 id=&quot;concatenation&quot;&gt;Concatenation&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/&quot;&gt;Using an 11ty Shortcode to craft a custom CSS pipeline&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;While not strictly about concatenation, it covers related ideas. The post explains how I built a custom CSS pipeline for my 11ty site using a bespoke short code rather than the default bundle plugin.&lt;/p&gt;&lt;p&gt;It details how I preserved local live reload, added content-based fingerprinting for long-term caching, minified CSS with &lt;a href=&quot;https://www.npmjs.com/package/clean-css&quot;&gt;clean-css package&lt;/a&gt;, enabled Brotli compression, and used disk and memory caching to prevent unnecessary work.&lt;/p&gt;&lt;p&gt;The build also generates hashed filenames and matching HTML link tags, ensuring production serves fully optimised, cache friendly CSS automatically.&lt;/p&gt;&lt;h2 id=&quot;more-compression&quot;&gt;More compression&lt;/h2&gt;&lt;p&gt;In this blog post, I’m going to take the compression a little further. I already have CSS and JavaScript Brotli compressed to the highest level (11) and served from Cloudflare Pages. But what about the third and final core technology of the web? Arguably the most important too… The HTML. In this post, I will look at how to compress your HTML to 11 during the 11ty build phase, and what modern technologies you need in order to make this work (it’s not as straightforward as I thought!)&lt;/p&gt;&lt;h2 id=&quot;why-html-brotli-matters&quot;&gt;Why HTML Brotli matters?&lt;/h2&gt;&lt;p&gt;Before we dive into the details, let’s discuss why Brotli compression is important for HTML. Well, to summarise Brotli compression in one sentence:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Brotli compression reduces file size by intelligently identifying repeated patterns in data and encoding them more efficiently using a combination of modern compression techniques.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This is fantastic for anything with repeating patterns as the bytes saved over the network can be huge! The great thing about HTML is that it has a tonne of repeating patterns (e.g. Markup)! This reduction in file size &lt;strong&gt;could&lt;/strong&gt; potentially equate to:&lt;/p&gt;&lt;h3 id=&quot;improved-web-performance&quot;&gt;Improved Web Performance&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Lower Time To First Byte impact on slow networks&lt;/li&gt;&lt;li&gt;Improved &lt;a href=&quot;https://web.dev/articles/fcp&quot;&gt;First Contentful Paint&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Improved &lt;a href=&quot;https://web.dev/articles/lcp&quot;&gt;Largest Contentful Paint&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Reduced &lt;a href=&quot;https://web.dev/articles/tbt&quot;&gt;Total Blocking Time&lt;/a&gt; (indirectly)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Every kilobyte saved here multiplies across your traffic volume.&lt;/p&gt;&lt;h3 id=&quot;at-scale-cost-savings-add-up&quot;&gt;At scale, cost savings add up&lt;/h3&gt;&lt;p&gt;Let’s assume you are serving a high traffic website, say the &lt;a href=&quot;https://www.bbc.co.uk&quot;&gt;BBC&lt;/a&gt;, &lt;a href=&quot;https://www.google.com&quot;&gt;Google&lt;/a&gt;, or &lt;a href=&quot;https://www.gov.uk&quot;&gt;GOV.UK&lt;/a&gt;. Imagine how much bandwidth could be saved by simply compressing your HTML. Any percentage saving on millions of requests per day is going to mean:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Less bandwidth&lt;/li&gt;&lt;li&gt;Lower &lt;a href=&quot;https://www.cloudflare.com/en-gb/learning/cloud/what-are-data-egress-fees/&quot;&gt;CDN egress&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Lower cloud costs&lt;/li&gt;&lt;li&gt;Lower carbon footprint&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is a win both commercially and environmentally!&lt;/p&gt;&lt;h3 id=&quot;improved-performance-on-slow-and-unstable-mobile-networks&quot;&gt;Improved performance on slow and unstable mobile networks&lt;/h3&gt;&lt;p&gt;Brotli 11 improves the web for users where they need it the most:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;3G connectivity&lt;/li&gt;&lt;li&gt;High latency rural connections&lt;/li&gt;&lt;li&gt;Congested public networks&lt;/li&gt;&lt;li&gt;International access&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;HTML blocks everything else. If you shrink it aggressively, you unblock the page faster, meaning users get a better experience.&lt;/p&gt;&lt;p&gt;This is a real-world performance gain.&lt;/p&gt;&lt;h3 id=&quot;it-indirectly-improves-core-web-vitals&quot;&gt;It indirectly improves Core Web Vitals&lt;/h3&gt;&lt;p&gt;Smaller HTML means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Faster DOM construction&lt;/li&gt;&lt;li&gt;Earlier CSS discovery&lt;/li&gt;&lt;li&gt;Earlier JS discovery&lt;/li&gt;&lt;li&gt;Reduced main thread idle gaps&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is especially important for server rendered pages or hybrid Server-side Rendered apps.&lt;/p&gt;&lt;h3 id=&quot;compression-cost-is-paid-once-for-static-assets&quot;&gt;Compression cost is paid once for static assets&lt;/h3&gt;&lt;p&gt;Yes, level 11 compression is expensive to compute. But this cost is paid back over time. You pay for the CPU time once. Users benefit forever assuming:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;the HTML is static and cacheable at the Content Delivery Network (CDN) using long-life caching headers&lt;/li&gt;&lt;li&gt;you automate and pre-compress the HTML at build time&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;compression-size-examples-v1&quot;&gt;Compression Size Examples v1&lt;/h3&gt;&lt;p&gt;Let’s have a look at a huge HTML page on the web. My go-to for this is either the &lt;a href=&quot;https://www.w3.org/TR/2011/WD-html5-20110405/Overview.html&quot;&gt;W3C HTML5 Specification page&lt;/a&gt; OR &lt;a href=&quot;https://apod.nasa.gov/apod/archivepix.html&quot;&gt;NASA’s Astronomy Picture of the Day Archive&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Rather than choose, let’s just compress both!&lt;/p&gt;&lt;h4 id=&quot;w3c-html5-specification-single-page&quot;&gt;W3C HTML5 Specification (Single Page)&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://www.w3.org/TR/2011/WD-html5-20110405/Overview.html&quot;&gt;https://www.w3.org/TR/2011/WD-html5-20110405/Overview.html&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uncompressed size&lt;/strong&gt;: 4.7 MB&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 590 KB (~88% saving)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That’s an &lt;strong&gt;88 percent reduction&lt;/strong&gt; in bytes over the network when compressing the HTML with Brotli 11. Not bad for something that takes just over 10-seconds to run.&lt;/p&gt;&lt;p&gt;And yes, that’s 4.7 MB of HTML alone. It’s an absolute beast of a page.&lt;/p&gt;&lt;p&gt;For perspective, that would take around 12 to 15 minutes to download on a 56k modem in the late 90s. Just for the HTML. I might be showing my age here 😭&lt;/p&gt;&lt;h4 id=&quot;nasa-s-astronomy-picture-of-the-day-archive&quot;&gt;Nasa’s Astronomy Picture of the Day Archive&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://apod.nasa.gov/apod/archivepix.html&quot;&gt;https://apod.nasa.gov/apod/archivepix.html&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uncompressed size&lt;/strong&gt;: 314 KB&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 53 KB (83% saving)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We’ve taken it from roughly a third of a megabyte down to just &lt;strong&gt;53 KB&lt;/strong&gt;. That is a pretty substantial reduction!&lt;/p&gt;&lt;p&gt;This means faster page loads, lower data usage, and a much smoother experience for anyone on a limited data plan or dealing with patchy connections. A very positive impact, right where it matters most for users.&lt;/p&gt;&lt;h2 id=&quot;how-is-this-different-from-my-other-compression-posts&quot;&gt;How is this different from my other compression posts?&lt;/h2&gt;&lt;p&gt;So how is this different from the Brotli compression I have mentioned in the previous blog posts?&lt;/p&gt;&lt;p&gt;In &lt;a href=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/&quot;&gt;Cranking Brotli up to 11 with Cloudflare Pro and 11ty&lt;/a&gt; I used a combination of:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Brotli CLI (e.g.&lt;code&gt;brew install brotli&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;bash scripts (&lt;code&gt;compress.sh&lt;/code&gt;, &lt;code&gt;compress-directory.sh&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In this post, I override the Cloudflare Dashboard&#39;s &quot;Compression Rules” (which dynamically compresses HTML at Brotli level 4). The CDN is compressing the HTML on-the-fly when the user’s browser requests it.&lt;/p&gt;&lt;p&gt;What this blog post describes is pre-compression to Brotli 11 at 11ty build time. To do this the workflow is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Apply Brotli level 11 pre-compression to HTML during the 11ty build on Cloudflare Pages.&lt;/li&gt;&lt;li&gt;&lt;code&gt;_helpers/html-compression.js&lt;/code&gt; runs after the site is written to &lt;code&gt;_site&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Each HTML file gets a matching &lt;code&gt;.br&lt;/code&gt; file&lt;/li&gt;&lt;li&gt;Use the built-in Node &lt;code&gt;zlib&lt;/code&gt; module, so no manual setup, CLI tools, scripts, Cloudflare configuration, or extra dependencies are required.&lt;/li&gt;&lt;li&gt;Take advantage of &lt;a href=&quot;https://developers.cloudflare.com/pages/functions/&quot;&gt;Cloudflare Pages Functions&lt;/a&gt;, which run on &lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers&lt;/a&gt;. This is a great opportunity to use a modern, fast evolving platform that opens the door to powerful edge capabilities.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;what-s-the-point&quot;&gt;What’s the point?&lt;/h2&gt;&lt;p&gt;Well, that’s a great question. As some readers may know by default Cloudflare compresses HTML at compression level 4. Now, if we compare this level to the compression level 11 above, let’s have a look at the difference:&lt;/p&gt;&lt;h3 id=&quot;compression-size-examples-v2&quot;&gt;Compression Size Examples v2&lt;/h3&gt;&lt;h4 id=&quot;w3c-html5-specification-single-page-2&quot;&gt;W3C HTML5 Specification (Single Page)&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://www.w3.org/TR/2011/WD-html5-20110405/Overview.html&quot;&gt;https://www.w3.org/TR/2011/WD-html5-20110405/Overview.html&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uncompressed size&lt;/strong&gt;: 4.7 MB&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 4)&lt;/strong&gt;: 729 KB (83% saving)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Compression Time (Level 4)&lt;/strong&gt;: 0.149 seconds&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 590 KB (87% saving)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 11.717 seconds&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;nasa-s-astronomy-picture-of-the-day-archive-2&quot;&gt;Nasa’s Astronomy Picture of the Day Archive&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href=&quot;https://apod.nasa.gov/apod/archivepix.html&quot;&gt;https://apod.nasa.gov/apod/archivepix.html&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uncompressed size&lt;/strong&gt;: 314 KB&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 4)&lt;/strong&gt;: 66 KB (79%)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Compression Time (Level 4)&lt;/strong&gt;: 0.011 seconds&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 53 KB (83% saving)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 0.743 seconds&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It’s worth noting that I used &lt;a href=&quot;https://paulcalvano.com/&quot;&gt;Paul Calvano&#39;s&lt;/a&gt; fantastic &lt;a href=&quot;https://tools.paulcalvano.com/compression-tester/&quot;&gt;Compression Tester Tool&lt;/a&gt;, to help with this basic analysis.&lt;/p&gt;&lt;p&gt;What you will likely notice is that even for large HTML files the size difference between compression level 4 and compression level 11 isn’t huge, only 12 KB in the W3C HTML5 Specification example. The real difference comes in computation time. Level 4 0.149 seconds verses 11.717 seconds! That’s a &lt;strong&gt;7764% increase&lt;/strong&gt; in time between Level 4 and Level 11! Although this is an extreme example, you can probably see why Level 11 isn’t used by Cloudflare for on-the-fly compression of HTML assets. Level 4 gives a good balence between file compression versus compression speed. I’m betting countless smart people were involved in the analysis of using Level 4 by default! When you are a company that is literally serving billions of requests per second, this decision really makes a difference in terms of processing power infrastructure and power usage!&lt;/p&gt;&lt;p&gt;Thankfully, the way that I have implemented level 11 compression on my blog, processing time doesn’t really matter. All the HTML is being compressed at 11ty build time. As I said above, the cost of this additional CPU time is paid back over time by users getting a better experience (even if only slightly). Furthermore, remember there’s a slight reduction in storage required on the CDN. From my perspective, if it is low effort after setup, it feels like the right move to implement it.&lt;/p&gt;&lt;h2 id=&quot;problems&quot;&gt;Problems&lt;/h2&gt;&lt;p&gt;As I found out during implementation it isn’t just as simple as compressing the HTML to 11 and setting a static &lt;code&gt;Content-Encoding&lt;/code&gt; header for HTML in the &lt;code&gt;_headers&lt;/code&gt; file (trust me, I tried it!)&lt;/p&gt;&lt;h3 id=&quot;problem-1-url-path-vs-actual-file-path-mismatch&quot;&gt;Problem 1: URL Path vs Actual File Path Mismatch&lt;/h3&gt;&lt;p&gt;When serving static assets like CSS, JS, or images, there is usually a simple one-to-one relationship between the URL and the file on disk. A request to &lt;code&gt;/css/site.css&lt;/code&gt; maps directly to &lt;code&gt;_site/css/site.css&lt;/code&gt;. No extra logic is required because the URL path matches the file path exactly. I had no idea, but I soon found out that HTML pages behave differently. A request to &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;/blog/post/&lt;/code&gt; does not correspond to a literal file at that path. Instead, the server applies a convention and serves &lt;code&gt;index.html&lt;/code&gt; inside that directory. So &lt;code&gt;/blog/post/&lt;/code&gt; actually maps to &lt;code&gt;blog/post/index.html&lt;/code&gt; on disk. This mapping happens automatically when Cloudflare serves uncompressed HTML through its very efficient static asset layer.&lt;/p&gt;&lt;p&gt;The problem appears when serving pre-compressed Brotli files. You cannot simply request the same URL and expect the .br file to resolve. Instead, the Cloudflare Function must manually translate the directory style URL into the real file path before appending &lt;code&gt;.br&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;For example, &lt;code&gt;/&lt;/code&gt; becomes &lt;code&gt;/index.html.br&lt;/code&gt;, &lt;code&gt;/blog/post/&lt;/code&gt; becomes &lt;code&gt;/blog/post/index.html.br&lt;/code&gt;, and &lt;code&gt;/404.html&lt;/code&gt; becomes &lt;code&gt;/404.html.br&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;In short, HTML requires explicit path translation because the browser sees a directory style URL while the actual file stored on disk is index.html. The Cloudflare Function must bridge that gap to correctly serve the Brotli 11 compressed version, rather than the uncompressed HTML version.&lt;/p&gt;&lt;p&gt;It actually makes sense now that I think about it, I’ve always just taken the automatic appending of &lt;code&gt;index.html&lt;/code&gt; to a URL Path for granted! The fact that as a user on the web doesn’t even have to think about that small detail, shows how well it works! As &lt;a href=&quot;https://en.wikipedia.org/wiki/Dieter_Rams&quot;&gt;Dieter Rams&lt;/a&gt; once said:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Good design is as little design as possible.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;problem-2-a-page-full-of-wingdings&quot;&gt;Problem 2: A page full of Wingdings&lt;/h3&gt;&lt;p&gt;I’m showing my age again, but for readers who don’t remember early versions of Windows (e.g. &lt;a href=&quot;https://en.wikipedia.org/wiki/Windows_3.1&quot;&gt;3.1&lt;/a&gt;), it came bundled with a font called &lt;a href=&quot;https://en.wikipedia.org/wiki/Wingdings&quot;&gt;Wingdings&lt;/a&gt;. This True Type font contains many largely recognised shapes and gestures as well as some recognised world symbols.&lt;/p&gt;&lt;p&gt;Wingdings were an early symbol font that experimented with pictographic digital symbols, that would later lead on to ASCII emoticons like &lt;code&gt;:-)&lt;/code&gt; &amp; &lt;code&gt;¯&#92;_(ツ)_/¯&lt;/code&gt;, which in turn would progress to the modern world of &lt;a href=&quot;https://emojipedia.org/&quot;&gt;Emoji’s&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;Essentially, what was happening the Cloudflare server was serving raw Brotli compressed HTML files to the browser, expecting it to understand what these (now binary, not text) files were, and how to read and understand them. I was essentially serving the HTML without the following headers:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Content-Type: text/html; charset=UTF-8
Content-Encoding: br
Vary: Accept-Encoding
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here’s a brief explanation of these headers:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;code&gt;Content-Encoding: br&lt;/code&gt;: This is telling the browser “What I’m sending you is a Brotli compressed file, you are going to need to decode it before you understand it”.&lt;/li&gt;&lt;li&gt;&lt;code&gt;Content-Type: text/html; charset=UTF-8&lt;/code&gt;: This tells the browser what character set (&#39;charset&#39;) it should use after decompression, this is essential as this is key to the browser parsing the HTML correctly.&lt;/li&gt;&lt;li&gt;&lt;code&gt;Vary: Accept-Encoding&lt;/code&gt; only affects caching (e.g. Content Delivery Networks). It’s basically saying to the cache to store separate versions of this file depending on the Accept-Encoding header.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The result of the above gave me a homepage that looked like the image below (interesting but not exactly readable!):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The garbage output of a raw Brotli encoded HTML file in the browser looks like something from the Wingdings font from the early 90’s&quot; decoding=&quot;async&quot; height=&quot;454&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/so0HRc7rVz-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/so0HRc7rVz-300.webp 300w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/so0HRc7rVz-600.webp 600w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/so0HRc7rVz-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;my-approach&quot;&gt;My Approach&lt;/h2&gt;&lt;h3 id=&quot;step-1-build-time-compression&quot;&gt;Step 1: Build-time compression&lt;/h3&gt;&lt;p&gt;After the site is generated, it moves into a final preparation stage before going live.&lt;/p&gt;&lt;p&gt;During this stage, the system goes through all the finished HTML pages and creates highly compressed versions of them. These compressed files sit alongside the originals and are ready to be served immediately.&lt;/p&gt;&lt;p&gt;Because this happens as part of the 11ty build process, every release automatically includes freshly optimised files. This means the live site can deliver pages faster, with smaller file sizes and no need to compress anything on the fly (e.g. what Cloudflare does with its HTML compression to Brotli level 4).&lt;/p&gt;&lt;p&gt;The result is better performance for users, with no extra overhead once the site is hosted and running on Cloudflare pages.&lt;/p&gt;&lt;h3 id=&quot;step-2-cloudflare-pages-function-for-content-negotiation&quot;&gt;Step 2: Cloudflare Pages Function for content negotiation&lt;/h3&gt;&lt;p&gt;When someone visits a page on the site, a lightweight Cloudflare Function (via a Cloudflare Worker) checks whether a users browser supports modern compression.&lt;/p&gt;&lt;p&gt;If it does, the system serves the Brotli 11 pre compressed version of the page. This keeps file sizes small and pages loading quickly.&lt;/p&gt;&lt;p&gt;If the browser does not support this compression, or if a compressed version is not available, the system simply serves the standard uncompressed version of the HTML instead.&lt;/p&gt;&lt;p&gt;No extra processing happens at this stage. The edge layer (Cloudflare Function + Worker) is only deciding which version of the already prepared files to send. All optimisation work has already been done earlier in the 11ty build process.&lt;/p&gt;&lt;p&gt;The final result is fast delivery, efficient bandwidth use, and a simple, reliable build setup. Everyone wins!&lt;/p&gt;&lt;h3 id=&quot;step-3-the-eleventy-build-uses-node-js-zlib-for-seamless-integration&quot;&gt;Step 3: The Eleventy build uses Node.js zlib, for seamless integration&lt;/h3&gt;&lt;p&gt;As mentioned earlier, this Brotli implementation differs from others I have used. Instead of relying on bash scripts such as &lt;code&gt;compress.sh&lt;/code&gt; or &lt;code&gt;compress-directory.sh&lt;/code&gt;, it uses Node.js’s built in &lt;code&gt;zlib&lt;/code&gt; module. Because &lt;a href=&quot;https://nodejs.org/dist/latest/docs/api/zlib.html#zlib&quot;&gt;zlib is part of Node.js core&lt;/a&gt;, it is stable, well maintained, and requires no external dependencies. It has been available since the earliest Node.js releases, so it is a sensible default choice. I may even revisit the other build process in future and consider replacing the remaining bash scripts with a fully Node.js based approach.&lt;/p&gt;&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;&lt;p&gt;Next, let’s stop the waffling and get onto the actual implementation, I assume that’s what readers are here for after all!&lt;/p&gt;&lt;h3 id=&quot;core-compression-utility&quot;&gt;Core compression utility&lt;/h3&gt;&lt;p&gt;Here is the main compression file that is used to compress the HTML to Brotli 11:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; data-gist=&quot;0dd55a4ba67da0a6d0053dc0e2884ba8&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Core compression utility: Brotli compression for build-time pre-compression.
 *
 * This module is the single source of truth for Brotli in the project. It is used by:
 * - html-compression.js  — compresses all HTML in _site to .br (level 11)
 * - css-manipulation.js  — compresses processed CSS to .br when writing to _site
 * - js-compression.js    — compresses minified JS in _site/js to .br
 *
 * All compression happens during the Eleventy production build (postbuild phase).
 * Pre-compressed .br files are then served via content negotiation in functions/[[path]].js
 * when the client sends Accept-Encoding: br, avoiding any runtime or CDN dynamic compression.
 */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; brotliCompressSync &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;zlib&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Default Brotli compression level.
 *
 * Brotli levels range from 0 to 11:
 * - 0–3: Fast, lower ratio (typical for dynamic/on-the-fly compression; e.g. CDNs often use 4).
 * - 11:  Maximum ratio, slowest; ideal for static assets compressed once at build time.
 *
 * We use 11 because compression runs only during the build, so CPU cost is paid once per
 * deploy. The resulting .br files are then served as-is with no re-compression at the edge.
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BROTLI_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Compress input data with Brotli.
 *
 * Uses Node&#39;s synchronous zlib API so callers can use the function in a simple, blocking
 * way during the build (no need to await). Input is normalized to a Buffer so that
 * strings (e.g. file contents read as UTF-8) and TypedArrays are supported.
 *
 * @param {Buffer | Uint8Array | string} input - Data to compress. If a string, it is
 *   encoded as UTF-8. Buffers and Uint8Array are used as-is (after copying to a Buffer
 *   when necessary via Buffer.from).
 * @param {number} [level=BROTLI_LEVEL] - Compression level 0–11. Defaults to 11 for
 *   maximum ratio. Can be overridden (e.g. via BROTLI_COMPRESSION_LEVEL in css-manipulation).
 * @returns {Buffer} - Compressed data as a Buffer. Callers typically write this to a
 *   file with a .br extension (e.g. index.html.br).
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;brotliCompress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;input&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; level &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BROTLI_LEVEL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; buffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isBuffer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; input &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;brotliCompressSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;buffer&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; level &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I appreciate that not everyone prefers heavily commented code, so there is also a version in &lt;a href=&quot;https://gist.github.com/Nooshu/0dd55a4ba67da0a6d0053dc0e2884ba8&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;this Gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; with minimal comments and improved readability.&lt;/p&gt;&lt;h3 id=&quot;html-compression-post-build&quot;&gt;HTML compression post-build&lt;/h3&gt;&lt;p&gt;The post-build HTML compression file:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; data-gist=&quot;568f44fe571d11660bc3e4e281f6329b&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * HTML Brotli compression (postbuild).
 *
 * This module compresses every HTML file in the Eleventy output directory (_site) to
 * Brotli level 11 and writes a corresponding .br file alongside each .html file
 * (e.g. index.html → index.html.br, blog/post/index.html → blog/post/index.html.br).
 *
 * When it runs:
 *   Only during the production postbuild phase, after Eleventy has finished writing
 *   _site. It is invoked from _config/build-events.js in the eleventy.after handler
 *   (alongside JS minification, JS Brotli, and preload header generation).
 *
 * How .br files are served:
 *   The Cloudflare Pages Function functions/[[path]].js performs content negotiation
 *   for HTML document requests. When the client sends Accept-Encoding: br, the Function
 *   fetches the pre-built .br asset (e.g. / → index.html.br) and returns it with
 *   Content-Encoding: br. Clients that do not advertise Brotli support receive the
 *   uncompressed .html from the static asset bucket. No dynamic compression runs at
 *   the edge; this step does all compression once at build time.
 *
 * Uses the core compression utility _helpers/compression.js for the actual Brotli call.
 */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; brotliCompress&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BROTLI_LEVEL&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;./compression.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Recursively find all .html files under a directory.
 *
 * Used to discover every HTML file in _site (root index.html, blog posts, portfolio
 * items, 404.html, etc.). Paths are returned as relative to _site so that we can
 * join them with siteDir for full paths and still have readable relative paths for
 * logging and error messages.
 *
 * @param {string} dir - Absolute or relative directory path to search (e.g. ./_site or a subdir).
 * @param {string[]} [acc=[]] - Accumulator array; results are pushed here during recursion.
 * @returns {string[]} Relative paths to .html files (e.g. [&#39;index.html&#39;, &#39;blog/post/index.html&#39;]).
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findHtmlFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; acc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entries &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;withFileTypes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entry &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; entries&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fullPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; relPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;relative&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fullPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token function&quot;&gt;findHtmlFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fullPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; acc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			acc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;relPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; acc&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Compress all HTML files in _site with Brotli level 11, writing .br files.
 *
 * Call this only after the Eleventy build has completed and _site is fully written.
 * For each .html file: reads content, compresses with brotliCompress (level 11),
 * writes &amp;lt;filename&gt;.br next to the original. Existing .br files are skipped if their
 * mtime is &gt;= the source .html mtime (incremental safety; in a full build all HTML is
 * usually newer, so most files are compressed). Logs counts, total bytes saved, and
 * duration; collects per-file errors without stopping the loop.
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;compressHtmlFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; startTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;🚀 PRODUCTION POSTBUILD: Starting HTML Brotli compression&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; siteDir &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;siteDir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;⚠️  _site directory not found, skipping HTML compression&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; htmlFiles &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findHtmlFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;siteDir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;htmlFiles&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;⚠️  No HTML files found, skipping compression&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; compressedCount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; skippedCount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; totalOriginal &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; totalCompressed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errors &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; relPath &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; htmlFiles&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;siteDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; relPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.br&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Skip writing if .br already exists and is not older than the source .html&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputStats &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputStats &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputStats&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mtime &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; inputStats&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mtime&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					skippedCount&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fileContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; originalSize &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fileContent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;brotliCompress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fileContent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BROTLI_LEVEL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; compressedSize &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; brotliBuffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			compressedCount&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			totalOriginal &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; originalSize&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			totalCompressed &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; compressedSize&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;❌ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;relPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; totalTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; startTime&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; savedPercent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; totalOriginal &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; totalCompressed &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; totalOriginal&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;0&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;✅ Compressed &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;compressedCount&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; HTML file(s) (&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;skippedCount&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; skipped, up-to-date)&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;compressedCount &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;totalOriginal &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; KB → &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;totalCompressed &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; KB (&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;savedPercent&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;% smaller)&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;   Finished in &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;totalTime&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;ms (&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;totalTime &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;s)&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;nErrors:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		errors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;e&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with minimal comments and much improved readability is available in a &lt;a href=&quot;https://gist.github.com/Nooshu/568f44fe571d11660bc3e4e281f6329b&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;cloudflare-pages-function-for-content-negotiation&quot;&gt;Cloudflare Pages Function for content negotiation&lt;/h3&gt;&lt;p&gt;Here we have the Cloudflare Function file that runs in the Cloudflare Worker. It is located in the &lt;code&gt;/functions&lt;/code&gt; directory in the root of the repository so it can be detected when Cloudflare Pages builds.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; data-gist=&quot;5176f0d3446e05629501130912f09570&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Catch-all Cloudflare Pages Function: HTML document content negotiation.
 *
 * This Function runs for every GET and HEAD request that is not handled by a more
 * specific route (e.g. POST /api/contact is handled by functions/api/contact.js).
 * Its job is to serve pre-compressed Brotli (.br) HTML when the client supports it,
 * and otherwise pass through to the static asset bucket (uncompressed HTML).
 *
 * Pre-compressed .br files are produced at build time by _helpers/html-compression.js
 * (postbuild): every .html in _site gets a matching .br (e.g. index.html.br,
 * blog/post/index.html.br). This Function does not compress on the fly; it only
 * chooses which asset to serve and sets the correct response headers.
 *
 * Flow:
 * 1. If the request is not for an HTML document URL (see below), pass through to ASSETS.
 * 2. If the client does not send Accept-Encoding: br, pass through (serve uncompressed HTML).
 * 3. Otherwise, fetch the corresponding .br asset; if missing or error, fall back to ASSETS
 *    for the original request (uncompressed).
 * 4. If .br exists, return it with Content-Encoding: br and no-transform so nothing
 *    re-compresses the body.
 */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pathname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Only treat as HTML document: root (/), directory-style paths ending in /, or 404&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// (Cloudflare Pages maps /404 to /404.html). All other paths (e.g. /script.js,&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// /style.css, /favicon.ico) go straight to static assets; Pages serves .br for&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// static files when available and client sends Accept-Encoding: br.&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; isHtmlDocument &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pathname &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; pathname&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; pathname &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/404.html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;isHtmlDocument&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ASSETS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; acceptsBrotli &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Accept-Encoding&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;br&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;acceptsBrotli&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ASSETS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Map URL path to the .br file in the asset bucket. Eleventy outputs directory&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// indexes as index.html (e.g. /blog/post/ → blog/post/index.html), so the .br&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// file is &amp;lt;path&gt;index.html.br.&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
		pathname &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/index.html.br&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; pathname &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/404.html&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/404.html.br&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;pathname&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;index.html.br&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;origin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ASSETS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;brResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ASSETS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Build response from the .br body with headers that declare Brotli and prevent&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// any downstream re-compression or transformation.&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; headers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Content-Encoding&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;br&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;text/html; charset=UTF-8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Vary&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Accept-Encoding&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// no-transform: tells caches (including Cloudflare) not to re-encode or modify the body&lt;/span&gt;
	headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Cache-Control&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;public, max-age=31536000, no-transform&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; brResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		headers&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// encodeBody: &#39;manual&#39; — we are returning the body as-is (already Brotli).&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Without this, the Workers runtime might try to compress the response again.&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;encodeBody&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;manual&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function function-variable&quot;&gt;onRequestGet&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function function-variable&quot;&gt;onRequestHead&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with minimal comments and much better readability is available in a &lt;a href=&quot;https://gist.github.com/Nooshu/5176f0d3446e05629501130912f09570&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;build-lifecycle-wiring&quot;&gt;Build lifecycle wiring&lt;/h3&gt;&lt;p&gt;This is the main build file I use to build my 11ty blog for production on Cloudflare Pages.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; data-gist=&quot;a6299b047d6f88a586de9b126216b49e&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Build lifecycle wiring: registers Eleventy before/after hooks for production.
 *
 * This module is the single place where postbuild steps are scheduled. It is loaded
 * from the main Eleventy config (e.g. eleventy.config.js), which calls
 * registerBuildEvents(eleventyConfig). Handlers run only when env.isLocal is false
 * (i.e. production or preview builds); local dev builds skip all of this so that
 * _site is left as plain output and the dev server stays fast.
 *
 * Order of operations:
 * - eleventy.before: clear CSS build cache so CSS is regenerated and Brotli-compressed
 *   from scratch when needed.
 * - eleventy.after: run the production postbuild phase in a fixed order:
 *   1. generatePreloadHeaders() — write Link headers into _headers for CSS (and .br).
 *   2. minifyJavaScriptFiles()   — minify JS in _site/js (must run before Brotli).
 *   3. compressJavaScriptFiles() — Brotli-compress JS to .br.
 *   4. compressHtmlFiles()       — Brotli-compress all HTML in _site to .br.
 *   Then: destroy HTTP/HTTPS global agents to avoid hanging connections, and on
 *   production only force process.exit(0) after a short delay so the process
 *   terminates cleanly on CI/Cloudflare Pages.
 */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; env &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;../_data/env.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; clearCssBuildCache &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;../_helpers/css-manipulation.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; generatePreloadHeaders &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;../_helpers/header-generator.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; compressHtmlFiles &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;../_helpers/html-compression.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; compressJavaScriptFiles &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;../_helpers/js-compression.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; minifyJavaScriptFiles &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;../_helpers/js-minify.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Register eleventy.before and eleventy.after handlers.
 * Only registered when !env.isLocal (production/preview).
 * @param {import(&quot;@11ty/eleventy&quot;).UserConfig} eleventyConfig
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;registerBuildEvents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isLocal&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Before build: clear any cached CSS build artifacts so this run produces fresh&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// processed and Brotli-compressed CSS when templates reference CSS.&lt;/span&gt;
	eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;eleventy.before&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;clearCssBuildCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;eleventy.after&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n═══════════════════════════════════════════════════════════════════════════════&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;🚀 PRODUCTION POSTBUILD PHASE: Beginning postbuild operations&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;═══════════════════════════════════════════════════════════════════════════════&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;generatePreloadHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;minifyJavaScriptFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;compressJavaScriptFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;compressHtmlFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n═══════════════════════════════════════════════════════════════════════════════&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;✅ PRODUCTION POSTBUILD PHASE: All postbuild operations completed&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;═══════════════════════════════════════════════════════════════════════════════&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Tear down Node&#39;s default HTTP/HTTPS agents so the process can exit without&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// waiting for keep-alive connections to time out (e.g. on Cloudflare Pages CI).&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;🔧 Forcing cleanup of HTTP connections and timers...&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resolve&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; http &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;http&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; https &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;https&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAgent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAgent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;destroy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;✅ Destroyed HTTP global agent&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;https&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAgent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				https&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAgent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;destroy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;✅ Destroyed HTTPS global agent&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;⚠️  Could not destroy HTTP agents:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resolve&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;✅ HTTP connection cleanup completed&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// On production builds only, force exit after a short delay so the runner&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// (e.g. Cloudflare Pages) gets a clean exit code and doesn&#39;t hang.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isProd&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;🏁 Production build complete - forcing process exit in 2 seconds...&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;👋 Forcing clean exit now&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with minimal comments and much improved readability is available in a &lt;a href=&quot;https://gist.github.com/Nooshu/a6299b047d6f88a586de9b126216b49e&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;other-technical-details-worth-mentioning&quot;&gt;Other Technical details worth mentioning&lt;/h2&gt;&lt;p&gt;Here are 4 other small technical details worth explaining as part of the implementation:&lt;/p&gt;&lt;h3 id=&quot;why-encodebody-manual-is-required&quot;&gt;Why encodeBody: &#39;manual&#39; is required&lt;/h3&gt;&lt;p&gt;The content we send back to the user&#39;s browser is already compressed using Brotli. It is being loaded from a file that has already been compressed in advance.&lt;/p&gt;&lt;p&gt;If we don&#39;t tell Cloudflare to leave it alone, it may assume the content is not compressed and try to compress it again. Compressing something that is already compressed can cause problems and may result in a broken or unreadable output, plus it is a waste of CPU time and resources.&lt;/p&gt;&lt;p&gt;By setting &lt;code&gt;encodeBody: ‘manual&#39;&lt;/code&gt;, we are telling Cloudflare to send the content exactly as it is, without changing it. This ensures the pre-compressed file is delivered correctly to the user&#39;s browser.&lt;/p&gt;&lt;h3 id=&quot;why-vary-accept-encoding-and-cache-control-no-transform-matter&quot;&gt;Why Vary: Accept-Encoding and Cache-Control: no-transform matter&lt;/h3&gt;&lt;p&gt;These 2 response headers are critical for making the pre-compression work. I&#39;ve given details as to why that is below:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;&lt;/strong&gt;: This notifies browsers and CDNs that the response can change depending on what kind of compression the browser supports.&lt;/p&gt;&lt;p&gt;For example, if a browser says it supports Brotli, the cache will store and return the Brotli version for those requests. If another browser doesn&#39;t support Brotli, the cache will store and return a different version, such as an uncompressed version. This prevents the wrong format being sent to the wrong browser.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;Cache-Control: no-transform&lt;/code&gt;&lt;/strong&gt;: This informs caches and other systems between the server and the user&#39;s browser not to modify the content. It asserts that the response should not be compressed again or altered in any way.&lt;/p&gt;&lt;p&gt;Without this setting, a proxy might try to compress the content again, which can cause errors and waste processing power. With this header in place, the already compressed file is stored and delivered exactly as intended.&lt;/p&gt;&lt;h3 id=&quot;incremental-build-optimisation-runtime-check-to-skip-unchanged-files&quot;&gt;Incremental build optimisation (runtime check to skip unchanged files)&lt;/h3&gt;&lt;p&gt;After the Eleventy build finishes, the post-build step recursively scans through the output folder, such as the &lt;code&gt;_site&lt;/code&gt; directory, and checks each HTML file.&lt;/p&gt;&lt;p&gt;Before compressing a file, it checks whether a matching &lt;code&gt;.br&lt;/code&gt; file already exists and whether it is up-to-date. If the &lt;code&gt;.br&lt;/code&gt; file is the same age or newer than the original file, it is skipped. If the page is new or has been updated, a fresh compressed version is created.&lt;/p&gt;&lt;p&gt;This avoids pages that have not changed being needlessly recompressed, keeping the post build step fast. When only a few pages are updated, the need for recompression is limited.&lt;/p&gt;&lt;h3 id=&quot;why-we-need-a-cloudflare-function-instead-of-just-the-headers-file-for-html-brotli&quot;&gt;Why we need a Cloudflare Function instead of just the &lt;code&gt;_headers&lt;/code&gt; file for HTML Brotli?&lt;/h3&gt;&lt;h4 id=&quot;1-headers-can-only-change-headers-not-the-file-itself&quot;&gt;1. &lt;code&gt;_headers&lt;/code&gt; can only change headers, not the file itself&lt;/h4&gt;&lt;p&gt;The &lt;code&gt;_headers&lt;/code&gt; file lets us add or modify (but not remove) response headers. It doesn&#39;t control which file is actually sent back to the browser.&lt;/p&gt;&lt;p&gt;So, when someone visits &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;/blog/post/&lt;/code&gt;, Cloudflare Pages automatically serves &lt;code&gt;index.html&lt;/code&gt; or &lt;code&gt;blog/post/index.html&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If we want to serve the Brotli version of the HTML, we need to send &lt;code&gt;index.html.br&lt;/code&gt; instead. But the &lt;code&gt;_headers&lt;/code&gt; file has no way to switch the file being served.&lt;/p&gt;&lt;h4 id=&quot;2-setting-the-header-alone-is-not-enough&quot;&gt;2. Setting the header alone is not enough&lt;/h4&gt;&lt;p&gt;Even if you add &lt;code&gt;Content-Encoding: br&lt;/code&gt; in the static &lt;code&gt;_headers&lt;/code&gt; file, the actual file being sent would still be the normal uncompressed version of the HTML.&lt;/p&gt;&lt;p&gt;The browser would see this Brotli header and try to decompress the response. Since the content being sent isn&#39;t compressed, it would simply fail and the page would break.&lt;/p&gt;&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;&lt;p&gt;Looking at my build logs I can now see that compressing the HTML to Brotli 11 has had the following results:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;📊 HTML Brotli 11 total savings: 123.4 KB (75.2% reduction)&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;That’s not too bad a saving considering how simple it is to set up and integrate into the 11ty build process! Thankfully, now that it’s done I can just “set it and forget it!”. Let&#39;s examine the results from the DevTools Network panel below:&lt;/p&gt;&lt;h3 id=&quot;devtools-before&quot;&gt;DevTools Before&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;The Firefox DevTools panel before implementation, no Brotli compression in the Content-Encoding response header Network panel column&quot; decoding=&quot;async&quot; height=&quot;341&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/NnpcUW5tX8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/NnpcUW5tX8-300.webp 300w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/NnpcUW5tX8-600.webp 600w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/NnpcUW5tX8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;devtools-after&quot;&gt;DevTools After&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;The Firefox DevTools panel before implementation, Brotli compression can be seen in the Content-Encoding response header Network panel column&quot; decoding=&quot;async&quot; height=&quot;341&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/16NWb1-HYF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/16NWb1-HYF-300.webp 300w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/16NWb1-HYF-600.webp 600w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/16NWb1-HYF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;devtools-after-page-reload&quot;&gt;DevTools After Page Reload&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;The Firefox DevTools panel on page reload, no Brotli compression in the Content-Encoding response header Network panel column can be seen&quot; decoding=&quot;async&quot; height=&quot;334&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/VCyv4pYajj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/VCyv4pYajj-300.webp 300w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/VCyv4pYajj-600.webp 600w, https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/VCyv4pYajj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Curiously, when reloading the page with DevTools open, the HTTP status code changes from 200 to 304 and the Brotli compression in the &lt;code&gt;Content-Encoding: br&lt;/code&gt; disappears. The reason for this is because either:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The reload request doesn’t contain an &lt;code&gt;Accept-Encoding: br&lt;/code&gt; header so the Cloudflare Worker is simply returning the uncompressed version of the HTML file as is expected.&lt;/li&gt;&lt;li&gt;The 304 has no response body, so there’s nothing to show as being compressed.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;difference-in-build-times&quot;&gt;Difference in build times&lt;/h2&gt;&lt;p&gt;Thanks to &lt;a href=&quot;https://toot.cafe/@tomayac&quot;&gt;Thomas Steiner on Mastodon&lt;/a&gt; for the reminder. It completely slipped my mind to share the difference in build time before and after adding HTML Brotli compression.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Total Build time before: ~ 2 minutes 58 seconds&lt;/li&gt;&lt;li&gt;Total Build time after: ~ 3 minutes 16 seconds&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Compressed&lt;/strong&gt;: 485 HTML file(s) (0 skipped, up-to-date) &lt;strong&gt;Total time&lt;/strong&gt;: 18.191299 seconds (about 18.19s) to Brotli compress all HTML files.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;By pre-compressing this blog’s HTML during the 11ty build phase, I have reduced the number of bytes sent on each page load. While the savings are small for a low traffic site like mine, at scale across millions of users and billions of requests per day, this approach could deliver meaningful bandwidth reductions and incremental performance improvements. This is especially true where network speed and stability vary globally.&lt;/p&gt;&lt;p&gt;Thank you for reading, I hope you found it useful. &lt;a href=&quot;https://en.wikipedia.org/wiki/Edge_computing&quot;&gt;Edge Workers&lt;/a&gt; are an incredibly powerful technology. I genuinely look forward to using them again in the future.&lt;/p&gt;&lt;p&gt;I always open to feedback and corrections. If you spot anything that needs fixing or is incorrect, please &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;. I will credit you in the post changelog below.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;21/02/26: Initial post published.&lt;/li&gt;&lt;li&gt;24/02/26: Thanks to &lt;a href=&quot;https://toot.cafe/@tomayac&quot;&gt;Thomas Steiner&lt;/a&gt; for nudging me to add details about the difference in build times!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Asset fingerprinting and the preload response header in 11ty</title>
    <link href="https://nooshu.com/blog/2025/09/02/asset-fingerprinting-and-the-preload-response-header-in-11ty/" />
    <updated>2025-09-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/09/02/asset-fingerprinting-and-the-preload-response-header-in-11ty/</id>
    <content type="html">&lt;p&gt;This blog post will be building on a number of blog posts that I wrote earlier in the year. These were the posts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/&quot;&gt;Using an 11ty Shortcode to craft a custom CSS pipeline&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/&quot;&gt;Cranking Brotli up to 11 with Cloudflare Pro and 11ty&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/01/23/the-speed-trifecta-11ty-brotli-11-and-css-fingerprinting/&quot;&gt;The Speed Trifecta: 11ty, Brotli 11, and CSS Fingerprinting&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Some insights from my earlier posts may carry over here, so check them out for overlap or a fuller view of my custom CSS pipeline for 11ty.&lt;/p&gt;&lt;p&gt;In this post, I’ll improve performance by adding the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload&quot;&gt;preload technique&lt;/a&gt; to my blog. First, let’s look at what it is.&lt;/p&gt;&lt;h2 id=&quot;preload-basics&quot;&gt;Preload basics&lt;/h2&gt;&lt;p&gt;In a standard web page load, once requested, the server sends over the HTML document as well as numerous response headers too. The HTML is progressively served to the browser, and it is &lt;strong&gt;only&lt;/strong&gt; when the parser encounters the standard &lt;code&gt;&amp;lt;link src=&quot;example.css&quot; rel=&quot;stylesheet&quot;&gt;&lt;/code&gt; link that the browser requests the CSS file from the server. Therefore, best practice is to place this CSS link as close to the top of the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag as you can. This ensures that the browser sees it quickly and thus starts downloading it as soon as it can. But what if you could give the browser a &quot;hint&quot; as to what is coming up in the document? This is where the Preload hint functionality comes in.&lt;/p&gt;&lt;p&gt;The Preload hint is essentially saying to the browser:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I know you&#39;re busy doing other things at the moment, but you should also know that you absolutly &lt;strong&gt;will&lt;/strong&gt; be requiring this file soon in the page load. So stick it at the top of your list to download as soon as you can.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It&#39;s important to realise that this is only a &quot;hint&quot;, not a mandatory instruction. The browser may choose to entirely ignore it, if for example it has already parsed and discovered the file you wish for it to preload. There are 2 ways in which you can implement a preload.&lt;/p&gt;&lt;h3 id=&quot;1-link-in-the-head&quot;&gt;1. Link in the head&lt;/h3&gt;&lt;p&gt;This is probably the easiest way to add a preload to a website. Stick it in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;style.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;main.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;script&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;2-preload-link-header&quot;&gt;2. Preload link header&lt;/h3&gt;&lt;p&gt;This method ensures that the browser is told about what other resources to load along with the HTML document in the form of a response header from the server. The above &quot;Link in the head&quot; functionality looks like this as a response header:&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;Link: &amp;lt;/style.css&gt;; rel=preload; as=style
Link: &amp;lt;/main.js&gt;; rel=preload; as=script&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;OR&lt;/strong&gt;&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;Link: &amp;lt;/style.css&gt;; rel=preload; as=style, &amp;lt;/main.js&gt;; rel=preload; as=script&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Both headers give the exact same functionality, it just comes down to readability. I don&#39;t believe the single line version give any performance advantages, especially when any form of header compression is applied, e.g. &lt;a href=&quot;https://httpwg.org/specs/rfc7541.html&quot;&gt;hpack&lt;/a&gt; for &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/HTTP_2&quot;&gt;HTTP/2&lt;/a&gt; or &lt;a href=&quot;https://datatracker.ietf.org/doc/rfc9204/&quot;&gt;qpack&lt;/a&gt; for &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/HTTP_3&quot;&gt;HTTP/3&lt;/a&gt;. But please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt; if this assumption I&#39;m making isn&#39;t true!&lt;/p&gt;&lt;p&gt;In both instances above, we are telling the browser to preload the page’s CSS and JavaScript because they &lt;strong&gt;will&lt;/strong&gt; be required to render the page. You may notice that I phrased it as “will be required”. This is deliberate because it is far too easy to abuse the preload functionality. If you instruct the browser to preload everything, you will likely harm web performance instead of improving it. So &lt;strong&gt;only&lt;/strong&gt; preload assets that are genuinely needed for the page to render. Otherwise, you risk wasting bandwidth on unnecessary resources and slowing down the rendering process. I know Firefox warns you in the DevTools console if an asset has been preloaded but not used during a certain time period, other browsers may do this as well. So always check your browser console for similar messages.&lt;/p&gt;&lt;h2 id=&quot;preload-and-fingerprinting&quot;&gt;Preload and fingerprinting&lt;/h2&gt;&lt;p&gt;There is a small added challenge when using asset fingerprinting with the preload functionality. Since the filename of the CSS or JavaScript changes completely whenever the file contents change, you cannot simply preload &lt;code&gt;index.css&lt;/code&gt; or &lt;code&gt;main.js&lt;/code&gt;. They will instead be renamed to something like &lt;code&gt;index-362ccd3816.css&lt;/code&gt; or &lt;code&gt;main-2fc0e9cad0.js&lt;/code&gt;. These are just example names, but the important point is that the file names are unpredictable and will change with each build, assuming the content of the files change. Since nobody wants to update a preload reference by hand every time a file changes, this is where a bit of 11ty scripting magic steps in to save the day.&lt;/p&gt;&lt;h2 id=&quot;the-code&quot;&gt;The Code&lt;/h2&gt;&lt;p&gt;In order to roll this functionality into my 11ty build, I created a helper file in my &lt;code&gt;_helpers&lt;/code&gt; directory in the root of my blog. This is called &lt;code&gt;header-generator.js&lt;/code&gt;. Imaginative name, huh! All functionality related to the header generation will be contained within this file.&lt;/p&gt;&lt;p&gt;It is then imported into my &lt;code&gt;eleventy.config.js&lt;/code&gt; like so:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; generatePreloadHeaders &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;./_helpers/header-generator.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now, I only want this code to run in production, and after the 11ty build completes, so I added the following later in the config:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;IsProduction&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;eleventy.after&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; generatePreloadHeaders&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hopefully, this code is fairly self-explanatory. I&#39;m hooking into the &lt;code&gt;eleventy.after&lt;/code&gt; event, which is the point at which my CSS has been Brotli compressed and fingerprinted, and the Link Header is ready to be generated and added to my Cloudflare Pages &lt;code&gt;_headers&lt;/code&gt; file (&lt;a href=&quot;https://developers.cloudflare.com/pages/configuration/headers/&quot;&gt;documentation here&lt;/a&gt;) before the &lt;code&gt;_site&lt;/code&gt; is built. Below is the complete &lt;code&gt;header-generator.js&lt;/code&gt; file I am using, with detailed comments to make it easier to follow:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;235ef1c57213479cd305f88f3389f6cf&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// standard node library imports&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// This script generates preload headers for fingerprinted CSS files in the _site/css directory&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// and adds them to the global section of the _headers file (/*) in the _site directory.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// It prefers Brotli-compressed files (e.g. *.css.br rather than *.css) if available.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generatePreloadHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// Log the start of the process&lt;/span&gt;
	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Generating preload headers for CSS files...&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// This is my CSS directory for my blog&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cssDir &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Check if CSS directory exists&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssDir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;CSS directory not found, skipping header generation&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Find fingerprinted CSS files (both .css and .css.br). We prefer .css.br if available.&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// Fingerprinted files match the pattern index-[hash].css or index-[hash].css.br&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cssFiles &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssDir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Match files like index-b9fcfe85ef.css.br or index-b9fcfe85ef.css&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; file&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;^index-[a-f0-9]{10}&#92;.css(&#92;.br)?$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Nothing found so exit&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssFiles&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;No fingerprinted CSS files found, skipping header generation&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Sort to prefer *.br files over *.css files&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// (compression is done via the zlib library in another helper file)&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// both .css and .css.br files exist in the same folder with the same file hash&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// The hash is generated from the unminified and uncompressed CSS file)&lt;/span&gt;
	cssFiles&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// If a is .br and b is not, a comes first&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.br&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.br&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// If b is .br and a is not, b comes first&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.br&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.br&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Take the first (preferably .br) file&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cssFile &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cssFiles&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Construct the path for the Link header&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cssPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/css/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssFile&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Found CSS file: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssFile&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// Now we need to read the existing _headers file, add the preload header to the global section (/*),&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// and write it back&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Read the source headers file&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sourceHeadersPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./public&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;_headers&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Set our target headers file&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; targetHeadersPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;_headers&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Check if source _headers file exists&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sourceHeadersPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Source _headers file not found&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Read the existing headers content&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; headersContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sourceHeadersPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Create the preload header&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Note: &#39;nopush&#39; prevents Cloudflare from doing HTTP/2 server push&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; preloadHeader &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;  Link: &amp;lt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&gt;; rel=preload; as=style; nopush&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Find the global /* rule and add the preload header to it&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Look for the line that just contains &quot;/*&quot; which is the global section&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; headersContent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; globalSectionIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; nextSectionIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Find the global section (line that starts with just &quot;/*&quot;)&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lines&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/*&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				globalSectionIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Find the next section (line that starts with a path)&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;globalSectionIndex &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalSectionIndex &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lines&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;lines&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;  &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					nextSectionIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// If we found the global section, proceed to add or update the Link header&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;globalSectionIndex &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Check if a Link header already exists in the global section&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; linkHeaderExists &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Iterate through the lines in the global section&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalSectionIndex &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;nextSectionIndex &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; nextSectionIndex&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Check for the existance of the Link header&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lines&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Link:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token comment&quot;&gt;// Replace existing Link header&lt;/span&gt;
					lines&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; preloadHeader&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token comment&quot;&gt;// The Link header exists and has been updated&lt;/span&gt;
					linkHeaderExists &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// If no Link header exists, add one&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;linkHeaderExists&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Find the last header line in the global section&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; lastHeaderIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalSectionIndex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Iterate until the next section or end of file&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalSectionIndex &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;nextSectionIndex &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; nextSectionIndex&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lines&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; lines&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;  &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
						lastHeaderIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Insert the Link header after the last header&lt;/span&gt;
				lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;splice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lastHeaderIndex &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; preloadHeader&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// rejoin the modified _headers file&lt;/span&gt;
			headersContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Could not find global section in _headers file&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Write the updated headers to the _site directory before deployment to Cloudflare pages&lt;/span&gt;
		fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;targetHeadersPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; headersContent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Generated preload header: Link: &amp;lt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&gt;; rel=preload; as=style; nopush&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Error generating preload headers:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For a cleaner version without comments, I’ve uploaded the code to a Gist. &lt;a href=&quot;https://gist.github.com/Nooshu/235ef1c57213479cd305f88f3389f6cf&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Find the code Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;the-cloudflare-headers-file&quot;&gt;The Cloudflare &lt;code&gt;_headers&lt;/code&gt; file&lt;/h2&gt;&lt;p&gt;This setup is currently working really well, the only minor &quot;issue&quot; that doesn&#39;t sit right with me presently is the fact that the Link header sits on the global header path (&lt;code&gt;/*&lt;/code&gt;) in the &lt;code&gt;_headers&lt;/code&gt; file. This means the Link header is added to &lt;strong&gt;all assets&lt;/strong&gt; served from my blog.&lt;/p&gt;&lt;p&gt;As far as I know, this shouldn&#39;t cause any issues, as browsers will just ignore it if on a file type that doesn&#39;t support it. But I would like to rectify this in the future.&lt;/p&gt;&lt;p&gt;In my testing with the Cloudflare &lt;code&gt;_headers&lt;/code&gt; file, once a header is set in Cloudflare Pages it cannot be removed or overwritten. The only &quot;fixes&quot; I’ve found for this are:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Use a response header transform rule in the Cloudflare dashboard to remove the Link header from all other file types served except CSS.&lt;/li&gt;&lt;li&gt;Look into a Cloudflare Workers solution to examine the server responses at &quot;the edge&quot; and remove them that way.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;I will eventually move forward with option 1 as it looks to be the most straight-forward way to do it.&lt;/p&gt;&lt;p&gt;I&#39;ve mentioned this &quot;minor issue&quot; in the blog post, simply to highlight the fact about not being able to remove headers using the &lt;code&gt;_headers&lt;/code&gt; file once they have been set. If anyone knows how to do this using &lt;strong&gt;only&lt;/strong&gt; the &lt;code&gt;_headers&lt;/code&gt; file, please &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;. I’d love to learn how.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;All this is now live on this very blog, so using my custom CSS pipeline with 11ty, I now have the following happening before deployment to live:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;CSS minified and Brotli compressed to level 11 (highest).&lt;/li&gt;&lt;li&gt;CSS Asset fingerprinting to allow for long life &lt;code&gt;cache-control&lt;/code&gt; headers, including &lt;code&gt;immutable&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Preloading of the CSS file to reduce the discovery time and improve page performance.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;On a side note: This is probably one of the fastest (and shortest) blog posts I&#39;ve written in a while! I knew I could do it! 🤣 I hope you found it as enjoyable to read as I did to write. As always, feedback and post corrections are welcome. Spot anything wrong? Please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;02/09/25: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Hack to the Future - Frontend</title>
    <link href="https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/" />
    <updated>2025-08-26T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/</id>
    <content type="html">&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#hack-to-the-future-frontend&quot;&gt;Hack to the Future - Frontend&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#1-introduction&quot;&gt;1. Introduction&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#context&quot;&gt;Context&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#looking-back-at-legacy-practices&quot;&gt;Looking back at &quot;legacy&quot; practices&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#lessons-we-can-apply-today&quot;&gt;Lessons we can apply today&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#2-setting-the-time-circuits-to-the-late-90s&quot;&gt;2. Setting the Time Circuits to the late 90s&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#my-first-website-build&quot;&gt;My first website build&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#the-late-90s-web-landscape&quot;&gt;The late 90s web landscape&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#3-the-early-web-layout-and-design-practices&quot;&gt;3. The Early Web - Layout and Design Practices&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#photoshop-psds-as-the-single-source-of-truth&quot;&gt;Photoshop PSDs as the single source of truth&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#frame-based-layouts&quot;&gt;Frame-Based Layouts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#table-based-layouts&quot;&gt;Table-Based Layouts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#quirks-mode-layouts&quot;&gt;Quirks Mode Layouts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#fixed-width-fonts-for-responsive-text&quot;&gt;Fixed Width Fonts for Responsive Text&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#4-the-plugin-era-flash-and-friends&quot;&gt;4. The Plugin Era – Flash and Friends&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#flash-based-content&quot;&gt;Flash-based content&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#scalable-inman-flash-replacement-sifr&quot;&gt;Scalable Inman Flash Replacement (sIFR)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#cufon&quot;&gt;Cufón&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#gif-text-replacements&quot;&gt;GIF Text Replacements&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#adobe-air&quot;&gt;Adobe AIR&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#yahoo-pipes&quot;&gt;Yahoo Pipes&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#phonegap-apache-cordova&quot;&gt;PhoneGap / Apache Cordova&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#microsoft-silverlight&quot;&gt;Microsoft Silverlight&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#java-applets&quot;&gt;Java Applets&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#5-the-javascript-framework-explosion&quot;&gt;5. The JavaScript Library Explosion&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#dhtml-beginnings-1997&quot;&gt;DHTML Beginnings (1997)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#prototype-js-2005&quot;&gt;Prototype.js (2005)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#script-aculo-us-2005&quot;&gt;Script.aculo.us (2005)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#dojo-toolkit-2005&quot;&gt;Dojo Toolkit (2005)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#yahoo-user-interface-yui-2006&quot;&gt;Yahoo! User Interface (YUI) (2006)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#moo-fx-2005&quot;&gt;moo.fx (2005))&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#mootools-2006&quot;&gt;MooTools (2006)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#jquery-core-2006&quot;&gt;jQuery (core) (2006)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#ext-js-2007&quot;&gt;Ext.js (2007)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#jquery-ui-2007&quot;&gt;jQuery UI (2007)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#angularjs-2010&quot;&gt;AngularJS (2010)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#backbone-2010&quot;&gt;Backbone (2010)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#knockout-2010&quot;&gt;Knockout (2010)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#6-css-workarounds-and-browser-quirks&quot;&gt;6. CSS Workarounds and Browser Quirks&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#old-css-practices&quot;&gt;Old CSS practices&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#sliding-doors-technique&quot;&gt;Sliding Doors Technique&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#image-sprites-for-icons&quot;&gt;Image Sprites for Icons&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#vendor-prefixes-for-css&quot;&gt;Vendor Prefixes for CSS&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#heavy-use-of-important-in-css&quot;&gt;Heavy Use of !important in CSS&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#oldie-hacks&quot;&gt;OldIE hacks&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#doctype-fragility&quot;&gt;DOCTYPE fragility&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#zoom-1-hack&quot;&gt;zoom: 1 hack&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#underscore-hack&quot;&gt;Underscore Hack&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#asterisk-hack&quot;&gt;Asterisk Hack&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#star-html-hack&quot;&gt;Star HTML Hack&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#child-selector-hack&quot;&gt;Child Selector hack&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#double-margin-float-bug&quot;&gt;Double Margin Float Bug&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#peekaboo-bug-fix&quot;&gt;Peekaboo bug fix&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#transparent-png-fix&quot;&gt;Transparent PNG fix&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#lack-of-ie-developer-tools&quot;&gt;Lack of IE Developer Tools&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#ie-conditional-comments&quot;&gt;IE Conditional Comments&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#ie-css-selector-limit&quot;&gt;IE CSS Selector Limit&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#7-markup-of-the-past&quot;&gt;7. Markup of the Past&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#xhtml-1-1-and-2-0&quot;&gt;XHTML 1.1 and 2.0&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#inline-javascript&quot;&gt;Inline JavaScript&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#document-write&quot;&gt;Document.write()&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#fixed-viewport-meta-tags&quot;&gt;Fixed Viewport Meta Tags&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#web-safe-fonts-only-before-font-face&quot;&gt;Web Safe Fonts Only (before @font-face)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#8-tools-and-workflow-relics&quot;&gt;8. Tools and Workflow Relics&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#svn-subversion-largely-replaced-by-git&quot;&gt;SVN (subversion, largely replaced by Git)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#chrome-frame&quot;&gt;Chrome Frame&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#css-resets&quot;&gt;CSS Resets&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#hover-only-interactions&quot;&gt;Hover-Only Interactions&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#9-legacy-web-strategies&quot;&gt;9. Legacy Web Strategies&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#blackhat-seo&quot;&gt;Blackhat SEO&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#above-the-fold-obsession&quot;&gt;&quot;Above the Fold&quot; obsession&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#superseded-compatibility-approaches&quot;&gt;Superseded compatibility approaches&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#modernizr&quot;&gt;Modernizr&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#10-tests-and-standards-of-yesteryear&quot;&gt;10. Tests and Standards of Yesteryear&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#acid2-and-acid3-tests&quot;&gt;Acid2 and Acid3 Tests&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;11. What Still Matters - Progressive Enhancement&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#not-legacy-but-often-forgotten&quot;&gt;Not legacy but often forgotten&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#what-is-progressive-enhancement&quot;&gt;What is Progressive Enhancement&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#html&quot;&gt;HTML&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#css&quot;&gt;CSS&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#javascript&quot;&gt;JavaScript&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#progressive-enhancement-summary&quot;&gt;Progressive Enhancement Summary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#importance-in-government-services&quot;&gt;Importance in government services&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#12-lessons-for-the-future&quot;&gt;12. Lessons for the Future&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#what-these-legacy-practices-teach-us-today&quot;&gt;What these legacy practices teach us today&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#applying-lessons-to-modern-frontend-work&quot;&gt;Applying lessons to modern frontend work&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#post-summary&quot;&gt;13. Post Summary&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;1-introduction&quot;&gt;1. Introduction&lt;/h2&gt;&lt;h3 id=&quot;context&quot;&gt;Context&lt;/h3&gt;&lt;p&gt;So over the last few months at work, I&#39;ve been conducting interviews to hire Frontend Developers for a number of new projects we have in the pipeline. It was only when looking at CV&#39;s that it struck me, a lot of these candidates weren&#39;t even born when I first started in my Web Development career! So I thought maybe developers getting into a Frontend Developer career today, may want to learn a bit about what it was like when I first started (that sentence just makes me feel old! 👴)&lt;/p&gt;&lt;h3 id=&quot;looking-back-at-legacy-practices&quot;&gt;Looking back at “legacy” practices&lt;/h3&gt;&lt;p&gt;Why would we want to look back on legacy best practices on the web? Other than the obvious academic and for general interest reasons?&lt;/p&gt;&lt;p&gt;Studying past best practices and legacy systems is crucial for understanding the evolution of technology and making informed decisions today. By examining the problems old practices were designed to solve, we gain a deeper appreciation for current best practices and avoid repeating past mistakes. As the philosopher &lt;a href=&quot;https://en.wikipedia.org/wiki/George_Santayana&quot;&gt;George Santayana&lt;/a&gt; once said: &lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Those who cannot remember the past are condemned to repeat it.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This historical perspective also reveals enduring principles like &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;progressive enhancement&lt;/a&gt;, which remains vital for creating accessible and resilient systems on the web.&lt;/p&gt;&lt;h3 id=&quot;lessons-we-can-apply-today&quot;&gt;Lessons we can apply today&lt;/h3&gt;&lt;p&gt;For developers, understanding past methodologies is essential for properly maintaining and modernising existing systems in the future without causing critical failures. This historical knowledge will ultimately help them navigate the complexities of older codebases, to ensure they make informed decisions about how to update or replace components. Above all, reflecting on the past can help us come up with creative new ideas and prevent us from blindly following new trends. This perspective also provides a comprehensive view of how the web has evolved, grounding our current practices in a deeper understanding of the technology&#39;s history.&lt;/p&gt;&lt;p&gt;This process of building on past knowledge is a fundamental aspect of human progress. Just as civilizations learn from historical events to avoid repeating mistakes, developers can learn from the successes and failures of past technological eras. It&#39;s how humanity has always evolved. By building upon the accumulated wisdom and experience of those who came before us. By studying the mistakes and triumphs of the past, we improve our own work and contribute to the continuous cycle of innovation and learning that drives our entire industry forward.&lt;/p&gt;&lt;h2 id=&quot;2-setting-the-time-circuits-to-the-late-90s&quot;&gt;2. Setting the Time Circuits to the late 90s&lt;/h2&gt;&lt;h3 id=&quot;my-first-website-build&quot;&gt;My first website build&lt;/h3&gt;&lt;p&gt;In 1998, while working toward my &lt;a href=&quot;https://en.wikipedia.org/wiki/GCSE&quot;&gt;GCSEs&lt;/a&gt;, I became interested in art and design, this was partly thanks to having an art teacher as my form tutor throughout secondary school. That influence, combined with the opportunity to take a double art GCSE for the same effort as a single GCSE, made the choice a pretty easy one! GCSE Art, here I come!&lt;/p&gt;&lt;p&gt;At the same time, I was already immersed in the emerging world of the internet, spending many hours online discovering a passion for many areas of computing and online gaming thanks to &lt;a href=&quot;https://en.wikipedia.org/wiki/Team_Fortress_Classic#Development&quot;&gt;QuakeWorld Team Fortress&lt;/a&gt;, despite the frustration it caused at home by tying up the phone line all hours of the day, oh how I loved my &lt;a href=&quot;https://support.usr.com/support/5668d/5668d-ug/main.htm&quot;&gt;US Robotics 56K modem&lt;/a&gt;, with its 120-150 ping! &lt;a href=&quot;https://en.wikipedia.org/wiki/ISDN&quot;&gt;Integrated Services Digital Network (ISDN)&lt;/a&gt; or any form of broadband was still many years away for most people!&lt;/p&gt;&lt;p&gt;I was never exactly blessed with traditional artistic talent, painting, drawing, all of those art forms just wasn’t my thing. But I spotted an opportunity to combine my love of technology with the art curriculum. Back then, there were only about 2.4 million websites in existence worldwide. Most businesses and schools (including mine), were firmly offline. So, I proposed building a website for my final art project. To my surprise, my art teacher was absolutely thrilled with the idea. It turned out to be a first for the school and, as I later discovered, a first for the entire exam board too. Shock horror: I was ahead of the curve once. The curve has been safely ahead of me ever since.&lt;/p&gt;&lt;p&gt;I ended up creating a website for a fake record label, complete with a dreadful album cover, fictional artist, and made-up discography. Honestly, I wish I still had it! It was gloriously awful! I don’t recall much, but I remember the site used a &lt;code&gt;&amp;lt;frameset&gt;&lt;/code&gt; with three &lt;code&gt;&amp;lt;frame&gt;&lt;/code&gt; elements. The top frame displayed the logo, the left frame held the navigation menu, and the main frame was used for the page content.&lt;/p&gt;&lt;p&gt;The logo, by the way, was crafted in a program called 3D Text Studio (or something similar to that) that churned out spectacularly cheesy animated text &lt;a href=&quot;https://text.media.giphy.com/v1/media/giphy.gif?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJwcm9kLTIwMjAtMDQtMjIiLCJzdHlsZSI6Im5ld2Jvcm4iLCJ0ZXh0IjoiTm9vc2h1LmNvbSIsImlhdCI6MTc0NjMwMTg0OH0.DUY2gb7c0wa8PVD1IvRYCVKUVTxKf_OlqNeSnpF_4ro&quot;&gt;like this&lt;/a&gt;! From a web performance perspective, that single GIF exceeded 2 MB. On a 56K modem, which was the standard connection for most users of the web at the time, that translates to a 6-minute loading time for just that GIF! Fortunately, it was never hosted online and was presented to the examiners directly from my local machine.&lt;/p&gt;&lt;p&gt;Long story short… the examiners loved my little website and I got a double A* Art GCSE for my effort!&lt;/p&gt;&lt;p&gt;So what&#39;s all this preamble leading too? Well, this is just a long-winded way to tell you (again) that I&#39;m old… 😭&lt;/p&gt;&lt;h3 id=&quot;the-late-90s-web-landscape&quot;&gt;The late 90s web landscape&lt;/h3&gt;&lt;p&gt;There have been some things I&#39;ve noticed while questioning candidates in interviews recently, many candidates don&#39;t have the faintest idea of some old methodologies used in the world of Frontend, especially during the &quot;unstable&quot; periods of the web like the late 90s and early 00s:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Browser_wars#First_browser_war_(1995%E2%80%932001)&quot;&gt;first browser war (1995–2001): Internet Explorer vs Netscape Navigator&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Browser_wars#Second_browser_war_(2004%E2%80%932017)&quot;&gt;second browser war (2004–2017): Internet Explorer vs Firefox vs Google Chrome&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Being a Frontend Developer in the late 90s was both fun in terms of innovation, but also exceedingly stressful due to the instability of the web platform! A prime example being cross-browser development. What worked in Netscape, often looked very broken in Internet Explorer (and vice versa)! And if you had clients who were looking for &quot;pixel perfect&quot; designs across all browsers, you were in for a bad time!&lt;/p&gt;&lt;p&gt;Throughout this period, a plethora of methodologies, tools, and workarounds were developed to address deficiencies in the web platform. And that’s what the rest of this post will delve into. Buckle up folks, we are about to time travel to an era when the internet started with the screeching of dial-up noises and I still had brown hair!&lt;/p&gt;&lt;h2 id=&quot;3-the-early-web-layout-and-design-practices&quot;&gt;3. The Early Web – Layout and Design Practices&lt;/h2&gt;&lt;h3 id=&quot;photoshop-psds-as-the-single-source-of-truth&quot;&gt;Photoshop PSDs as the “single source of truth”&lt;/h3&gt;&lt;p&gt;Using Adobe Photoshop Documents (PSD) as a single source of design truth was a very common practice in the early days of web design. This was particularly common when design and development teams were siloed. A designer would create a PSD file that was intended to be precisely what the website would look like in the browser.&lt;/p&gt;&lt;h4 id=&quot;issues&quot;&gt;Issues&lt;/h4&gt;&lt;p&gt;There were no considerations made for page structure, behaviour, and interactions. These fixed layout PSD&#39;s encouraged bad practices like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Fixed page dimensions e.g. 1024px x 768px as a static canvas.&lt;/li&gt;&lt;li&gt;1:1 mapping of Photoshop file to web page, which was rarely achievable, especially given cross-browser inconsistencies with page rendering.&lt;/li&gt;&lt;li&gt;Lack of fluid or responsive design. I realise responsive design wasn&#39;t &quot;a thing&quot; at this time, but could it have been adopted sooner if fixed-width PSD workflows hadn&#39;t ever taken hold?&lt;/li&gt;&lt;li&gt;The technique was more suited to static layouts, like print design, rather than web design.&lt;/li&gt;&lt;li&gt;There were issues tracking interaction states like anchors with hover, active, disabled, and focus.&lt;/li&gt;&lt;li&gt;Dynamic content was difficult to visualise (e.g., the rendering of different lengths of text in the browser).&lt;/li&gt;&lt;li&gt;Poor accessibility adaptations, (e.g., increased font sizes, high-contrast modes weren’t considered in the design files).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The only way to solve many of these issues would be to create multiple PSD&#39;s to hold all these different design assumptions. And in doing so, file management and design revisions would quickly become impractical and prone to being incomplete or inconsistent.&lt;/p&gt;&lt;h4 id=&quot;broken-team-collaboration&quot;&gt;Broken team collaboration&lt;/h4&gt;&lt;p&gt;The use of PSD&#39;s as the single source of truth broke how teams could collaborate and innovate. This was because:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Developers would often have to interpret or translate the PSD design manually without the help of designers (e.g. due to siloed teams and strict job roles).&lt;/li&gt;&lt;li&gt;Changes in the design required round-trips to designers, rather than being evolved collaboratively in code.&lt;/li&gt;&lt;li&gt;Small team bottlenecks were common e.g. all design or development decisions needed to go through individuals rather than a whole team.&lt;/li&gt;&lt;li&gt;Files became outdated rapidly leading to teams working on outdated designs without realising it.&lt;/li&gt;&lt;li&gt;Designers often came up with designs that simply couldn&#39;t be built with the web technologies that existed at the time, especially when their designs were expected to work across different browsers.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;modern-alternatives&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;I&#39;d like to think that designers using Photoshop for modern web design is a thing of the past, given the vast number of tools and techniques that are way more suited to the job than Photoshop ever was. Modern teams typically use:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://m3.material.io/foundations/design-tokens/overview&quot;&gt;Design tokens&lt;/a&gt; and internal component libraries as the &quot;single source of truth&quot;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.figma.com/&quot;&gt;Figma&lt;/a&gt; or similar tools with structured, token-aware components.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://webdesignerdepot.com/how-to-create-a-living-style-guide/&quot;&gt;Living style guides&lt;/a&gt; and code-driven prototypes (e.g., &lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;Clear handoffs between teams using tools like &lt;a href=&quot;https://zeroheight.com/&quot;&gt;zeroheight&lt;/a&gt;, or integrated design-to-dev platforms.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The advantage of using these modern collaboration tools enables design and development teams to share the same language and source of truth, rooted in reusable, well-tested, and accessible components.&lt;/p&gt;&lt;h4 id=&quot;photoshop-psds-summary&quot;&gt;Photoshop PSDs Summary&lt;/h4&gt;&lt;p&gt;In the early days of my frontend career, slicing PSDs was second nature, but that workflow is now obsolete. Using Photoshop as a &quot;single source of truth&quot; leads to siloed teams, rigid layouts, and poor collaboration. It ignores responsiveness, accessibility, and the realities of modern web development. Today, tools like &lt;a href=&quot;https://www.figma.com/&quot;&gt;Figma&lt;/a&gt;, &lt;a href=&quot;https://design-system.service.gov.uk/&quot;&gt;design systems&lt;/a&gt;, and &lt;a href=&quot;https://open-wc.org/guides/community/component-libraries/&quot;&gt;component libraries&lt;/a&gt; enable faster, more inclusive, and collaborative workflows. If you’re still building from PSDs, it’s time to move on! As the web has evolved, it is imperative that we all do the same.&lt;/p&gt;&lt;h3 id=&quot;frame-based-layouts&quot;&gt;Frame-Based Layouts&lt;/h3&gt;&lt;p&gt;The Frame-based layouts were introduced into browsers to solve a specific set of problems. These were:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;To allow static content like navigation menus to remain in place while only the main content of the page gets updated on navigation.&lt;/li&gt;&lt;li&gt;To Reduce the amount of data transferred over the network, since only one part of the page would need to be loaded. This was important at the time as remember in the late 1990s and early 2000s, broadband for most people simply wasn&#39;t available. If you were very lucky (and had the money), you&#39;d be able to get an Integrated Services Digital Network (ISDN) line installed in your home, but it was mostly online businesses that had the money (and justification) for this type of connection, even ISDN wasn’t particularly quick. Adjusted for inflation you&#39;d be looking at £60 to £80 per month for a &lt;strong&gt;0.128 Mbps&lt;/strong&gt; connection!&lt;/li&gt;&lt;li&gt;To simulate a more app-like experience before JavaScript (JS) and CSS became more standardised and mature.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;example&quot;&gt;Example&lt;/h4&gt;&lt;p&gt;For those curious here&#39;s an simple example of an HTML page using frames:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01 Frameset//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/html4/frameset.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Simple Frame Example&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Note no Body element: 2 vertical columns --&gt;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 30% width the menu.html document --&gt;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 70% width for the main content of the page --&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;frameset&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;30%,70%&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;frame&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;menu.html&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;menuFrame&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;frame&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content.html&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;contentFrame&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;frameset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;To use &lt;code&gt;&amp;lt;frame&gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;frameset&gt;&lt;/code&gt; you needed to use a specific HTML 4.01 &lt;strong&gt;Frameset&lt;/strong&gt; DOCTYPE, in the index.html file.&lt;/li&gt;&lt;li&gt;In my example, for a single HTML page you&#39;d have to maintain 3 HTML files (index.html, menu.html, and content.html).&lt;/li&gt;&lt;li&gt;Each frame was like a mini browser window that loaded its own HTML document.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;problems&quot;&gt;Problems&lt;/h4&gt;&lt;p&gt;Unfortunately, there were a number of major issues with Frame-Based Layouts:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Terrible user experience&lt;/strong&gt;: the use and navigation of frames was confusing for users, since you effectively had multiple browser panes in a single page. The URL bar would often remain static even as the content of the page changed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Poor Accessibility&lt;/strong&gt;: Screen readers and other assistive technology struggled to navigate frames, making it incredibly difficult for users with disabilities to understand the page content and overall page structure.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Limited Search Engine Optimisation (SEO) compatibility&lt;/strong&gt;: Even Search engines of the day struggled to understand the index pages built within frames. This lead to poor visibility in search results, as crawlers frequently failed to understand the relationship between the different frames.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Navigation and Browser Compatibility&lt;/strong&gt;: Because the back and forward buttons did not consistently produce the desired results, frames disrupted the navigation history, making it difficult for users to find their way around. The fact that different browser vendors weren&#39;t aligned with how frames should work in browsers lead to cross-browser issues too.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Bad for security&lt;/strong&gt;: Frames allowed for security risks like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Clickjacking&quot;&gt;clickjacking&lt;/a&gt;. This is where an attacker gets a user to interact with a page that contains malicious content without the user even realising. Modern browsers now include &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options&quot;&gt;protections&lt;/a&gt; to stop these types of security issues.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;modern-alternatives-2&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Modern CSS Layouts&lt;/strong&gt;: Flexbox and Grid allow for responsive layouts without compromising navigation, accessibility, and SEO.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Single Page Applications (SPAs)&lt;/strong&gt;: Frameworks like React, Angular, and Vue allow developers to load page content dynamically without the need for full-page reloads. Be careful though, these libraries come with their own inherent issues if not used correctly!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Server-Side Rendering and Partial Updates&lt;/strong&gt;: techniques like &lt;a href=&quot;https://en.wikipedia.org/wiki/Server_Side_Includes&quot;&gt;server-side includes&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/AJAX&quot;&gt;AJAX&lt;/a&gt;, or component-based rendering to update portions of a page efficiently.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;frame-based-summary&quot;&gt;Frame-Based Summary&lt;/h4&gt;&lt;p&gt;As mentioned in the introduction at the start of this post, my first website was built using frames! I sincerely hope you never have to maintain a frame-based website! But given the enormity of the internet, it is almost certain websites exist somewhere out there, having been untouched for decades! If you do come across one remember to take a quick peek at the source code, it&#39;s like looking back in time! They once served a purpose in the early days of the web but are now considered obsolete. Their usage introduced more problems than they solved, and have been replaced with techniques that are more performant, accessible, and maintainable. Any modern website should be using semantic HTML, CSS-based layouts, and &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;progressive enhancement&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;table-based-layouts&quot;&gt;Table-Based Layouts&lt;/h3&gt;&lt;p&gt;In the late 1990s and early 2000s table-based layouts were a common technique for building a web page structure: A simple example of what this would look like is below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/html4/loose.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Table Layout Example&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;!-&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;Very&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;simple&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;CSS&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;modify&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;elements&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;table-based&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;layout&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;
    &lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Arial&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;td&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px solid #ccc&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;.header&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #f2f2f2&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;text-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bold&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;.nav&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #e0e0e0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;vertical-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; top&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;.content&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ffffff&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;vertical-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; top&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;.footer&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #f2f2f2&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;text-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.9em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;table&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;100%&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;cellspacing&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;cellpadding&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Header --&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;tr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;td&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;colspan&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;header&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      My Table-Based Web Page
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;td&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;tr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Body --&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;tr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Navigation --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;td&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;nav&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;#&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Home&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;#&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;About&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;#&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Contact&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;td&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Main Content --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;td&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Welcome&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;This layout uses an HTML table for structure, which was common before CSS-based layouts became standard.&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;td&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;tr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Footer --&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;tr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;td&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;colspan&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;footer&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token entity named-entity&quot; title=&quot;©&quot;&gt;&amp;ampcopy;&lt;/span&gt; 2025 Example Company
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;td&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;tr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;table&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;why-was-it-used&quot;&gt;Why was it used?&lt;/h4&gt;&lt;p&gt;At the time CSS and layout techniques were inconsistent and unstable across browsers. Developers looking for stability in cross-browser rendering turned to tables in order to do this. At the time, tables offered:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Predictable cross-browser rendering&lt;/li&gt;&lt;li&gt;Control over alignment, spacing, and sizing&lt;/li&gt;&lt;li&gt;Ability to nest elements in a grid-like structure&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It was very common to see nested tables and transparent &quot;spacer GIFs&quot; in invisible table cells to control these layouts more precisely. You&#39;d often find logo&#39;s, sidebars, navigations, footer, and content areas all laid out within a deeply nested HTML table in order to achieve the layout and design that was required.&lt;/p&gt;&lt;h4 id=&quot;why-was-it-so-bad&quot;&gt;Why was it so bad?&lt;/h4&gt;&lt;p&gt;The first and hopefully most obvious point is that the &lt;code&gt;&amp;lt;table&gt;&amp;lt;/table&gt;&lt;/code&gt; element was intended for the display of &lt;strong&gt;tabular data&lt;/strong&gt;. The fact that it was used as a workaround for the lack of standardised layout techniques, shows the ingenuity of developers at the time.&lt;/p&gt;&lt;p&gt;Unfortunately, the use of tables for layout came with many considerable downsides, these included:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Semantics&lt;/strong&gt;: As mentioned, tables should represent structured data, not layout. Misusing them confuses assistive technologies and harms accessibility.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Maintainability&lt;/strong&gt;: Table-based layouts are challenging to read, modify, or scale. Small changes often require restructuring entire layouts.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Responsiveness&lt;/strong&gt;: They are rigid and not suited to fluid or responsive design, that was to come a number of years later.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: They delay rendering because browsers need to calculate the entire table layout before painting it to the page.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;is-the-technique-still-used&quot;&gt;Is the technique still used?&lt;/h4&gt;&lt;p&gt;There are some areas where table-based layouts may still be seen:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Legacy code bases that desperately need to be refactored, I can imagine there are many internal systems across the world where table-based layouts are still used. I’d imagine the conversation about modernising goes something like this… &quot;If it still works, why change it?&quot;. Very short-sighted I know!&lt;/li&gt;&lt;li&gt;Table-based layouts are still widely used in emails due to the very limited support for CSS in email clients. It&#39;s not always the lack of support, it&#39;s the fact that many clients simply strip out any CSS in the process of rendering the email HTML.&lt;ul&gt;&lt;li&gt;To give you an example of how bad it still is, from Outlook 2007+, Microsoft switched to &lt;strong&gt;Microsoft Word&lt;/strong&gt; as the HTML rendering engine! And it&#39;s still in use today with Outlook 365! I did my fair share of HTML emails as a Junior Frontend Developer, the internationalised versions were the worst! Using the same table-based layouts for 19+ languages is never going to work well, especially with languages like German with their huge word length! Sorry… rant over!&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;They are often still used in PDF generation tools e.g. data-driven print views: invoices etc.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;modern-alternatives-3&quot;&gt;Modern alternatives&lt;/h4&gt;&lt;p&gt;Modern CSS offers clean, semantic, and powerful layout tools, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout&quot;&gt;Flexbox&lt;/a&gt;&lt;/strong&gt;: One-dimensional layouts (ideal for nav bars, toolbars, etc.)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout&quot;&gt;CSS Grid&lt;/a&gt;&lt;/strong&gt;: Two-dimensional layouts (ideal for full-page layout and complex structures)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries&quot;&gt;Media Queries&lt;/a&gt;&lt;/strong&gt;: Enable responsiveness across devices&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries&quot;&gt;Container Queries&lt;/a&gt;&lt;/strong&gt; (still an emerging technology): Context-aware layout changes.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;table-based-summary&quot;&gt;Table-Based Summary&lt;/h4&gt;&lt;p&gt;Table-based layouts are a throwback to a bygone era, thankfully! The years of building HTML emails has scared me for life! As they were developed during a period in which CSS was inadequate for the task. Developers had to get creative to wrestle with browser quirks, and tables were the go-to workaround. Thankfully, these days, we’ve moved on to semantic HTML and proper CSS that actually does what we need (for webpages anyway). It’s cleaner, more flexible, and maintainable, and way better for accessibility.&lt;/p&gt;&lt;h3 id=&quot;quirks-mode-layouts&quot;&gt;Quirks Mode Layouts&lt;/h3&gt;&lt;p&gt;This topic is covered in more detail later in the blog post, but I’ll briefly mention it here for completeness.&lt;/p&gt;&lt;p&gt;It&#39;s important to realise that Quirks Mode Layouts weren&#39;t only limited to Internet Explorer (IE). It &lt;strong&gt;originated&lt;/strong&gt; with Internet Explorer, but it was &lt;strong&gt;not exclusive&lt;/strong&gt; to IE. Not only that, it later became a cross-browser convention in order to preserve the compatibility with many web pages on the internet. As that&#39;s the primary rule to consider when rolling out any new technology changes on the web. Whatever you do, &quot;&lt;strong&gt;don&#39;t break the web&lt;/strong&gt;!&quot;.&lt;/p&gt;&lt;p&gt;For example, if a vendor released a new browser feature that wasn&#39;t backwards compatible with earlier versions of web pages, then you have a major issue as you&#39;ve just broken the web! I talk about &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#what-did-xhtml-2-0-aim-to-achieve&quot;&gt;XHTML 2.0&lt;/a&gt; later in the post, as it is a prime example of a proposed technology that would have broken the web. This backwards compatibility was the sole purpose of Quirks mode. It gave modern browsers the ability to switch between:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Quirks Mode&lt;/strong&gt;: Mimic pre-standards behaviour. Used for old, non-compliant pages.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Standards Mode&lt;/strong&gt;: Adheres to modern web specifications (W3C and WHATWG standards).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Almost Standards Mode&lt;/strong&gt;: The same as Standards mode only with one exception, table cell line-height rendering. This was to preserve layouts that used inline images inside HTML tables.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;how-were-layouts-triggered&quot;&gt;How were layouts triggered?&lt;/h4&gt;&lt;p&gt;The browser decided which layout mode to use from the list above purely from the DOCTYPE used on the page. For example:&lt;/p&gt;&lt;h5 id=&quot;trigger-quirks-mode&quot;&gt;Trigger Quirks mode&lt;/h5&gt;&lt;p&gt;This DOCTYPE will trigger Quirks mode layout:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It looks valid, but it is missing the system identifier (URL) therefore it is a malformed DOCTYPE so Quirks Mode is triggered. A valid DOCTYPE is given below for comparison:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/html4/loose.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That missing URL in the DOCTYPE is vital. Quirks Mode would also be activated if a page did not have a DOCTYPE or was not identical to the valid DOCTYPE given above &lt;strong&gt;in any way&lt;/strong&gt;. IE even had a really nasty habit of triggering Quirks Mode if any character was output in the page source before the DOCTYPE. This included invisible characters and new lines and line returns too! As you can imagine, it made debugging issues an absolute nightmare!&lt;/p&gt;&lt;h5 id=&quot;almost-standards-mode&quot;&gt;Almost Standards Mode&lt;/h5&gt;&lt;p&gt;The following DOCTYPE&#39;s will trigger Almost Standards Mode:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;HTML 4.01 &lt;strong&gt;Transitional&lt;/strong&gt; (with full system identifier):&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/html4/loose.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;2&quot;&gt;&lt;li&gt;HTML 4.01 &lt;strong&gt;Frameset&lt;/strong&gt; (with full system identifier):&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01 Frameset//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/html4/frameset.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;3&quot;&gt;&lt;li&gt;XHTML 1.0 &lt;strong&gt;Transitional&lt;/strong&gt;:&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;4&quot;&gt;&lt;li&gt;XHTML 1.0 &lt;strong&gt;Frameset&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD XHTML 1.0 Frameset//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h5 id=&quot;standards-mode&quot;&gt;Standards Mode&lt;/h5&gt;&lt;p&gt;And lastly and most importantly for modern web development. This is the DOCTYPE you should be using to trigger standards mode in all modern browsers:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This simplified DOCTYPE was brought in as part of the HTML5 Specification after 6 years of standardisation (2008–2014).&lt;/p&gt;&lt;h6 id=&quot;why-was-this-version-created&quot;&gt;Why was this version created?&lt;/h6&gt;&lt;p&gt;As outlined in all the examples above, previous DOCTYPE versions were:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Long&lt;/li&gt;&lt;li&gt;Error-prone&lt;/li&gt;&lt;li&gt;Required both a public and a system identifier&lt;/li&gt;&lt;li&gt;Affected rendering modes (Quirks, Almost Standards, Standards)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In order to solve these issues this the new DOCTYPE:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;does not reference a Document Type Definition (DTD) as (HTML5 no longer relies on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/SGML&quot;&gt;SGML-based validation&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;only has a single purpose: to trigger Standards Mode in all modern browsers.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;quirks-mode-summary&quot;&gt;Quirks Mode Summary&lt;/h4&gt;&lt;p&gt;As we have discussed above, Quirks Mode wasn&#39;t an IE exclusive layout mode. It was introduced into all browsers in order to &quot;not break the web&quot;. To ensure your website uses Standards Mode, use:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And remember it &lt;strong&gt;must&lt;/strong&gt; be the first characters in the source code on the page!&lt;/p&gt;&lt;h3 id=&quot;iframe-embeds-for-layouts-or-content&quot;&gt;Iframe Embeds for Layouts or Content&lt;/h3&gt;&lt;p&gt;If you&#39;ve already read the &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#frame-based-layouts&quot;&gt;Frame-based Layouts&lt;/a&gt; section above, then this section will be very similar. Although both are now considered legacy techniques, they come with distinct differences.&lt;/p&gt;&lt;h4 id=&quot;frameset&quot;&gt;Frameset&lt;/h4&gt;&lt;p&gt;As I discussed earlier here&#39;s example &lt;code&gt;&amp;lt;frameset&gt;&lt;/code&gt; code:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- column 1 25% width / column 2 75% width --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;frameset&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;25%,75%&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;frame&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;nav.html&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;frame&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;main.html&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;frameset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;The &lt;code&gt;&amp;lt;frameset&gt;&lt;/code&gt; tag completely replaced the &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt; tag and allowed developers to split the browser window into multiple, scrollable, resizable sections.&lt;/li&gt;&lt;li&gt;Each section (&lt;code&gt;&amp;lt;frame&gt;&lt;/code&gt;) loaded a separate HTML document (as seen in the code above).&lt;/li&gt;&lt;li&gt;This technique was intended to use them as a layout structure. e.g. different parts of the User interface (UI) came from different HTML documents.&lt;/li&gt;&lt;li&gt;Navigation in one frame would control the content in another frame.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;inline-frames-iframes&quot;&gt;Inline frames (Iframes)&lt;/h4&gt;&lt;p&gt;These were introduced later in the &lt;a href=&quot;https://www.w3.org/TR/html401/&quot;&gt;HTML 4.01 Transitional specification&lt;/a&gt;. Example &lt;code&gt;&amp;lt;iframe&gt;&lt;/code&gt; code is found below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;iframe&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content.html&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;iframe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You will immediately notice the difference, using an &lt;code&gt;&amp;lt;iframe&gt;&lt;/code&gt; doesn&#39;t replace the &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt; tag, it embeds an external HTML page into the original page.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The usage of iframes was to embed content into other pages, not structure the whole layout.&lt;/li&gt;&lt;li&gt;iframes were used to load external or isolated content within a single, self-contained HTML page.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;user-experience&quot;&gt;User Experience&lt;/h4&gt;&lt;p&gt;Both methods came with issues, but iframes were slightly better but still caused significant problems:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Embeds could be styled and resized, but remained isolated from the parent.&lt;/li&gt;&lt;li&gt;Navigation, SEO, and accessibility were still severely impacted when misused.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;standards-and-browser-support&quot;&gt;Standards and Browser Support&lt;/h4&gt;&lt;p&gt;Framesets were deprecated and completely removed from the &lt;a href=&quot;https://html.spec.whatwg.org/&quot;&gt;HTML5 specification&lt;/a&gt;, modern browsers either no longer support them or support is very limited without the correct DOCTYPE.&lt;/p&gt;&lt;p&gt;Iframes are still a part of the HTML5 specification, but they come with limited use cases, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Secure sandboxing&lt;/li&gt;&lt;li&gt;Third-party embeds&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In fact, the performance of iframe embeds has recently &lt;a href=&quot;https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-loading-attributes&quot;&gt;been improved&lt;/a&gt; by browsers vendors &lt;a href=&quot;https://caniuse.com/loading-lazy-attr&quot;&gt;adding support&lt;/a&gt; for &lt;code&gt;loading=&quot;lazy&quot;&lt;/code&gt; on iframes thus allowing the browser to delay loading until the iframe is in the viewport.&lt;/p&gt;&lt;h4 id=&quot;security&quot;&gt;Security&lt;/h4&gt;&lt;p&gt;Thankfully, iframes are more secure on the modern web thanks to attributes like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/sandbox&quot;&gt;&lt;code&gt;sandbox&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/allow&quot;&gt;&lt;code&gt;allow&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy&quot;&gt;&lt;code&gt;referrerpolicy&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Although the use of iframes still needs careful consideration for content integrity, cross-domain policies, and maintainability. They can still introduce security vulnerabilities such as:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Clickjacking&quot;&gt;Clickjacking&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS&quot;&gt;Cross-site scripting (XSS)&lt;/a&gt; exploits.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS&quot;&gt;Cross-origin resource sharing (CORS)&lt;/a&gt; exploits.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-them&quot;&gt;Can I still use them?&lt;/h4&gt;&lt;p&gt;Yes, iframes are still a part of the HTML5 specification, so you can still use them, but you should try to only use them in specific scenarios where required. For example, when you need to embed third-party content into a website. This is often seen in banking on the modern web, when you are paying for something online. The final step of the transaction frequently loads an iframe from your bank asking to verify your purchase, usually via some form of Multi-factor Authentication (MFA) e.g. SMS, banking app approval on your phone. While iframe embeds aren&#39;t specifically required for &lt;a href=&quot;https://www.pcisecuritystandards.org/&quot;&gt;PCI compliance&lt;/a&gt;. Many online merchants tend to implement them along with &lt;a href=&quot;https://en.wikipedia.org/wiki/3-D_Secure&quot;&gt;3D Secure authentication&lt;/a&gt; to help reduce PCI scope and exposure.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-4&quot;&gt;Modern alternatives&lt;/h4&gt;&lt;p&gt;As mentioned in the &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#frame-based-layouts&quot;&gt;Frame-Based Layouts&lt;/a&gt; section, there are a number of modern alternatives rather than using iframes for page layout, modularisation, and code isolation. These include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Modern CSS Layouts&lt;/strong&gt;: Flexbox and Grid allow for responsive layouts without compromising navigation, accessibility, and SEO.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Single Page Applications (SPAs)&lt;/strong&gt;: Frameworks like React, Angular, and Vue allow developers to load page content dynamically without the need for full-page reloads. Be careful though, these libraries come with their own inherent issues if not used correctly!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Server-Side Rendering and Partial Updates&lt;/strong&gt;: techniques like &lt;a href=&quot;https://en.wikipedia.org/wiki/Server_Side_Includes&quot;&gt;server-side includes&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/AJAX&quot;&gt;AJAX&lt;/a&gt;, or component-based rendering to update portions of a page efficiently.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;iframe-embeds-summary&quot;&gt;Iframe Embeds Summary&lt;/h4&gt;&lt;p&gt;Using iframes for modularisation and isolation is now seen as a legacy approach and should generally be avoided on the modern web. That said, they still have a place for things like third-party embeds. I remember the early days of Facebook marketing, iframes were everywhere and an absolute nightmare to work with, especially when trying to get the sizing right within their UI. Urghh!&lt;/p&gt;&lt;h3 id=&quot;pixel-perfect-design&quot;&gt;Pixel-Perfect Design&lt;/h3&gt;&lt;p&gt;Pixel-perfect design is a now outdated design approach that was once considered the gold-standard in frontend development. The presented UI on the web page was intended to be a pixel-perfect replica of the design mockups.… If that sounds like a ridiculous and totally unworkable strategy, then I can confirm, yes it was!&lt;/p&gt;&lt;h4 id=&quot;what-the-technique-means-in-practice&quot;&gt;What the technique means in practice&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Strict visual fidelity&lt;/strong&gt;: Developers were expected to reproduce every element of a design using the exact measurements, colours and fonts given in a static design file like a Photoshop Document file (PSD).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Close alignment with design tools&lt;/strong&gt;: The priority while using this technique was to match the layout in the design document, no matter what. Inconsistencies across various browsers, not to mention differing screen sizes and user preferences, frequently made this an impossible task.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Used in fixed-resolution environments&lt;/strong&gt;: The technique worked reasonably well in the era of desktop-only websites with very few fixed screen sizes (e.g. 800×600, 1024×768).&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-is-it-considered-legacy&quot;&gt;Why is it considered legacy?&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Responsive design is now standard&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Modern devices now range from the size of watches, all the way up to ultra-wide monitors and TV&#39;s and everything in between. Pixel perfect design fails on both smaller and larger size screens.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Accessibility and user preferences&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Modern Accessibility-first design requires designs to be flexible in terms of layout and scaling. For example, &lt;a href=&quot;https://www.w3.org/TR/WCAG22/#resize-text&quot;&gt;WCAG 2.2 Success Criterion 1.4.4&lt;/a&gt; expects text to be scaled up to 200% without loss of content or functionality. This simply can&#39;t be done when using the pixel-perfect design technique.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Performance and Maintainability&lt;/strong&gt;:&lt;ul&gt;&lt;li&gt;It encourages the use of hardcoded CSS rather than using scalable design systems and components.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Modern Design Tools and Systems&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Tools like &lt;a href=&quot;https://www.figma.com/&quot;&gt;Figma&lt;/a&gt; and &lt;a href=&quot;https://www.sketch.com/&quot;&gt;Sketch&lt;/a&gt; now exist with scalable units, that can create responsive mockups rather than static visuals.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Design Intent Over Exact Replication&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Modern web design focuses on intent and consistency across contexts, prioritising &lt;strong&gt;usability&lt;/strong&gt; and &lt;strong&gt;accessibility&lt;/strong&gt; over pixel-perfect replication by leveraging newly introduced responsive browser technologies.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;I hate to be the bearer of bad news, but no, you can&#39;t. It simply isn&#39;t compatible with the modern web, for all the reasons I&#39;ve given above. You are better off understanding what the technique was trying to achieve and focus on modern-day alternatives.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-5&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;Use of &lt;a href=&quot;https://www.w3.org/community/design-tokens/&quot;&gt;Design tokens&lt;/a&gt; for spacing, colour and typography.&lt;/li&gt;&lt;li&gt;Modern browser layouts like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/grid&quot;&gt;CSS Grid&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout&quot;&gt;Flexbox&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://component.gallery/&quot;&gt;Component libraries&lt;/a&gt; and &lt;a href=&quot;https://designsystemsrepo.com/design-systems/&quot;&gt;Design Systems&lt;/a&gt; that abstract repeated patterns.&lt;/li&gt;&lt;li&gt;Focus on the user content, not the dimensions of the UI viewing it.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;pixel-perfect-summary&quot;&gt;Pixel-Perfect Summary&lt;/h4&gt;&lt;p&gt;Pixel-perfect design was always a terrible design technique that set everyone up to fail with the expectation of pixel accurate designs across browsers and devices. It served its purpose when the web was a lot simpler than it is today. A pixel-perfect design on today&#39;s web is a clear indication of an outdated aesthetic, urgently requiring a modern overhaul.&lt;/p&gt;&lt;h3 id=&quot;fixed-pixel-layouts&quot;&gt;Fixed Pixel Layouts&lt;/h3&gt;&lt;p&gt;There&#39;s a reason why I&#39;ve mentioned Fixed Pixel layouts directly after Pixel-Perfect Design. They are related, but have differing concepts. They both emerged in an early era of web design, but they serve different purposes.&lt;/p&gt;&lt;h4 id=&quot;pixel-perfect-design-2&quot;&gt;Pixel-perfect Design&lt;/h4&gt;&lt;p&gt;As you will have just read above, pixel-perfect design emphasises the precise implementation of a design on a website. The output of the website should be identical to the design, down to every pixel. For this reason, it is a &lt;strong&gt;philosophy&lt;/strong&gt;, not a &lt;strong&gt;layout strategy&lt;/strong&gt;.&lt;/p&gt;&lt;h4 id=&quot;fixed-pixel-layouts-2&quot;&gt;Fixed Pixel Layouts&lt;/h4&gt;&lt;p&gt;Fixed pixel layouts are a &lt;strong&gt;layout strategy&lt;/strong&gt; where all widths, heights, and positions are defined using fixed pixel values (e.g. &lt;code&gt;width: 1024px&lt;/code&gt;). This means it the page layout doesn&#39;t adapt to different screen sizes or resolutions. They are rigid and non-responsive, and are optimised for a single resolution or screen size. Because of this Fixed Pixel Layouts break on small screen sizes (like mobiles), or large displays. They are very much associated with older websites and legacy intranet tools. If you look at early websites on the web they would often say &quot;Best viewed with Internet Explorer (or Netscape Navigator) at 800×600 (or 1024×768)&quot;. These are prime examples of where Fixed Pixel layouts were used.&lt;/p&gt;&lt;h4 id=&quot;fixed-pixel-summary&quot;&gt;Fixed Pixel Summary&lt;/h4&gt;&lt;p&gt;Fixed pixel layouts and pixel-perfect design are outdated. They are relics from a time when monitor sizes were uniform and browser zooming was uncommon. Pixel-perfect demands you match the design mockup exactly, which usually ends in fragile CSS and breaks the moment someone dares to increase their font size or use a high contrast mode. Fixed pixel layouts take it even further by hard-coding dimensions, making sites fall apart on anything that isn’t a desktop from 2010. Adding to their drawbacks, neither option integrates well with modern accessibility standards, responsive design principles, or current web usage patterns.&lt;/p&gt;&lt;h3 id=&quot;css-floats-for-layout&quot;&gt;CSS Floats for Layout&lt;/h3&gt;&lt;p&gt;CSS floats were my go-to layout technique for many years, simply because there really wasn&#39;t a viable cross-browser alternative (other than &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#table-based-layouts&quot;&gt;Tables&lt;/a&gt;, which I covered earlier in the post). Floats were originally intended to be used for wrapping text around images on a page. They weren&#39;t intended to be used for full-page layouts. At the time, designers and developers did this as a creative way to make certain layouts, not because that was the design goal of the CSS specifications.&lt;/p&gt;&lt;h4 id=&quot;common-issues&quot;&gt;Common issues&lt;/h4&gt;&lt;p&gt;Float-based layouts came with a major issue: fragile designs often required “clearfix” hacks to stop float containers from collapsing.&lt;/p&gt;&lt;h5 id=&quot;example-clearfix-hacks&quot;&gt;Example clearfix hacks&lt;/h5&gt;&lt;p&gt;Here are a few example hacks that ensured the float container would wrap around the floated elements:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Modern (Recommended)&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.clearfix::after&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; table&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; both&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Usage&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;clearfix container&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-css css value&quot;&gt;&lt;span class=&quot;token property&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; left&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Left&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-css css value&quot;&gt;&lt;span class=&quot;token property&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; right&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Right&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Overflow Hidden (Quick Fix)&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; hidden&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This was my personal go to for &quot;clearfix&quot; as it was so simple to add to the CSS. A nasty downside of this method was it would hide any content that needed to overflow outside the float container, so its usage really depended on the design requirements.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Float the Container Itself (Not Recommended)&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; left&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I don&#39;t remember really using this method simply because it could interfere with other layout elements on a page and could create unexpected layout shifts during page rendering.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Using display: flow-root (Modern, Clean Alternative)&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flow-root&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is far too modern for me, it didn&#39;t exist when I was still building UI&#39;s regularly. As, it has been stable in major browsers since &lt;a href=&quot;https://caniuse.com/flow-root&quot;&gt;January 2020&lt;/a&gt;. The advantage of this method is no pseudo-elements or hacks required. This is recommended for use if you are modernising code and want the cleanest approach without adding extra markup. Other float-based issues include:&lt;/p&gt;&lt;ol start=&quot;2&quot;&gt;&lt;li&gt;&lt;strong&gt;Poor readability&lt;/strong&gt;: Additional markup was required just for the container clearfix hacks. This also impacted maintainability too.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Inconsistent behaviour&lt;/strong&gt;: Floats tended to behave differently depending on the browser in use, although not as problematic as it was, it still requires more testing to ensure cross-browser UI consistency.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Stacking issues&lt;/strong&gt;: Aligning text, or centring horizontally and vertically, is &lt;a href=&quot;https://css-tricks.com/centering-css-complete-guide/&quot;&gt;non-trivial&lt;/a&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;modern-alternatives-6&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;As with most of the topics in this section of the post, there are a list of modern alternatives available for float-based layouts.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout&quot;&gt;Flexbox&lt;/a&gt;&lt;/strong&gt;: One-dimensional layouts (ideal for nav bars, toolbars, etc).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout&quot;&gt;CSS Grid&lt;/a&gt;&lt;/strong&gt;: Two-dimensional layouts (ideal for full-page layout and complex structures).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries&quot;&gt;Media Queries&lt;/a&gt;&lt;/strong&gt;: Enable responsiveness across devices&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries&quot;&gt;Container Queries&lt;/a&gt;&lt;/strong&gt; (still an emerging technology): Context-aware layout changes.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;performance-and-maintainability&quot;&gt;Performance and Maintainability&lt;/h4&gt;&lt;p&gt;Modern layout techniques:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Lead to cleaner and more maintainable code.&lt;/li&gt;&lt;li&gt;Reduce reliance on utility classes for clearing float-specific bugs.&lt;/li&gt;&lt;li&gt;Enhance responsiveness and accessibility by ensuring that the HTML code&#39;s structure is more predictable.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;legacy-support-considerations&quot;&gt;Legacy Support Considerations&lt;/h4&gt;&lt;p&gt;Floats still appear in legacy code, so understanding them remains important for maintenance and modernisation. However, they should be avoided in greenfield projects unless strictly necessary (I&#39;m open to suggestions if anyone has an idea as to what this necessity might be).&lt;/p&gt;&lt;h4 id=&quot;css-floats-summary&quot;&gt;CSS Floats Summary&lt;/h4&gt;&lt;p&gt;Using CSS floats for layout is now considered an anti-pattern. While historically important, floats have been replaced with modern layout systems like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout&quot;&gt;Flexbox&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout&quot;&gt;Grid&lt;/a&gt;, which offer cleaner, more maintainable, and more powerful solutions. In the future as the web platform evolves, newer layout techniques such as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid&quot;&gt;CSS Subgrid&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries&quot;&gt;Container Queries&lt;/a&gt;, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning&quot;&gt;Anchor Positioning&lt;/a&gt; are also progressing through standardisation and will further improve layout flexibility. Avoiding floats is a key best practice when building or modernising frontend architecture.&lt;/p&gt;&lt;h3 id=&quot;faux-columns&quot;&gt;Faux Columns&lt;/h3&gt;&lt;p&gt;I&#39;m not sure why the French word &quot;Faux&quot; was chosen for this technique, rather than just &quot;false&quot; or &quot;fake&quot;, maybe to make it sound more appealing? Or more complex than it actually is? The term works though, as in English, it is used to describe something made to look like something else, which is precisely what this technique did.&lt;/p&gt;&lt;h4 id=&quot;what-is-it&quot;&gt;What is it?&lt;/h4&gt;&lt;p&gt;In the early days of CSS there was no reliable way to make two or more columns stretch to the same height when the content length in the columns varied. This was because floats or &lt;code&gt;inline-block&lt;/code&gt; elements don&#39;t align their heights, so developers looked for a workaround.&lt;/p&gt;&lt;h4 id=&quot;how-it-worked&quot;&gt;How it worked&lt;/h4&gt;&lt;p&gt;It was a clever workaround actually, it typically worked like this:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;A background image was applied to the &lt;strong&gt;parent container&lt;/strong&gt; of the floated columns (that designers wanted to look to be equal in height)&lt;/li&gt;&lt;li&gt;This background image was often a vertical gradient or a solid block of colour. This image was then repeated (&lt;code&gt;repeat-y&lt;/code&gt;) vertically down the parent container, giving the illusion (&quot;fake&quot;) that the columns were of equal height.&lt;/li&gt;&lt;li&gt;As each of the inner floated elements extended due to varying content within, the container background image extended with it. Very clever, huh!&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The CSS for this &quot;workaround&quot; was as simple as this:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* container with the background repeat vertically */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&#39;faux-columns.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; repeat-y&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/* Note the width of this column, it is important */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.left-column&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; left&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/* Use an identical margin to the width, to &quot;push&quot; the right column into place next to the left column */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.right-column&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin-left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The background image of the container would have 2 sections, one colour for the left column and another for the right column, thus tricking a users eyes into seeing the columns having equal heights!&lt;/p&gt;&lt;p&gt;Check out the &lt;a href=&quot;https://alistapart.com/article/fauxcolumns/&quot;&gt;Faux Columns A List Apart (ALA) article by Dan Cederholm from January 2004&lt;/a&gt;, if you are still unsure how it works, he explained it a lot better than I have!&lt;/p&gt;&lt;h4 id=&quot;limitations&quot;&gt;Limitations&lt;/h4&gt;&lt;p&gt;Unfortunately, as with all of these early CSS workarounds, there were limitations. The faux columns technique was no different. These limitations included:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Not adaptable&lt;/strong&gt;: it required the background image to perfectly match the layout dimensions of the container, and it&#39;s inner &quot;columns&quot;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Unresponsive&lt;/strong&gt;: Exactly what the word says, this only worked for fixed layouts, fluid of more dynamic layouts simply broke the illusion.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;: The technique was difficult to maintain, since any layout change required editing the CSS, background image, or both.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Poor semantics&lt;/strong&gt;: Yes, it solved the &lt;em&gt;visual&lt;/em&gt; presentation problem, but the underlying code wasn&#39;t semantic, the additional &lt;code&gt;&amp;lt;div&gt;&lt;/code&gt;&#39;s just for layout purposes, inherently held no semantic meaning.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;modern-alternatives-7&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;As you would expect, there are modern alternatives that make this layout trivial:&lt;/p&gt;&lt;h5 id=&quot;flexbox&quot;&gt;Flexbox&lt;/h5&gt;&lt;p&gt;With Flexbox, it really is this simple:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.left-column, .right-column&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;flex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h5 id=&quot;css-grid&quot;&gt;CSS Grid&lt;/h5&gt;&lt;p&gt;In CSS grid, it&#39;s even easier as all child elements in a row are explicitly defined and align by default, no extra CSS required:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;2&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1fr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* 1fr = 1 fraction unit */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;can-i-still-use-it-2&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Errr, silly question, no not at all! Look how effortless the modern alternatives above are! Imagine how good it feels to rip out the legacy faux column code from a legacy codebase, and replace it with 1 or 2 lines of CSS!&lt;/p&gt;&lt;h4 id=&quot;faux-columns-summary&quot;&gt;Faux Columns Summary&lt;/h4&gt;&lt;p&gt;The Faux Columns technique was one of those clever hacks we leaned on back when CSS didn’t give designers and developers much to work with. It did the job, but it was fragile and fiddly, and you were always one layout change away from breaking it. These days, it’s more of a historical curiosity. Flexbox and Grid have long since made it obsolete, and with newer tools like Subgrid and Container Queries coming through the standards process, we’ve moved on from trickery to browser tools that are actually built for layout.&lt;/p&gt;&lt;h3 id=&quot;zoom-layouts-using-css-zoom-for-responsiveness&quot;&gt;Zoom Layouts (using CSS zoom for responsiveness)&lt;/h3&gt;&lt;p&gt;Back when responsive design was first emerging, a technique called Zoom Layouts emerged in order to scale whole elements within a UI. This technique emerged because responsive CSS layout techniques were very limited.&lt;/p&gt;&lt;h4 id=&quot;example-2&quot;&gt;Example&lt;/h4&gt;&lt;p&gt;An simple example of this is given below:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.8&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This CSS is easy to understand, it simply scales the entire &lt;code&gt;.container&lt;/code&gt; by 80%.&lt;/p&gt;&lt;h4 id=&quot;when-was-it-useful&quot;&gt;When was it useful?&lt;/h4&gt;&lt;p&gt;This technique was useful when you needed to shrink or enlarge an entire layout without refactoring a fixed-width design. It also came in handy when working with legacy layouts that could not adapt with fluid widths or media queries. Lastly, it was used as a workaround before widespread browser support for the &lt;code&gt;transform: scale()&lt;/code&gt; CSS property or &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#relative_length_units&quot;&gt;relative units&lt;/a&gt; like rem, em, %, vw, and vh.&lt;/p&gt;&lt;h4 id=&quot;why-is-it-outdated&quot;&gt;Why is it outdated?&lt;/h4&gt;&lt;p&gt;There are a number of reasons as to why the Zoom Layout technique is now outdated. These include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;zoom&lt;/code&gt; is non-standard and inconsistent&lt;/strong&gt;: The &lt;code&gt;zoom&lt;/code&gt; property isn&#39;t a part of any official CSS specification. It is a proprietary feature initially supported by Internet Explorer, then later Chromium-based browsers. Interestingly, Firefox and Safari have never supported the &lt;code&gt;zoom&lt;/code&gt; property, making cross-browser layouts using the technique very tricky.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Causes Accessibility issues&lt;/strong&gt;: &lt;code&gt;zoom&lt;/code&gt; does affect the layout scaling, but it doesn&#39;t interact well with user-initiated zoom or accessibility scaling preferences. Thus, using this technique can create barriers for users with visual impalements that rely on native browser zooming or OS-level zooming tools.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Breaks layout semantics&lt;/strong&gt;: zoomed elements don&#39;t always reflow correctly, for example text can reflow outside its container, images can become blurry, and form elements may not align correctly when scaled.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Modern CSS has better solutions&lt;/strong&gt;: As with most outdated techniques in this post, modern browsers now support much better layout techniques and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#relative_length_units&quot;&gt;relative units&lt;/a&gt;, that make responsive design much more consistent and easier to maintain. These include Flexbox, CSS Grid and rem, em, %, vw, vh units. Along with Media Queries and container queries, this gives developers the ability to adapt individual elements proportionally, rather than resorting to scaling the entire UI.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Performance issues&lt;/strong&gt;: The use of &lt;code&gt;zoom&lt;/code&gt; can cause serious performance issues, especially on low-powered devices since the rescaling causes the browser to scale rasterised layers rather than reflow content natively, which increases UI repaint costs.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-it-3&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Seriously, only if you hate your users and love additional maintenance. In practical terms, using it would not be a responsible choice; avoid it. If you come across a critical legacy site using this approach, plan to refactor it with modern techniques. Build your layouts using CSS Grid or Flexbox for flexibility across breakpoints, implement fluid typography with clamp and viewport units, adopt container queries for component-level responsiveness, rely on viewport-based units for consistency, and always test with browser zoom and assistive technologies to ensure accessibility and adaptability for all users.&lt;/p&gt;&lt;h4 id=&quot;zoom-layouts-summary&quot;&gt;Zoom Layouts Summary&lt;/h4&gt;&lt;p&gt;Using &lt;code&gt;zoom&lt;/code&gt; for layout responsiveness is an outdated, non-standard technique that can compromise accessibility, compatibility, and performance. Modern responsive design principles provide far more robust, scalable, and accessible solutions.&lt;/p&gt;&lt;p&gt;If you require a transition approach for legacy systems still using Zoom Layouts, consider refactoring incrementally to CSS Grid and Flexbox combined with relative units like rem or percentages to modernise their responsiveness. Luckily for you, this isn’t the last time you’ll hear about the infamous proprietary &lt;code&gt;zoom&lt;/code&gt; property, as it makes quite a few appearances later in the blog post when we dive into those classic IE layout quirks.&lt;/p&gt;&lt;h3 id=&quot;nested-ems-instead-of-pixels&quot;&gt;Nested ems instead of Pixels&lt;/h3&gt;&lt;p&gt;Before the CSS &lt;code&gt;rem&lt;/code&gt; unit was added to the &lt;a href=&quot;https://www.w3.org/TR/css-values-3/&quot;&gt;CSS Values and Units Module Level 3&lt;/a&gt;, developers used the &lt;code&gt;em&lt;/code&gt; unit as a responsive strategy to avoid fixed pixel font sizes. Having used it for years I can confirm it was a real pain in the ass to work with (pardon my French!). When using an &lt;code&gt;em&lt;/code&gt; unit, both the CSS font size and spacing were sized relative to their immediate parent container.&lt;/p&gt;&lt;p&gt;For example, given this HTML:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;container&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;child&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Some text here&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And this CSS:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.2em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* relative to body */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.child&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.2em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* relative to .container, so compounded */&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Can you guess what the &lt;code&gt;.child&lt;/code&gt; font size of the text is in pixels?&lt;/p&gt;&lt;p&gt;Better get your Math(s) hat on, let&#39;s go through it!&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Default body font size = 16px so 1em x 16px = 16px. The &lt;code&gt;.container&lt;/code&gt; DIV is relative to the body font size so: &lt;code&gt;.container&lt;/code&gt; font size = 1.2em x 16px = 19.2px. The &lt;code&gt;.child&lt;/code&gt; paragraph is relative to the &lt;code&gt;.container&lt;/code&gt; font size so: &lt;code&gt;.child&lt;/code&gt; font size = 1.2em x 19.2px = &lt;strong&gt;23.04px&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;That&#39;s right, that well-known font size &lt;strong&gt;23.04px&lt;/strong&gt;!&lt;/p&gt;&lt;p&gt;Now this is just a very basic example, imagine if you include &lt;code&gt;em&lt;/code&gt; units for margins and paddings too! And also layer on additional nesting! Hopefully, you are starting to realise how painful &lt;code&gt;em&lt;/code&gt; units were to use on a website, especially when the only viable alternatives were percentages (which had the same relative nesting issue and were even less intuitive to use than &lt;code&gt;em&lt;/code&gt;), or CSS keywords e.g. &lt;code&gt;font-size: small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large&lt;/code&gt;, &lt;code&gt;x-large&lt;/code&gt;, etc. As you can see, there weren’t a lot of viable or maintainable options in terms of responsive typography and spacing in the early responsive design era (around 2010-2013).&lt;/p&gt;&lt;h4 id=&quot;why-its-outdated&quot;&gt;Why it&#39;s outdated?&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Complexity and unpredictability&lt;/strong&gt;: Nested ems lead to compounded calculations as we saw in the simple example I gave above, making sizing unpredictable in deeply nested components. A small change in a parent font size cascades unexpectedly and could completely obliterate your well-crafted layout.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Maintenance overhead&lt;/strong&gt;: Adjusting layouts or typography with nested ems quickly creates brittle CSS and significant technical debt, especially when ems are used for spacing like margins and padding.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Inconsistent UI scales&lt;/strong&gt;: Components may render differently in different contexts if they rely on em units, especially in large applications with diverse layout containers.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;modern-alternatives-8&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;You can utilise several modern options for nested &lt;code&gt;em&lt;/code&gt; units. These include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.a11y-collective.com/blog/what-is-rem-in-css/&quot;&gt;rem units&lt;/a&gt; for consistent global scaling relative to the root font size&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/clamp&quot;&gt;Clamp-based&lt;/a&gt; fluid typography for responsive design, for example &lt;a href=&quot;https://utopia.fyi/&quot;&gt;Utopia.fyi&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties&quot;&gt;CSS custom properties&lt;/a&gt; (variables) for consistent, maintainable scales&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-use-them-today&quot;&gt;Can I use them today?&lt;/h4&gt;&lt;p&gt;You could, but I have no idea why you would! When more viable alternatives exist today like &lt;code&gt;rem&lt;/code&gt; units for global scaling, &lt;code&gt;clamp&lt;/code&gt; for fluid typography, and CSS variables for maintainable scales, why make life harder than it needs to be??&lt;/p&gt;&lt;h4 id=&quot;nested-ems-summary&quot;&gt;Nested ems Summary&lt;/h4&gt;&lt;p&gt;Using nested &lt;code&gt;em&lt;/code&gt; units is outdated. It adds unnecessary complexity and unpredictability. For modern responsive design you are far better off using rems for consistent global scaling, or taking advantage of the &lt;code&gt;clamp&lt;/code&gt; CSS function if you are feeling adventurous. Lastly, you could always use modern CSS variables for more consistent and maintainable code.&lt;/p&gt;&lt;h3 id=&quot;setting-the-browsers-base-font-size-to-62-5&quot;&gt;Setting the browsers base font size to 62.5%&lt;/h3&gt;&lt;p&gt;As a direct follow on from the nested &lt;code&gt;em&lt;/code&gt; technique earlier in the post, there was an alternative that developers came up with to simplify the math(s) behind percentages (since they had the same &quot;relative to parent&quot; issue as &lt;code&gt;em&lt;/code&gt;&#39;s). When developers decided to use percentages for fonts, they often set the font size on the &lt;code&gt;&amp;lt;html&gt;&lt;/code&gt; element to:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 62.5%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* default font size now 10px not 16px, due to scaling making `em` units easier to work with (base-10 rather than base-16) */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.6em &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/*16px*/&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2.4em &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/*24px*/&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 3.6em &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/*36px*/&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This avoided complicated fractional calculations when using &lt;code&gt;em&lt;/code&gt; units:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Without the percentage: 1em = 16px → 24px = 1.5em.&lt;/li&gt;&lt;li&gt;With the percentage: 1em = 10px → 24px = 2.4em.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You still had the problem with nested elements, but that was later fixed by using &lt;code&gt;rem&lt;/code&gt; units (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#ems_and_rems&quot;&gt;root em&lt;/a&gt;).&lt;/p&gt;&lt;h4 id=&quot;why-are-these-techniques-less-common-today&quot;&gt;Why are these techniques less common today?&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;It overrides user defaults&lt;/strong&gt;: Some users may increase their base font size from 16px for accessibility reasons, hard-coding the base size to 62.5% undermines this user preference.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Modern teams work with &lt;code&gt;rem&lt;/code&gt;&lt;/strong&gt;: Most developers and teams now accept that 1rem = 16px and use &lt;a href=&quot;https://www.uxpin.com/studio/blog/what-are-design-tokens/&quot;&gt;design tokens&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties&quot;&gt;variables&lt;/a&gt;, or a spacing scale instead of forcing a base-10 (62.5% hack) mental model.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Simplicity from Modern tooling&lt;/strong&gt;: &lt;a href=&quot;https://www.designsystems.com/&quot;&gt;Design systems&lt;/a&gt;, &lt;a href=&quot;https://automaticcss.com/docs/utility-classes-what-why/&quot;&gt;utility classes&lt;/a&gt;, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties&quot;&gt;CSS variables&lt;/a&gt; handle sizing scales more predictably without the 62.5% hack.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it-today&quot;&gt;Can I still use it today?&lt;/h4&gt;&lt;p&gt;No, not really, mainly due to the list of reasons I&#39;ve given above. &lt;code&gt;font-size: 62.5%&lt;/code&gt; was merely a developer convenience hack to make 1em / 1rem equal 10px for easy math(s). Look at the short list of modern alternatives I have listed above instead.&lt;/p&gt;&lt;h4 id=&quot;base-font-size-summary&quot;&gt;Base font size Summary&lt;/h4&gt;&lt;p&gt;As mentioned above, this math(s) hack for easier font sizing is no longer required on the modern web, in fact it should be avoided due to the impact it has on users who change their base font size for accessibility reasons. Look to use one of the more modern techniques mentioned in the &quot;Why are these techniques less common today?&quot; section above.&lt;/p&gt;&lt;h3 id=&quot;fixed-width-fonts-for-responsive-text&quot;&gt;Fixed-Width Fonts for Responsive Text&lt;/h3&gt;&lt;p&gt;Fixed-width fonts are better known as monospaced fonts, they allocate the same horizontal spacing to each character. For example:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.mono-spaced-font&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Courier New&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;Courier&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;Lucida Sans Typewriter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;Lucida Typewriter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;monospace&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The example provided above demonstrates how to render text in a monospaced font and concurrently defines a monospaced font stack for most web page implementations. The reason I say &quot;most&quot; is because Windows has a 99.73% support for &lt;code&gt;Courier New&lt;/code&gt; and OSX has 95.68% support according to &lt;a href=&quot;https://www.cssfontstack.com/Courier-New&quot;&gt;CSSFontStack&lt;/a&gt;. Which is why it is listed first in the font stack, for the less than 1% of users that don’t support it, the browser will look for Courier and so on, until it gets to the end of the font stack, where it just tells the browser to use &lt;em&gt;any&lt;/em&gt; monospace font the system has available.&lt;/p&gt;&lt;p&gt;Historically, monospaced fonts were used for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Terminal emulation.&lt;/li&gt;&lt;li&gt;Code editors for alignment.&lt;/li&gt;&lt;li&gt;Early web design, where the layout predictability was prioritised over aesthetics or responsiveness.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-was-the-technique-used-in-responsive-text&quot;&gt;Why was the technique used in responsive text?&lt;/h4&gt;&lt;p&gt;Developers and designers struggled with the web platform&#39;s limitations at the time due to a lack of suitable tools. So monospaced text was usually used for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Consistent character spacing across browsers.&lt;/li&gt;&lt;li&gt;Easier text alignment in table-based layouts.&lt;/li&gt;&lt;li&gt;Simplifying calculations for layout sizing, since browser layout strategies were much less mature than they are today.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-is-the-technique-outdated&quot;&gt;Why is the technique outdated?&lt;/h4&gt;&lt;p&gt;The technique is now considered outdated for various reasons, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It limits design flexibility. Modern responsive design has moved on from fixed typography, as fluid typography is now possible, which is better served by proportional fonts that adapt visually to varying screen sizes and reading contexts.&lt;/li&gt;&lt;li&gt;Monospaced fonts are harder to read, especially for paragraphs or long text blocks. This requirement on the modern web is critical for accessibility-focused design.&lt;/li&gt;&lt;li&gt;Instead of outdated methods, modern CSS offers enhanced tools and support for contemporary layout techniques. Flexbox and CSS Grid, coupled with various typography scaling units like &lt;code&gt;rem&lt;/code&gt;, &lt;code&gt;em&lt;/code&gt;, &lt;code&gt;vw&lt;/code&gt;, &lt;code&gt;vh&lt;/code&gt;, and &lt;code&gt;clamp()&lt;/code&gt;, enable more predictable and reliable layout control.&lt;/li&gt;&lt;li&gt;There&#39;s no performance difference between modern proportional fonts and monospaced fonts, they both have similar browser overhead, so why choose a technique that is harder to maintain and comes with a whole host of other disadvantages?&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;whats-a-modern-replacement&quot;&gt;What&#39;s a modern replacement?&lt;/h4&gt;&lt;p&gt;There are a number of modern alternatives, some of which we touched on above. These include the use of:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Fluid typography with CSS &lt;code&gt;clamp()&lt;/code&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#relative_length_units&quot;&gt;viewport units&lt;/a&gt; to ensure text scales responsively across devices.&lt;/li&gt;&lt;li&gt;Proportional fonts with &lt;a href=&quot;https://www.cssfontstack.com/&quot;&gt;font fallback stacks&lt;/a&gt; to optimise readability and layout adaptability.&lt;/li&gt;&lt;li&gt;Only using monospaced fonts for semantic or functional reasons, not aesthetics. Code blocks and tabular data are prime examples of where monospaced fonts should be used to enhance readability of these certain areas of a website. Adventurous designers can even transform a web UI into a retro Terminal window with these elements, though readability must be carefully considered.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-it-4&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;As stated repeatedly in this section of the blog post, while technically possible, it would be highly illogical to employ this technique. Given the numerous disadvantages outlined earlier in this section, utilising such an antiquated method on the modern web would be ill-advised.&lt;/p&gt;&lt;h4 id=&quot;fixed-width-fonts-summary&quot;&gt;Fixed-Width Fonts Summary&lt;/h4&gt;&lt;p&gt;There are so many font options available to developers and designers today. There is no way you should ever use a monospaced font for anything besides sections of code, or possibly text in a data table, depending on what type of data you are wishing to display. In both of these cases, a monospaced font can enhance readability if used correctly. &lt;/p&gt;&lt;h2 id=&quot;4-the-plugin-era-flash-and-friends&quot;&gt;4. The Plugin Era – Flash and Friends&lt;/h2&gt;&lt;h3 id=&quot;flash-based-content&quot;&gt;Flash-Based Content&lt;/h3&gt;&lt;p&gt;I distinctly remember having a conversation with a then colleague regarding iOS not supporting Flash content and how it was the beginning of the end of Adobe Flash (Flash) on the web. At the time he refused to believe it, but thankfully for the web, my prediction came true!&lt;/p&gt;&lt;h4 id=&quot;what-was-flash-based-content&quot;&gt;What was Flash-based content?&lt;/h4&gt;&lt;p&gt;Flash was a proprietary multimedia software platform developed by Adobe. It was used to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Deliver animations, video, and interactive content via a plugin in the web browser.&lt;/li&gt;&lt;li&gt;Enable rich media applications embedded in websites.&lt;/li&gt;&lt;li&gt;Power early interactive interfaces on the web, this was way before the web platform matured and could support these types of interactivity natively.&lt;/li&gt;&lt;li&gt;I, personally, remember it for flash-based advertisements of which I created many when I was first starting out in web development!&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-was-it-popular&quot;&gt;Why was it popular?&lt;/h4&gt;&lt;p&gt;Flash was hugely popular at the time due to the fact that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Cross-browser multimedia support was lacking on the web platform (i.e. no native support)&lt;/li&gt;&lt;li&gt;Advanced vector animation support&lt;/li&gt;&lt;li&gt;In 2005, Flash was the sole method for streaming audio and video on the web, as exemplified by YouTube&#39;s reliance on it.&lt;/li&gt;&lt;li&gt;Interaction was programmed through the use of &lt;a href=&quot;https://en.wikipedia.org/wiki/ActionScript&quot;&gt;ActionScript&lt;/a&gt;. If it sounds very familiar to JS, that&#39;s because it is, as they are both based on the &lt;a href=&quot;https://ecma-international.org/publications-and-standards/standards/ecma-262/&quot;&gt;ECMAScript Standard&lt;/a&gt;. That&#39;s a massive oversimplification, but if you are curious, read all about it on &lt;a href=&quot;https://en.wikipedia.org/wiki/ECMAScript&quot;&gt;Wikipedia&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;In the late 1990s there was a popular trend on the web of having completely pointless Flash intros that would load and play automatically before you entered a site. There are countless example of these intros &lt;a href=&quot;https://www.youtube.com/results?search_query=website+flash+intro&quot;&gt;on YouTube&lt;/a&gt; if you are interested!&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-was-it-deprecated&quot;&gt;Why was it deprecated?&lt;/h4&gt;&lt;p&gt;There are many reasons as to why Flash is now deprecated. These include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Flash was well-known for serious security vulnerabilities, which were often used for malicious software and browser takeovers.&lt;/li&gt;&lt;li&gt;Flash content often consumed significant CPU and memory, leading to poor performance and excessive battery drain on mobile devices.&lt;/li&gt;&lt;li&gt;As mentioned earlier, Apple refused to support Flash on iOS, citing security, performance, and stability concerns, which contributed heavily to its decline.&lt;/li&gt;&lt;li&gt;Adobe&#39;s proprietary Flash technology was incompatible with open web standards, hindering accessibility, interoperability, and sustainability.&lt;/li&gt;&lt;li&gt;Lastly, open web standards and the web platform evolved to replace Flash with native (and non-proprietary) functionality like:&lt;ul&gt;&lt;li&gt;Native &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Structuring_content/HTML_video_and_audio&quot;&gt;video and audio playback&lt;/a&gt; (&lt;code&gt;&amp;lt;video&gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;audio&gt;&lt;/code&gt; tags)&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/animation&quot;&gt;CSS animations and transitions&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/canvas&quot;&gt;Canvas&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API&quot;&gt;WebGL&lt;/a&gt; for interactive graphics and games&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/SVG&quot;&gt;SVG&lt;/a&gt; for scalable vector graphics&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Flash met its end with the advent of modern web APIs, including HTML5, CSS3, and modern JS.&lt;/p&gt;&lt;p&gt;In 2017 &lt;a href=&quot;https://blog.adobe.com/en/publish/2017/07/25/adobe-flash-update&quot;&gt;Adobe announced that Flash&#39;s end of life&lt;/a&gt; would be in 2020. In December 2020 Adobe released the final update for Flash. By January 2021, major browsers disabled Flash by default and eventually blocked Flash content entirely.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-5&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;At last! A straightforward answer to this question: No, it&#39;s impossible for you to use Flash on the modern web as Flash content in no longer supported in any modern browser. Simple! RIP Adobe (&lt;a href=&quot;https://en.wikipedia.org/wiki/Macromedia&quot;&gt;Macromedia&lt;/a&gt;) Flash 1996 to 2020. You won&#39;t be missed.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-9&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;As mentioned above, there are a number of native browser-based alternatives for Flash (HTML5, CSS3, Modern JS) functionality these are: - Native &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Structuring_content/HTML_video_and_audio&quot;&gt;video and audio playback&lt;/a&gt; (&lt;code&gt;&amp;lt;video&gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;audio&gt;&lt;/code&gt; tags) - &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/animation&quot;&gt;CSS animations and transitions&lt;/a&gt; - &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/canvas&quot;&gt;Canvas&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API&quot;&gt;WebGL&lt;/a&gt; for interactive graphics and games - &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/SVG&quot;&gt;SVG&lt;/a&gt; for scalable vector graphics&lt;/p&gt;&lt;h3 id=&quot;scalable-inman-flash-replacement-sifr&quot;&gt;Scalable Inman Flash Replacement (sIFR)&lt;/h3&gt;&lt;p&gt;Before the introduction of the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face&quot;&gt;@font-face&lt;/a&gt; at-rule, which is defined in the &lt;a href=&quot;https://drafts.csswg.org/css-fonts/&quot;&gt;CSS Fonts Module Level 4 specifications&lt;/a&gt;, Web Designers and Frontend Developers were desperately seeking to expand the limited number of cross-browser, and cross-operating system fonts that were available on the web. In order to achieve that, a number of workarounds and general ingenious browser hackery were built. From the name of this technique you may also be able to guess the answer for the &quot;Can I still use it?&quot; section!&lt;/p&gt;&lt;h4 id=&quot;what-is-sifr&quot;&gt;What is sIFR?&lt;/h4&gt;&lt;p&gt;Scalable Inman Flash Replacement (sIFR) was an creative technique that used JS and Flash together to replace HTML text elements with Flash-rendered text. This feature allowed developers to embed custom fonts directly within the Flash file. Consequently, they could modify the HTML text, and the Flash file would dynamically render the updated content.&lt;/p&gt;&lt;p&gt;This workaround was required at the time because there was very limited support for custom web fonts via the use of &lt;code&gt;@font-face&lt;/code&gt;. Surprisingly, &lt;code&gt;@font-face&lt;/code&gt; was first introduced by Microsoft in &lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_Explorer_4&quot;&gt;Internet Explorer 4&lt;/a&gt; in 1997, using &lt;a href=&quot;https://en.wikipedia.org/wiki/Embedded_OpenType&quot;&gt;Embedded OpenType (EOT)&lt;/a&gt; as the font format. This was proprietary to IE, so no other browsers supported it. Since there wasn&#39;t a cross-browser way to use custom fonts, alternative techniques like sIFR emerged.&lt;/p&gt;&lt;h4 id=&quot;sifr-popularity&quot;&gt;sIFR Popularity&lt;/h4&gt;&lt;p&gt;sIFR emerged in the early to mid-2000s, with its first public version released around 2004-2005. sIFR was widely used until around 2009-2010, especially for headings and branded typography. Its popularity grew during that period due to the technique&#39;s preservation of SEO and accessibility advantages. This was because the original HTML text remained in the Document Object Model (DOM), allowing it to still be readable by search engines and assistive technology. And once setup it was simple to update the underlying text and sIFR took care of the rest. There was also the added bonus that the text remained selectable, so could be copied and pasted when needed. It sounds like a great solution, so where did it all go wrong?&lt;/p&gt;&lt;h4 id=&quot;why-its-outdated-2&quot;&gt;Why it&#39;s outdated?&lt;/h4&gt;&lt;p&gt;There are several reasons why the sIFR technique is now outdated. We covered the main one in the previous &quot;Flash-Based Content&quot; technique above:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It relies on the Adobe Flash Player browser plugin, that is now deprecated and blocked in all major browsers due to security vulnerabilities and performance issues.&lt;/li&gt;&lt;li&gt;It slowed down page performance by increasing page load time, due to having to download the Flash assets.&lt;/li&gt;&lt;li&gt;Although it was partially accessible from an HTML perspective, it certainly wasn&#39;t perfect as it introduced accessibility and compatibility issues on devices &lt;strong&gt;without Flash support&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;Web standards came to the rescue. CSS3 brought with it native cross-browser support for &lt;code&gt;@font-face&lt;/code&gt; for custom fonts, without the need for any browser plugins. The new standards supported Web Open Font Format (WOFF and WOFF2) font formats which are a standardised and optimised custom font format for the delivery of fonts on the web. Basically, HTML5 and CSS working together simply removed the need for plugin-based typography workarounds.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-it-6&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;As I mentioned, above this is a pretty simple question to answer… No, not at all, the removal of Flash from all modern browsers used today guarantees that!&lt;/p&gt;&lt;h4 id=&quot;modern-alternative&quot;&gt;Modern Alternative&lt;/h4&gt;&lt;p&gt;There&#39;s only one modern alternative that should be used on the web today: &lt;code&gt;@font-face&lt;/code&gt;. An example of its usage is given below:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;MyCustomFont&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.eot&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* IE9 Compat Modes */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.eot?#iefix&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;embedded-opentype&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* IE6-IE8 */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* Super modern browsers */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* Modern browsers */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.ttf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;truetype&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* Safari, Android, iOS */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Thankfully, modern browsers widely support WOFF, thus, simplifying the above code:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;MyCustomFont&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* Super modern browsers */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* modern browsers */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In fact, any modern web browser that supports WOFF also supports WOFF2. Therefore, the code you should use today is as follows:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;MyCustomFont&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;fonts/MyCustomFont.woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In all instances above you&#39;d use the custom font like so:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.myfontclass&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;MyCustomFont&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* other &quot;fallback&quot; fonts here */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The browser will take care of the rest!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: that you should always provide a font fallback in your &lt;code&gt;font-family&lt;/code&gt; property, just in case the font file fails to load, or it is accidentally deleted from your server. There is so much more to the use of &lt;code&gt;@font-face&lt;/code&gt;. If you are interested in advanced topics around its usage you should definitely check out &lt;a href=&quot;https://fediverse.zachleat.com/@zachleat&quot;&gt;Zach Leatherman&lt;/a&gt;&#39;s copious &lt;a href=&quot;https://www.zachleat.com/web/fonts/&quot;&gt;amount of work&lt;/a&gt; on the subject over the years!&lt;/p&gt;&lt;h3 id=&quot;cufon&quot;&gt;Cufón&lt;/h3&gt;&lt;p&gt;Much like sIFR above, Cufón was created due to the lack of options when it came to using custom fonts on the web. It was popular around the same time as sIFR (late 2000s and early 2010s). It essentially was solving the same problem, but using a different cross-browser technique. Whereas sIFR used Flash, Cufón worked like so:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Fonts were converted into vector graphics, and canvas (or &lt;a href=&quot;https://en.wikipedia.org/wiki/Vector_Markup_Language&quot;&gt;VML&lt;/a&gt; for older versions of IE) this was then used to render the text in place.&lt;/li&gt;&lt;li&gt;The JS then replaced the HTML text with the custom font rendered version of the text.&lt;/li&gt;&lt;li&gt;Since it was JS-based, there was no need for any plugins (like Flash).&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;why-was-it-used-2&quot;&gt;Why was it used?&lt;/h4&gt;&lt;p&gt;As previously noted with sIFR, browser support for CSS &lt;code&gt;@font-face&lt;/code&gt; was inadequate or inconsistent at the time. Designers and developers wanted to use custom fonts for branding and stylistic reasons without users having to install a plugin or the fonts locally. Cufón was attractive because it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Didn&#39;t require a plugin for it to work.&lt;/li&gt;&lt;li&gt;Provided near pixel-perfect rendering of the custom font.&lt;/li&gt;&lt;li&gt;Was easy to integrate with minimal JS setup.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-is-it-outdated-2&quot;&gt;Why is it outdated?&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;Modern browsers all support &lt;code&gt;@font-face&lt;/code&gt;, a much better solution as it allows the direct use of web fonts like WOFF or WOFF2 files without the use of JS hacks.&lt;/li&gt;&lt;li&gt;Its usage impacted accessibility. Due to Cufón replacing text in the DOM with rendered graphics, screen readers couldn&#39;t interpret these replacements as text, thus degrading a site&#39;s accessibility.&lt;/li&gt;&lt;li&gt;Cufón caused web performance issues as the text replacement script was run &lt;strong&gt;after&lt;/strong&gt; page load, which increased page render time, blocked interactivity and degraded the overall performance, especially on slower devices.&lt;/li&gt;&lt;li&gt;Although Cufón attempted to preserve the replaced text in the DOM, the results were often inconsistent, mainly because search engine crawlers at the time had inconsistent results when parsing JS-replaced content.&lt;/li&gt;&lt;li&gt;Cufón didn&#39;t work with responsive design, once rendered the Cufón replaced text didn&#39;t scale correctly, unless the page was reloaded at the new page size.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it-7&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Although the &lt;a href=&quot;https://cufon.shoqolate.com/generate/&quot;&gt;site is still available here&lt;/a&gt;, and the &lt;code&gt;cufon.js&lt;/code&gt; script is still available to &lt;a href=&quot;https://raw.githubusercontent.com/sorccu/cufon/master/js/cufon.js&quot;&gt;download&lt;/a&gt;. The font generator has been taken down and is no longer maintained. So to get it working you&#39;d need to jump through quite a few hoops! So, what I&#39;m really trying to say is: Yes, you can, but it &lt;strong&gt;isn&#39;t&lt;/strong&gt; worthwhile. Even the original author Simo Kinnunen says on the website:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Seriously, though you should be using standard web fonts by now.&lt;/p&gt;&lt;/blockquote&gt;&lt;h4 id=&quot;modern-alternatives-10&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;Rather than repeat myself, I&#39;ll refer you to the &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#scalable-inman-flash-replacement-sifr&quot;&gt;same section from the sIFR methodology above&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;gif-text-replacements&quot;&gt;GIF Text Replacements&lt;/h3&gt;&lt;p&gt;Although I am aware that this is not a plug-in, it seemed like the most appropriate section to include it in since we are discussing font replacement techniques. Of all the custom font techniques I&#39;ve listed, this one is by far the worst in my opinion. The technique was popular in the late 1990s and into the early 2000s, when there were very few other options for using custom fonts on the web.&lt;/p&gt;&lt;h4 id=&quot;what-is-a-gif-text-replacement&quot;&gt;What is a GIF Text Replacement?&lt;/h4&gt;&lt;p&gt;It&#39;s a self-explanatory name. In order to use a custom font, a designer would create the static asset (usually via Photoshop or similar), then the developer would cut out the text as a GIF. This image would then be used to replace the HTML text on the page with the image, in order to make it look like a custom font was in use. Example code for this technique can be seen below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Gif text replacement for a heading --&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;images/heading-text.gif&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Welcome to Our Website&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Gif text replacement for a navigation link --&gt;&lt;/span&gt;
			&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/about.html&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
				&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;images/about-link.gif&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;About Us&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
			&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Some readers may wonder why a GIF was used instead of a PNG, since both support transparency. The main reason is that Internet Explorer offered poor support for transparent PNGs and required a complicated hack to make them work, which I will explain &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#transparent-png-fix&quot;&gt;later in the post&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;why-was-it-so-bad-2&quot;&gt;Why was it so bad?&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;It was time-consuming and maintenance heavy. Should the design change, then so would all the GIFs that had to be manually cut out of the design files again.&lt;/li&gt;&lt;li&gt;It was bad for Accessibility. Screen readers are unable to process text embedded within GIFs or images that lack meaningful alt text. The absence or outdated nature of alt text therefore created an exclusionary experience for users with visual impairments.&lt;/li&gt;&lt;li&gt;It was bad for SEO. Search engines could not index text within images, thus, harming discoverability, this technique relied on developers ensuring they had accurate alt-text, which isn&#39;t always the case.&lt;/li&gt;&lt;li&gt;It was bad for performance. At the time of its popularity, the web was going through a transition period from HTTP/1.0 to HTTP/1.1. Although &lt;a href=&quot;https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html&quot;&gt;HTTP/1.1 was better for TCP connections than HTTP/1.0&lt;/a&gt;, these TCP connections were very expensive in terms of web performance, and each of these GIF replacements required its own TCP connection, which increased page load times.&lt;/li&gt;&lt;li&gt;It was terrible for responsiveness. Although the responsive web was a few years away when it was popular, the difference between images and text rendering was text&#39;s ability to scale across different devices and screen sizes. Images simply couldn&#39;t do that, leading to poor rendering and pixelation on some devices.&lt;/li&gt;&lt;li&gt;GIF only supports 256 colours (8-bit) and for the GIF to be transparent, one of those colours would need to be transparent. So if your text had a complex colour palette, it either wouldn&#39;t work, or just look terrible.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it-8&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;No, it&#39;s as simple as that. It&#39;s a technique with so many negatives and so few positives, it should be confined to the interesting history of the web platform!&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-11&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;Again, rather than repeat myself, I&#39;ll refer you to the &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#scalable-inman-flash-replacement-sifr&quot;&gt;same section from the sIFR methodology above&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;adobe-air&quot;&gt;Adobe AIR&lt;/h3&gt;&lt;p&gt;I remember going to a conference around 2007 / 2008, and there was so much hype about Adobe AIR. It was going to be the &quot;next big thing&quot;, due to the fact it could enable developers to create rich desktop and mobile apps using &lt;strong&gt;only&lt;/strong&gt; web skills and technologies.&lt;/p&gt;&lt;h4 id=&quot;what-was-adobe-air&quot;&gt;What was Adobe AIR?&lt;/h4&gt;&lt;p&gt;The AIR in Adobe AIR stood for Adobe Integrated Runtime. It was a cross-platform runtime developed by Adobe. It allowed developers to use: HTML, JS, Adobe Flash, &lt;a href=&quot;https://flex.apache.org/&quot;&gt;Flex&lt;/a&gt;, and &lt;a href=&quot;https://en.wikipedia.org/wiki/ActionScript&quot;&gt;ActionScript&lt;/a&gt;. All combined they could run as a standalone desktop or mobile application. It supported Windows and macOS for desktop, and later Android and iOS on mobile.&lt;/p&gt;&lt;p&gt;Furthermore, it also enabled:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Running Flash-based applications outside the browser.&lt;/li&gt;&lt;li&gt;Enabling rich multimedia, animations, and offline capabilities.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-is-it-outdated-3&quot;&gt;Why is it outdated?&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;It relied on Flash and ActionScript. With flash reaching its end of life in late 2020 due to persistent security vulnerabilities, and the momentum behind open standards like HTML5, CSS3, and JS ES6+. AIR ceased to exist due to the loss of its core technology.&lt;/li&gt;&lt;li&gt;A shift to modern cross-platform frameworks. The market moved towards more efficient and performant technologies like:&lt;ul&gt;&lt;li&gt;React Native&lt;/li&gt;&lt;li&gt;Flutter&lt;/li&gt;&lt;li&gt;Electron (for desktop apps) The advantage of these frameworks is they use native components or JS runtimes, without the heavy reliance on Flash. This offered developers and users greater performance, maintainability, security, and community support.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Lack of Adobe support. Adobe handed AIR over to a subsidiary of &lt;a href=&quot;https://www.harman.com/&quot;&gt;Samsung (Harman)&lt;/a&gt; in &lt;a href=&quot;https://blog.adobe.com/en/publish/2019/05/30/the-future-of-adobe-air&quot;&gt;June 2019&lt;/a&gt;for ongoing maintenance. Support is still provided by Harman, but only for enterprises with legacy applications that they still rely on, There&#39;s no active innovation or features being added to AIR by Harman.&lt;/li&gt;&lt;li&gt;Security concerns. As with Flash in the browser, security was always an ongoing issue, and this continued in AIR since it was the backbone to its core functionality. By continuing to build on AIR, it poses security risks and compatibility limitations with modern browsers and operating systems.&lt;/li&gt;&lt;li&gt;Lack of developer interest and ecosystem. Developers on the modern web tend to favour open ecosystems with an active community for support and updates. Adobe AIR’s ecosystem has completely stagnated.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it-9&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;As with any other Flash-based technology, I&#39;m afraid not, it is no longer supported and even if you could, there are more modern open frameworks you could use like, &lt;a href=&quot;https://reactnative.dev/&quot;&gt;React Native&lt;/a&gt;, &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt;, or &lt;a href=&quot;https://www.electronjs.org/&quot;&gt;Electron&lt;/a&gt; (for desktop applications). AIR is now history, and if you are using an AIR application within your digital estate, it is &lt;strong&gt;strongly recommended&lt;/strong&gt; you prioritise migration, due to higher maintainability, poor security, and lack of developer availability.&lt;/p&gt;&lt;h3 id=&quot;yahoo-pipes&quot;&gt;Yahoo Pipes&lt;/h3&gt;&lt;p&gt;It is easy to forget just how dominant Yahoo was on the web during the late 1990s. Before Google emerged as the leading search engine, Yahoo was one of the primary gateways to the internet. Its peak influence was between 1996 and 2000, when it played a central role in how people accessed and navigated the web. It was the default starting point for most web users due to its curated directory, news, and email services combined. It was also a technology leader on the web too, as I mention later in the blog post when I look at their extensive JS Library: &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#yahoo-user-interface-yui-2006&quot;&gt;Yahoo! User Interface (YUI)&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I remember using Yahoo Pipes for combining my many RSS feeds at the time, it really was a fantastic visual tool for data manipulation.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the innovative data manipulation tool with its drag and drop UI.&quot; decoding=&quot;async&quot; height=&quot;524&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/rn9Imq_Ru6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/rn9Imq_Ru6-300.webp 300w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/rn9Imq_Ru6-600.webp 600w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/rn9Imq_Ru6-800.webp 800w&quot; width=&quot;800&quot;&gt;&lt;/p&gt;&lt;h4 id=&quot;what-was-it&quot;&gt;What was it?&lt;/h4&gt;&lt;p&gt;Yahoo pipes was a visual data mashup tool that allowed users to aggregate, manipulate, and filter data from around the web. It was released in 2007, and it allowed developers (and non-developers) alike to aggregate, manipulate, and filter data from around the web. It provided a drag and drop interface where users could connect various manipulation modules and join them together by creating pipes between modules. You were essentially piping data &quot;through&quot; the tool (hence the name!) and the manipulated date would come out the other end. It was considered highly innovative at the time, and it was used a lot for rapid prototyping.&lt;/p&gt;&lt;h4 id=&quot;why-is-it-outdated-4&quot;&gt;Why is it outdated?&lt;/h4&gt;&lt;p&gt;Have a look at the &lt;a href=&quot;https://www.yahoo.com/&quot;&gt;Yahoo! homepage&lt;/a&gt;, and you will see it is the shadow of its former self, it looks to more of a news aggregation service now than a popular search engine. This was because Yahoo decided to make a giant shift in business strategy, moving away from developer tools and open web utilities to concentrate on advertising and media products. Although it was popular with technology enthusiasts, it was never a mainstream product for Yahoo, so the operational expenses vs. usage statistics didn&#39;t align with Yahoo&#39;s business priorities. Lastly, the web evolved beyond Yahoo Pipes, and it couldn&#39;t keep pace with the changes. Modern APIs, JSON-based services, and JS frameworks allowed developers to build similar data transformations programmatically with greater flexibility. Due to all these factors Yahoo Pipes was sadly shut down in 2015.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-today-2&quot;&gt;Can I still use it today?&lt;/h4&gt;&lt;p&gt;Nope, it was shutdown by Yahoo in 2015, with no further support or hosting by Yahoo.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-12&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;While innovative at the time, visual mashups have been replaced by:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Dedicated data transformation tools (e.g. &lt;a href=&quot;https://zapier.com/&quot;&gt;Zapier&lt;/a&gt;, &lt;a href=&quot;https://www.make.com/en&quot;&gt;Integromat/Make&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;Serverless functions (&lt;a href=&quot;https://aws.amazon.com/lambda/&quot;&gt;AWS Lambda&lt;/a&gt;, Azure Functions, &lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers&lt;/a&gt;, and &lt;a href=&quot;https://www.fastly.com/documentation/guides/compute/getting-started-with-compute/&quot;&gt;Fastly Compute@Edge&lt;/a&gt;) for real-time data processing.&lt;/li&gt;&lt;li&gt;Low-code platforms with integrated API management.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The web has also become a lot more complicated regarding Web scraping and feed aggregation. Because anti-scraping measures, authentication, and API rate limits weren&#39;t necessary when Yahoo Pipes was created, the techniques it employed didn&#39;t support the robust backend processes now required to handle these requirements. Although Yahoo Pipes was innovative at the time, it has long been discontinued and is now considered a obsolete part of web platform history.&lt;/p&gt;&lt;h3 id=&quot;phonegap-apache-cordova&quot;&gt;PhoneGap / Apache Cordova&lt;/h3&gt;&lt;p&gt;The one thing that sticks in my mind when I think about PhoneGap is when I saw a talk from one of the &lt;a href=&quot;https://www.nitobi.com/&quot;&gt;Nitobi&lt;/a&gt; engineers back in 2009 / 2010, he said something along the lines of:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;We are using PhoneGap to bridge the current gap for developers in creating cross-platform mobile applications. Our goal is for it to become obsolete once native platforms fully support these capabilities.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This really impressed me at the time, spending so much time on a product with the &lt;strong&gt;aim&lt;/strong&gt; for it to become obsolete.&lt;/p&gt;&lt;h4 id=&quot;what-was-phonegap&quot;&gt;What was PhoneGap?&lt;/h4&gt;&lt;p&gt;PhoneGap was a mobile development framework created by a Canadian company called &lt;a href=&quot;https://www.nitobi.com/products/phonegap/index&quot;&gt;Nitobi&lt;/a&gt; in 2009, it was later acquired by Adobe in 2011. PhoneGap allowed web developers to build cross-platform mobile applications, simply, using web technologies: HTML, CSS, and JS. The applications were packaged into native containers allowing them to run as mobile apps, while also having access to device API&#39;s via JS.&lt;/p&gt;&lt;h4 id=&quot;why-was-it-required&quot;&gt;Why was it required?&lt;/h4&gt;&lt;p&gt;At the time of release, the mobile web was incredibly popular and getting bigger month on month. It&#39;s important to remember, the first version of the iPhone was released only 2-years before (June 2007). This really was an exciting time in the Web Platform&#39;s history. If you wanted to release a cross-platform application at the time and wanted to support Android, iOS, and Windows Phone, you needed developers with knowledge of multiple programming languages:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Android&lt;/strong&gt; required Java.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;iOS&lt;/strong&gt; required Objective-C.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Windows Phone&lt;/strong&gt; required C#.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Finding a single developer with all these skills would be incredibly hard, so to build and maintain all 3 platforms usually required a whole team of developers.&lt;/p&gt;&lt;p&gt;One of the main advantages of PhoneGap was that all 3 platforms had a single central codebase, which reduced development time and maintenance.&lt;/p&gt;&lt;p&gt;PhoneGap leveraged &lt;a href=&quot;https://cordova.apache.org/&quot;&gt;Cordova&lt;/a&gt; under the hood, which it essentially branded and wrapped for broader adoption.&lt;/p&gt;&lt;h4 id=&quot;why-is-it-outdated-5&quot;&gt;Why is it outdated?&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;PhoneGap apps performed poorly&lt;/strong&gt;. This was especially true for graphics-intensive, or animation-heavy interfaces. This is because PhoneGap apps ran within a WebView container rather than as a native application.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Adobe stopped supporting it&lt;/strong&gt;. This seems to be a common theme in this blog post… Adobe ended support for PhoneGap in October 2020. At the time developers were advised to either migrate to &lt;a href=&quot;https://cordova.apache.org/&quot;&gt;Apache Cordova&lt;/a&gt; or consider other frameworks.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Alternatives evolved&lt;/strong&gt;. As the mobile platform expanded, so did the availability of other frameworks to help developers build apps. These alternatives included:&lt;ol&gt;&lt;li&gt;&lt;a href=&quot;https://reactnative.dev/&quot;&gt;React Native&lt;/a&gt; allowing near-native performance with JS and React paradigms.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; enabling high-performance apps with a single Dart codebase and native compilation.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps&quot;&gt;Progressive Web Apps (PWAs)&lt;/a&gt; reducing the need for wrapping web apps as native apps in many use cases.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://capacitorjs.com/&quot;&gt;Capacitor (by Ionic)&lt;/a&gt; providing modern native bridging with a streamlined developer experience compared to PhoneGap/Cordova.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;PhoneGap&#39;s Ecosystem growth stalled&lt;/strong&gt;. As newer frameworks were released and Adobe stopped supporting it the community moved away and PhoneGap’s plugin ecosystem stagnated.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it-10&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;No, there are several alternatives I have listed above that you should consider instead. PhoneGap served its initial purpose as a bridge, enabling developers to build cross-platform mobile applications. As was its mission, It became obsolete when native platforms fully incorporated these capabilities.&lt;/p&gt;&lt;h3 id=&quot;microsoft-silverlight&quot;&gt;Microsoft Silverlight&lt;/h3&gt;&lt;p&gt;NOTE: I never used Silverlight (I do remember it being announced) I&#39;m just adding it to the post for completeness.&lt;/p&gt;&lt;h4 id=&quot;what-is-silverlight&quot;&gt;What is Silverlight?&lt;/h4&gt;&lt;p&gt;Silverlight was a rich internet application (RIA) framework introduced by Microsoft in 2007. It was conceptually similar to Adobe Flash, designed to deliver interactive multimedia, animations, and streaming video inside the browser.&lt;/p&gt;&lt;p&gt;It used a subset of the .NET Framework, with applications typically written in C# or VB.NET, and presentation defined using XAML (an XML-based UI markup language). Developers could reuse existing .NET skills, which made Silverlight attractive in Microsoft-centric enterprises.&lt;/p&gt;&lt;p&gt;Silverlight was often used for:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Media streaming (notably Netflix in its early streaming days)&lt;/li&gt;&lt;li&gt;Interactive dashboards and line-of-business web apps&lt;/li&gt;&lt;li&gt;Cross-browser, cross-platform plugins (Windows and Mac were supported, but Linux support lagged)&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;why-is-it-considered-legacy-2&quot;&gt;Why is it considered legacy?&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Plugin dependency&lt;/strong&gt;: By the 2010s browser vendors had moved away from browser plugins in favour of newly developed web platform technologies. Plugins were often unsecure, unstable, and inaccessible.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Limited cross-platform reach&lt;/strong&gt;: Although Silverlight was well supported on Microsoft platforms (as you would expect!), it also had support on Mac, but it had limited support on Linux (via project Moonlight), and no support on mobile devices (Android, iOS).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Rise of open web standards&lt;/strong&gt;: HTML5, CSS3, and JavaScript rose so rapidly for audio, video and advanced graphics (via canvas). The use of plugins was no longer required.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;End of support&lt;/strong&gt;: Considering the above points, Microsoft only stopped support in October 2021. Although Browser vendors stopped a long time before: Chrome in 2015, Edge never supported it. Firefox ended support in March 2017.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-it-11&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Well, this is another easy one. No, you can&#39;t Microsoft have dropped support and no Modern browsers support it either.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-13&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;The answer to this is basically native web platform API&#39;s. Specifics include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Video Streaming&lt;ul&gt;&lt;li&gt;&lt;strong&gt;HTML5 &lt;code&gt;&amp;lt;video&gt;&lt;/code&gt; element&lt;/strong&gt; with adaptive bitrate streaming (&lt;a href=&quot;https://en.wikipedia.org/wiki/HTTP_Live_Streaming&quot;&gt;HLS&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP&quot;&gt;MPEG-DASH&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;DRM&lt;/strong&gt; is handled via &lt;a href=&quot;https://www.w3.org/TR/encrypted-media-2/&quot;&gt;Encrypted Media Extensions (EME)&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;For interactive apps and dashboards:&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Modern JavaScript frameworks&lt;/strong&gt; such as React, Angular, Vue, or Svelte.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;WebAssembly (Wasm)&lt;/strong&gt; for near-native performance, including options like &lt;a href=&quot;https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor&quot;&gt;Blazor (from Microsoft)&lt;/a&gt; which lets you run .NET in the browser without plugins.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;For graphics, animation, and UI:&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/animation&quot;&gt;&lt;strong&gt;CSS3 animations and transforms&lt;/strong&gt;&lt;/a&gt; for UI transitions.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/canvas&quot;&gt;&lt;strong&gt;Canvas API&lt;/strong&gt;&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API&quot;&gt;&lt;strong&gt;WebGL&lt;/strong&gt;&lt;/a&gt; for 2D and 3D graphics.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/SVG&quot;&gt;&lt;strong&gt;SVG&lt;/strong&gt;&lt;/a&gt; for scalable vector graphics.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API&quot;&gt;&lt;strong&gt;WebGPU&lt;/strong&gt;&lt;/a&gt; (emerging) for modern GPU-accelerated rendering.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;silverlight-summary&quot;&gt;Silverlight Summary&lt;/h4&gt;&lt;p&gt;Silverlight is legacy because it relied on a now-obsolete plugin model, had poor cross-platform support, and was outpaced by open web standards. Today, everything Silverlight did can be done more securely and portably with HTML5, CSS, JavaScript frameworks, and WebAssembly.&lt;/p&gt;&lt;h3 id=&quot;java-applets&quot;&gt;Java Applets&lt;/h3&gt;&lt;p&gt;NOTE: I never used Java Applets (although I remember them!) I&#39;m just adding it to the post for completeness.&lt;/p&gt;&lt;h4 id=&quot;what-is-java-applets&quot;&gt;What is Java Applets?&lt;/h4&gt;&lt;p&gt;Java Applets were small applications written in Java that could be embedded into web pages and run inside the browser through a special Java Plug-in (based on the &lt;a href=&quot;https://en.wikipedia.org/wiki/NPAPI&quot;&gt;NPAPI plugin architecture&lt;/a&gt;). Introduced in the mid-1990s, they were part of &lt;a href=&quot;https://en.wikipedia.org/wiki/Sun_Microsystems&quot;&gt;Sun Microsystems’&lt;/a&gt; vision of “write once, run anywhere” – letting developers build interactive content and complex functionality that browsers of the time (pre-HTML5) could not support natively.&lt;/p&gt;&lt;p&gt;They were often used for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Interactive educational content and simulations&lt;/li&gt;&lt;li&gt;Online games&lt;/li&gt;&lt;li&gt;Financial tools like mortgage calculators or trading dashboards&lt;/li&gt;&lt;li&gt;Enterprise intranet applications&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;why-is-it-considered-legacy-3&quot;&gt;Why is it considered legacy?&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Plugin dependency&lt;/strong&gt;: To use the Applets this required a user to install the &lt;a href=&quot;https://docs.oracle.com/goldengate/1212/gg-winux/GDRAD/java.htm#BGBFJHAB&quot;&gt;Java Runtime Environment (JRE)&lt;/a&gt; plugin and keep it updated. I distinctly remember the &lt;a href=&quot;https://p0w3rsh3ll.wordpress.com/wp-content/uploads/2013/06/jre-1-7-update-prompt.jpg&quot;&gt;update prompt&lt;/a&gt; for these nuisance updates!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Security risks&lt;/strong&gt;: The Java plugin was a frequent target of exploits and malware, leading browsers and enterprises to actively block or disable it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Performance and user experience&lt;/strong&gt;: Applets often loaded slowly, had inconsistent UI integration with web pages, and required clunky permission dialogs.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Decline of NPAPI support&lt;/strong&gt;: Browsers started phasing out NPAPI (the plugin technology Applets relied on). &lt;strong&gt;Chrome&lt;/strong&gt; dropped NPAPI in 2015, &lt;strong&gt;Firefox&lt;/strong&gt; dropped NPAPI in 2017 (except Flash until 2021) , and Microsoft Edge never supported NPAPI.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Official deprecation&lt;/strong&gt;: Oracle deprecated the Java browser plugin in Java 9 (2017) and removed it entirely in later releases.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-it-12&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Nope! Modern browser no longer support it and Oracle stopped supporting the plugin in 2017.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-14&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;This list is basically native web platform API&#39;s. I don&#39;t want to repeat myself so refer to the &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#modern-alternatives-13&quot;&gt;Silverlight Modern Alternatives&lt;/a&gt; from earlier in the post.&lt;/p&gt;&lt;h4 id=&quot;java-applet-summary&quot;&gt;Java Applet Summary&lt;/h4&gt;&lt;p&gt;Java Applets are legacy because they relied on a fragile plugin model that posed significant security risks and is no longer supported by modern browsers. Today, HTML5, JavaScript, and WebAssembly provide richer, faster, and safer alternatives without requiring any plugins.&lt;/p&gt;&lt;h2 id=&quot;5-the-javascript-framework-explosion&quot;&gt;5. The JavaScript Framework Explosion&lt;/h2&gt;&lt;h3 id=&quot;dhtml-beginnings-1997&quot;&gt;DHTML Beginnings (1997)&lt;/h3&gt;&lt;p&gt;Dynamic HTML (DHTML) was all the rage around 1997-1998. By combining the primary web technologies: HTML, CSS, JS, and the DOM, developers realised that they could make a web page interactive and &quot;dynamically&quot; update the page without the need to reload the entire page. This new technique was frequently employed for animated HTML elements, such as image rollovers and dynamic navigation menus. It also provided immediate user feedback, particularly for form validation, by checking if the user had entered a valid email, for instance. If not available, then use standard HTML and CSS to show a user-friendly error message.&lt;/p&gt;&lt;p&gt;DHTML wasn&#39;t necessarily bad, if implemented in moderation, unfortunately it always seemed like the wild-west. What would work in Internet Explorer 4 (IE4) wouldn&#39;t always work in Netscape Navigator because Microsoft and Netscape Communications Corporation (NCC), had different levels of support for JavaScript in fact, Microsoft had their own version of the ECMAScript standard called JScript. This led to lots of maintenance headaches for developers as the solution often involving forking code for different browsers.&lt;/p&gt;&lt;p&gt;Secondly it turned into a bit of Copy / Paste madness. Since any developer could simply &quot;View Source&quot; on a web page and copy the code and add it to their own website, you often ended up with a mishmash of different interactions and animations all over a website! Thankfully, the trend eventually died out!&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-13&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;A web developer could technically still use DHTML on the modern web, but doing so would be strongly discouraged for any serious or production-level work. This is because in using it, a developer wouldn’t be following modern best practices like separating concerns with structured HTML, CSS and JS, building accessible and performant interfaces, using modular and maintainable code, or leveraging modern frameworks and tooling that enforce consistency, security and scalability.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-15&quot;&gt;Modern alternatives&lt;/h4&gt;&lt;p&gt;There are several modern alternatives to DHTML, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Using Frameworks like React, Vue, or Svelte&lt;/li&gt;&lt;li&gt;Native browser APIs&lt;/li&gt;&lt;li&gt;Modern CSS techniques&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;Progressive enhancement&lt;/a&gt; and accessibility standards&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;early-frameworks-and-libraries&quot;&gt;Early frameworks and libraries&lt;/h3&gt;&lt;h4 id=&quot;prototype-js-2005&quot;&gt;Prototype.js (2005)&lt;/h4&gt;&lt;p&gt;Prototype.js version 1.0 was released in February 2005, and was initially developed to help simplify JS tasks in Ruby on Rails (RoR) projects. Its key features were a host of DOM manipulation utilities, AJAX abstraction to make &lt;code&gt;XMLHttpRequest&lt;/code&gt; easier to handle cross-browser, Class-based inheritance in the form of a lightweight object-oriented programming (OOP) in JS. But by far its most influential feature was its shorthand DOM element selector &lt;code&gt;$()&lt;/code&gt;, later popularised by jQuery.&lt;/p&gt;&lt;p&gt;I remember trying to learn and use Prototype a few times, but as a JS beginner, I found the name confusing. Especially since the prototype object sits at the heart of JavaScript, with almost the whole language hanging off it through things like property and method inheritance.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-14&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;I mean, technically you could, but be warned it hasn&#39;t been updated in &lt;a href=&quot;http://prototypejs.org/&quot;&gt;almost 10-years&lt;/a&gt;! Given the significant evolution of JS and the availability of modern, updated alternatives that leverage the latest browser JS APIs, it would not be a wise choice.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-16&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;It really depends on what a developer was using prototype.js for, as it had quite a range of functionality:&lt;/p&gt;&lt;h5 id=&quot;dom-manipulation&quot;&gt;DOM manipulation&lt;/h5&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://262.ecma-international.org/6.0/&quot;&gt;Native JS (ES6+)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/fabiospampinato/cash&quot;&gt;Cash&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://umbrellajs.com/&quot;&gt;Umbrella JS&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h5 id=&quot;ajax&quot;&gt;AJAX&lt;/h5&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch&quot;&gt;fetch&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://axios-http.com/docs/intro&quot;&gt;Axios&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/ky&quot;&gt;Ky&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h5 id=&quot;utility-functions&quot;&gt;Utility functions&lt;/h5&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://lodash.com/&quot;&gt;Lodash&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://selfrefactor.github.io/rambda/#/&quot;&gt;Rambda&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h5 id=&quot;templating&quot;&gt;Templating&lt;/h5&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://handlebarsjs.com/&quot;&gt;Handlebars&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals&quot;&gt;ES6 Template literals&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h5 id=&quot;full-replacement&quot;&gt;Full replacement&lt;/h5&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://jquery.com/&quot;&gt;jQuery&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://react.dev/&quot;&gt;React&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://vuejs.org/&quot;&gt;Vue&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Prototype.js was incredibly powerful. My preference would be to utilise several micro-JS libraries for specific functionalities rather than adopting an extensive framework such as React, but that&#39;s just my opinion, given the complexity of the React ecosystem.&lt;/p&gt;&lt;h4 id=&quot;script-aculo-us-2005&quot;&gt;Script.aculo.us (2005)&lt;/h4&gt;&lt;p&gt;Script.aculo.us v1.0 was released in 2005 as an extension to Prototype.js. It built on Prototype.js by delivering a powerful set of visual effects, animations, and UI components. It also featured Drag-and-drop support out of the box, as well as sortable lists and autocompletion widgets. As with Prototype.js, Script.aculo.us popularity was partly because it was bundled with RoR, and it had widespread adoption within the &quot;Rails&quot; community.&lt;/p&gt;&lt;p&gt;I still remember Script.aculo.us for its &lt;a href=&quot;http://script.aculo.us/&quot;&gt;distinctive URL&lt;/a&gt;, its bright, animated homepage, and how it really embodied the spirit of ‘web 2.0,’ making the web feel more alive. It&#39;s a library that left a lasting legacy, influencing later libraries like jQuery UI.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-15&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;I really wouldn&#39;t recommend it, as it hasn&#39;t been updated in over 15 years! However, if you&#39;re curious to delve into some web 2.0 history, the site is &lt;a href=&quot;http://script.aculo.us/&quot;&gt;still live&lt;/a&gt; (on HTTP, not HTTPS).&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-17&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;Assuming you are only looking for a JS library for animations:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://greensock.com/gsap&quot;&gt;GSAP (GreenSock Animation Platform)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://motion.dev/&quot;&gt;Motion One (Vanilla JS)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://popmotion.io&quot;&gt;Popmotion&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://animejs.com&quot;&gt;Anime.js&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API&quot;&gt;CSS + Web Animations API (WAAPI)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;dojo-toolkit-2005&quot;&gt;Dojo Toolkit (2005)&lt;/h4&gt;&lt;p&gt;Dojo Toolkit was one of the first major cross-browser toolkits, released in March 2005 with version 0.1. It was developed by &lt;a href=&quot;https://infrequently.org/&quot;&gt;Alex Russell&lt;/a&gt; and was also maintained by a community of other developers too. It&#39;s open-source and still available on &lt;a href=&quot;https://github.com/dojo/dojo&quot;&gt;GitHub&lt;/a&gt; It was one of the earliest frameworks to help build rich web applications by simplifying DOM manipulation, AJAX, event handling, animations and internationalisation (i18n), among many other cutting-edge features. Not only that, it was an early advocate for Asynchronous Module Definition (AMD) and modular JS. Furthermore, it was one of the first JS libraries with strong accessibility (a11y) support. In 2018 Dojo was rewritten as Dojo 2 which supports TypeScript, reactive patterns, virtual DOM, and modern build systems. Dojo 1.x is still in use in some long-running enterprise applications on the web. The last stable release was released in 2022, which was mainly for bug fixes and security updates. New feature development has now shifted purely to Dojo 2+.&lt;/p&gt;&lt;p&gt;Dojo 1.x was incredibly influential for later JS libraries like jQuery, MooTools, Prototype, especially when it came to governance. It was governed by the &lt;a href=&quot;https://download.dojotoolkit.org/release-0.4.4/dojo-0.4.4-src/documents/web0.3/foundation/&quot;&gt;Dojo Foundation&lt;/a&gt;, which later merged with the &lt;a href=&quot;https://blog.jquery.com/2012/03/06/announcing-the-jquery-foundation/&quot;&gt;jQuery Foundation&lt;/a&gt; to form the &lt;a href=&quot;https://venturebeat.com/dev/node-js-and-js-foundations-are-merging-to-form-openjs/&quot;&gt;JS Foundation&lt;/a&gt; (now part of the &lt;a href=&quot;https://openjsf.org/&quot;&gt;OpenJS Foundation&lt;/a&gt;).&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-16&quot;&gt;Can I still Use it?&lt;/h4&gt;&lt;p&gt;Version 1.x, would be a bad idea, but you could &lt;em&gt;technically&lt;/em&gt; still use the latest version (8.0.0), although that may not be wise either, given there hasn&#39;t been a new release in &lt;a href=&quot;https://github.com/dojo/framework/releases/tag/v8.0.0&quot;&gt;over 3-years&lt;/a&gt;. So it&#39;s most likely better to stick with more modern framework alternatives.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-18&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;There are a number of modern alternatives you could consider, keeping in mind that Dojo was very focussed on Accessibility (A11y) and Internationalisation (i18n):&lt;/p&gt;&lt;ol&gt;&lt;li&gt;React: Maintained by: Meta (Facebook).&lt;ul&gt;&lt;li&gt;A11y: Strong support, but it’s &lt;strong&gt;developer-driven&lt;/strong&gt;. ARIA roles and keyboard navigation must be implemented explicitly by developers.&lt;/li&gt;&lt;li&gt;i18n: Excellent ecosystem (&lt;code&gt;react-intl&lt;/code&gt;, &lt;code&gt;formatjs&lt;/code&gt;, &lt;code&gt;lingui&lt;/code&gt;, etc.)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Vue.js (v3): Maintained by: &lt;a href=&quot;https://evanyou.me/&quot;&gt;Evan You&lt;/a&gt; and the Vue core team.&lt;ul&gt;&lt;li&gt;A11y: Good defaults; still &lt;strong&gt;developer-led&lt;/strong&gt;, but accessible components are emerging.&lt;/li&gt;&lt;li&gt;i18n: &lt;code&gt;vue-i18n&lt;/code&gt; is well-maintained and powerful.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Angular: Maintained by: Google&lt;ul&gt;&lt;li&gt;A11y: Arguably the best among mainstream frameworks. The Angular Material team publishes a11y guidance, and many baked-in best practices exist.&lt;/li&gt;&lt;li&gt;i18n: Built-in i18n support, including message extraction and compile-time translation.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Svelte / SvelteKit: Maintained by: &lt;a href=&quot;https://bsky.app/profile/rich-harris.dev&quot;&gt;Rich Harris&lt;/a&gt; and the Svelte core team&lt;ul&gt;&lt;li&gt;A11y: Improving, but not as mature as React or Angular. Accessible components need to be &lt;strong&gt;explicitly chosen or built&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;i18n: Community libraries exist (&lt;code&gt;svelte-i18n&lt;/code&gt;), but official support is not as comprehensive.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;yahoo-user-interface-yui-2006&quot;&gt;Yahoo! User Interface (YUI) (2006)&lt;/h4&gt;&lt;p&gt;YUI was released in February 2006, its first release was version 2.0.0, this is because there was lots of internal development and usage within Yahoo! before it was released publicly. It was originally designed to standardise frontend development at Yahoo and provide the team with a solid cross-browser solution on which the Yahoo team could build feature-rich web applications. It contained a custom loader system that only loaded the components it needed, this was an ingenious approach in the pre-ECMAScript 6 (ES6) module era. Furthermore, it also came with a whole host of feature rich UI widgets, cross-browser abstraction, Event handling, DOM utilities, Animations, CSS Tools (which heavily influencing &lt;a href=&quot;https://nicolasgallagher.com/about-normalize-css/&quot;&gt;Normalize.css&lt;/a&gt; that was developed later), and it was one of the first libraries (after Dojo 1.x) to prioritise internationalisation (i18n) and accessibility, specifically &lt;a href=&quot;https://www.w3.org/WAI/standards-guidelines/aria/&quot;&gt;Accessible Rich Internet Applications (ARIA)&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;What I remember most about YUI 2.x is just how huge it was! Not just the sheer number of UI modules, but the file size too: 300–350 KB minified, or 90–120 KB gzipped! This was before the widespread availability of fast broadband, and when hardware and browsers were significantly less optimised. A full build of the library could easily exceed those figures, too. This is why Yahoo also provided a combination aware CDN service to help reduce the number of requests made and bundle &lt;strong&gt;only&lt;/strong&gt; the components needed. This was a practice that was way ahead of its time!&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-17&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;No, not really, it hasn&#39;t been updated since 2014, the reason for this is detailed in this &lt;a href=&quot;https://yahooeng.tumblr.com/post/96098168666/important-announcement-regarding-yui&quot;&gt;Yahoo Engineering announcement&lt;/a&gt; from the time.&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-19&quot;&gt;Modern alternatives&lt;/h4&gt;&lt;p&gt;Given YUI&#39;s extensive nature as a framework, and to avoid repetition, I recommend referencing my notes on &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#modern-alternatives-14&quot;&gt;Prototype.js modern alternatives&lt;/a&gt; as an initial guide.&lt;/p&gt;&lt;h4 id=&quot;moo-fx-2005&quot;&gt;moo.fx (2005)&lt;/h4&gt;&lt;p&gt;Moo.fx was a lightweight animation library designed to be &lt;a href=&quot;https://en.wikipedia.org/wiki/Unobtrusive_JavaScript&quot;&gt;unobtrusive&lt;/a&gt; and it also worked well with Prototype.js. It focused purely on DOM animations like height transitions and fading etc. It was part of a movement in JS to modularise the codebase for lighter and more responsive interactions. Furthermore, it was a distinct diversion away from larger and heavier animation libraries like &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#script-aculo-us-2005&quot;&gt;Script.aculo.us&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I believe moo.fx was one of the first animation libraries I ever saw. Annoyingly the site hasn&#39;t been archived on &lt;a href=&quot;https://web.archive.org&quot;&gt;archive.org&lt;/a&gt;. I do remember it having a simple and colourful homepage with examples of the animations you could add by only adding a tiny library to your page (3 KB).&lt;/p&gt;&lt;p&gt;It also had a fantastic URL too &quot;http://moo.fx&quot;. What&#39;s so cool about the name, you may ask? Well, the &lt;code&gt;.fx&lt;/code&gt; country code top-level domain (ccTLD) has now been rendered obsolete and is no longer available. It was originally reserved for Metropolitan France, but it was never officially delegated or made available for registration. France later adopted &lt;code&gt;.fr&lt;/code&gt; as its official ccTLD. I can&#39;t fathom how Valerio Proietti, the creator of moo.fx, managed to register the name, but it&#39;s all true. As proof only a single record can be found on archive.org, dating back to 2007, it links to the site&#39;s &lt;a href=&quot;https://web.archive.org/web/20070925000000*/http://moo.fx/robots.txt&quot;&gt;robots.txt file&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-18&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;I can&#39;t even find an archived copy of the homepage, let alone the library itself! So, it definitely falls into the &quot;no, you can&#39;t still use it&quot; category!&lt;/p&gt;&lt;h4 id=&quot;modern-alternatives-20&quot;&gt;Modern Alternatives&lt;/h4&gt;&lt;p&gt;Assuming we are only looking for a modern animation library, it&#39;s best to refer to the list I gave above for &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#modern-alternatives-15&quot;&gt;Script.aculo.us modern alternatives&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;mootools-2006&quot;&gt;MooTools (2006)&lt;/h4&gt;&lt;p&gt;The author of &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#moo-fx-2005&quot;&gt;moo.fx&lt;/a&gt;, Valerio Proietti wanted more than an animation library. He aimed to develop a complete JS framework with an Object-Oriented Programming, modularity and extensibility as first-class features. Thus, MooTools was born! v1.0 was released in September 2006, and it featured some fantastic features for its lightweight size. These included a modular core, and separate components, you could also include, for extra functionality, an Advanced class system (predating the ES6 &lt;code&gt;class&lt;/code&gt; functionality). Powerful DOM manipulation utilities, Ajax handling, effects (moo.fx), and custom events. It was both performant (for the time) and syntactically elegant. Development ceased in the mid-2010&#39;s with v1.5 (the final active release). moo.fx and MooTools hold a special place in my memory as one of the first JS libraries I learnt as a Junior developer.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-19&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Well, the website still exists &lt;a href=&quot;https://mootools.net/&quot;&gt;here&lt;/a&gt;, But considering it hasn&#39;t been updated since January 2016, it&#39;s probably best to look for a &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#modern-alternatives-14&quot;&gt;modern alternative&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;jquery-core-2006&quot;&gt;jQuery (core) (2006)&lt;/h4&gt;&lt;p&gt;In 2006, a truly revolutionary JS library was released called &lt;a href=&quot;https://jquery.com/&quot;&gt;jQuery&lt;/a&gt;. It was developed by the legendary &lt;a href=&quot;https://johnresig.com/&quot;&gt;John Resig&lt;/a&gt;. It offered a lightweight, chainable API in JS to simplify tasks like DOM traversal, and Ajax, among many other &lt;a href=&quot;https://api.jquery.com/&quot;&gt;helpful tools and methods&lt;/a&gt; in its fantastically written API.&lt;/p&gt;&lt;p&gt;jQuery always prided itself on its easy-to-use API and its ability to abstract away the many cross-browser bugs related to different browser vendors implementation of JS (Mozilla) / JScript (Microsoft). It finally gave developers a &quot;stable&quot; API to start building JS powered websites without all the stress of the cross-browser hacks, and forking of code to make features work in every browser. With jQuery, it just worked!&lt;/p&gt;&lt;p&gt;I must admit, I absolutely adored jQuery (and still do)! The API was so clean and readable. The complete opposite to the DOM and the JS API! This library has saved more than just my code, it’s rescued entire projects and probably saved my sanity in the process! Especially working in Digital Marketing, as I did at the time. In those days, clients were constantly after the newest, flashiest animations, regardless of usability. It was all about chasing trends. And when the client&#39;s paying, you just nod and make that already bouncing button pulse and change colour!&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-20&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;Finally, I can say &quot;Yes&quot; to this question! Assuming you plan to use &lt;a href=&quot;https://jquery.com/download/&quot;&gt;v3.x of jQuery&lt;/a&gt;, as the 1.x and 2.x branches are no longer supported or maintained. In fact version 4.0 is currently in beta, according to the &lt;a href=&quot;https://jquery.com/support/&quot;&gt;Support page&lt;/a&gt;. Amazingly, almost a full 19-years after its first release, it is still under active maintenance! Not only that, according to the &lt;a href=&quot;https://almanac.httparchive.org/en/2024/&quot;&gt;Web Almanac 2024&lt;/a&gt;, it&#39;s still the &lt;a href=&quot;https://almanac.httparchive.org/en/2024/javascript#library-usage&quot;&gt;most popular JavaScript library in use on the web&lt;/a&gt;! Truly impressive work by the jQuery team and community!&lt;/p&gt;&lt;h3 id=&quot;later-era-frameworks&quot;&gt;Later era frameworks&lt;/h3&gt;&lt;h4 id=&quot;ext-js-2007&quot;&gt;Ext.js (2007)&lt;/h4&gt;&lt;p&gt;Ext.js version 1.0 was released in April 2007. It was initially developed as an &lt;strong&gt;extension to the YUI Library&lt;/strong&gt; before becoming a fully standalone framework. Its key features included a comprehensive suite of rich UI components, a powerful event model, and advanced layout management capabilities that far exceeded most contemporaries. It introduced a highly structured approach to building web applications, with a strong emphasis on reusable widgets and object-oriented design. But by far its most distinctive contribution was its fully integrated, desktop-like component model for the web. Something rarely seen at the time, and which set the tone for many enterprise-grade JS frameworks that followed.&lt;/p&gt;&lt;p&gt;I never used YUI personally, as its sheer size and breadth of functionality simply didn’t align with the kind of work I was doing at the time. As a result, Ext.js (which as mentioned above, was initially built upon YUI) wasn’t on my radar either. That being said, I’ve included it here for completeness, as it clearly played a significant role in the evolution of rich client-side application frameworks. During my research I discovered how Ext.js transformed into an enterprise-grade toolkit under the &lt;a href=&quot;https://www.sencha.com/products/extjs/&quot;&gt;Sencha brand&lt;/a&gt;. Its strong emphasis on data-driven UIs distinguished it from other lightweight libraries of that period.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-21&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;Yes, Ext.js is still a viable option that you can use on the modern web, if you are building a web application. It continues to be actively maintained by &lt;a href=&quot;https://store.sencha.com/&quot;&gt;Sencha&lt;/a&gt; and even offers a &lt;a href=&quot;https://www.sencha.com/products/reext/&quot;&gt;React Extension&lt;/a&gt;, allowing for seamless integration of Ext.js components into React applications. However, be aware that Ext.js is now a &lt;a href=&quot;https://store.sencha.com/&quot;&gt;paid library&lt;/a&gt;, with a per-year, per-developer licensing model that can be costly. While a &lt;a href=&quot;https://store.sencha.com/&quot;&gt;free community version exists&lt;/a&gt;, it appears to have a very limited feature set.&lt;/p&gt;&lt;h4 id=&quot;jquery-ui-2007&quot;&gt;jQuery UI (2007)&lt;/h4&gt;&lt;p&gt;jQuery UI emerged in 2007 as an official companion library to jQuery, at a time when the JS ecosystem was fragmented and riddled with browser inconsistencies. It was developed to bring a unified, extensible suite of widgets, effects, and interactions to the web. jQuery UI offered an easy-to-integrate API that drastically lowered the barrier for implementing rich UI&#39;s, with full cross-browser compatibility. It played a crucial role in making dynamic front-end behaviour accessible to developers at all skill levels, becoming a staple in both enterprise and amateur hobbyist applications during the formative years of modern web development.&lt;/p&gt;&lt;p&gt;Although I was a big fan of jQuery and used it extensively across many projects, I never really had the opportunity to use jQuery UI in its entirety. When I did, it was typically for a single component, such as a date picker or drag-and-drop functionality. These components were reliable and well-supported, but required a lot of JS to function, and added complexity that I never felt was acceptable for a single feature. Especially when there were plenty of alternative micro-frameworks available, offering small, focused libraries that solved one problem well. I was far more inclined to take that modular approach than to include an entire suite of UI components unnecessarily.&lt;/p&gt;&lt;p&gt;One resource, I found invaluable at the time, was &lt;a href=&quot;https://microjs.com/&quot;&gt;MicroJS&lt;/a&gt;. It hasn’t been updated in quite some time, but it remains a powerful illustration of how easy it is to cherry-pick the exact only the functionality you need, without burdening your page with hundreds of kilobytes of JS.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-22&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;For this question, just as I did with &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#jquery-core-2006&quot;&gt;jQuery&lt;/a&gt;, my answer is yes, you can still use it. It isn&#39;t updated very often, but it is still updated! The last release being in &lt;a href=&quot;https://github.com/jquery/jquery-ui/releases/tag/1.14.1&quot;&gt;October 2024 with version 1.14.1&lt;/a&gt;&lt;/p&gt;&lt;p&gt;To put the enduring popularity of jQuery and jQuery UI into perspective, the &lt;a href=&quot;https://almanac.httparchive.org/en/2024/javascript#fig-38&quot;&gt;2024 Web Almanac&lt;/a&gt; reports that jQuery is still the most-used JS library on the web, appearing on 74% of pages in the &lt;a href=&quot;https://almanac.httparchive.org/en/2024/methodology&quot;&gt;dataset analysed&lt;/a&gt;. jQuery UI comes in fourth, with a 22% usage rate, Though described as mostly deprecated, it’s a clear reminder of how quickly modern tools become legacy software that must be maintained for decades. The latest release came nearly 18 years after jQuery UI’s first version. That’s an incredible achievement by the jQuery UI team, talk about dedication!&lt;/p&gt;&lt;h4 id=&quot;angularjs-2010&quot;&gt;AngularJS (2010)&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;AngularJS&lt;/strong&gt; and &lt;strong&gt;Angular&lt;/strong&gt; are fundamentally different frameworks that share a name and lineage but are otherwise entirely different. AngularJS (1.x) was based on a Model-View-Controller (MVC) architecture with two-way data binding written in JS with support for ECMAScript 5 (ES5) and some ECMAScript (ES6) features. Angular (2+) was built entirely differently, it has a component-based architecture boasts stronger modularity, enabling both two-way binding and promoting unidirectional data flow. Another major difference is that Angular is written in TypeScript, a distinct superset of JS, that enables better tooling and type safety like Java and many other languages. &lt;strong&gt;Angular&lt;/strong&gt; remains actively used and maintained on the modern web today.&lt;/p&gt;&lt;p&gt;I distinctly remember when Google released AngularJS because I was in Melbourne, Australia, working at a digital media agency. One of the Tech Directors there was raving about it, and how it was going to change Frontend development entirely. In hindsight, I agree with him, but I, personally, don’t believe it was a positive change. Single Page Apps (SPA’s) have had such a huge negative impact on Web Performance and Accessibility. Plus, a lot of the code in many of these SPA frameworks essentially reinvents functionality that’s already built into the browser and the Web Platform as a whole. Let&#39;s not overcomplicate things with framework-managed page state; we have a perfectly good back and forward buttons, thank you very much!&lt;/p&gt;&lt;p&gt;As you can probably tell, I’m not a big fan of SPA&#39;s. Admittedly, they have their place under some circumstances; However, I genuinely think they&#39;re excessive and unnecessarily complicate frontend web development for most applications. But I guess complex is the new simple, right? Anyway, rant over!&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-23&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;&lt;a href=&quot;https://angularjs.org/&quot;&gt;AngularJS&lt;/a&gt;, no. It is no longer actively maintained. &lt;a href=&quot;https://angular.dev/&quot;&gt;Angular&lt;/a&gt;, absolutely, it is still a very popular framework on the modern web, although according to the &lt;a href=&quot;https://2024.stateofjs.com/&quot;&gt;State of JS 2024 survey&lt;/a&gt;, it has been overtaken by &lt;a href=&quot;https://vuejs.org/&quot;&gt;Vue.js&lt;/a&gt; for usage, for the &lt;a href=&quot;https://2024.stateofjs.com/en-US/libraries/front-end-frameworks/&quot;&gt;2nd year running&lt;/a&gt;. Frontend developers can be fickle, often chasing the next shiny framework like a kitten distracted by a dangling set of keys. It will be interesting to see if this decline in Angular usage continues in future JS surveys.&lt;/p&gt;&lt;h4 id=&quot;backbone-2010&quot;&gt;Backbone (2010)&lt;/h4&gt;&lt;p&gt;&lt;a href=&quot;https://backbonejs.org/&quot;&gt;Backbone.js&lt;/a&gt; was released in 2010 by Jeremy Ashkenas, who also created &lt;a href=&quot;https://underscorejs.org/&quot;&gt;Underscore.js&lt;/a&gt; and &lt;a href=&quot;https://coffeescript.org/&quot;&gt;CoffeeScript&lt;/a&gt;. As with AngularJS it was one of the first JS libraries that implemented the MVC architecture pattern to client-side JS. The key features of Backbone were its models, collections, views, router, events, and sync functionality that allowed it to easily communicate with RESTful API&#39;s.&lt;/p&gt;&lt;p&gt;I&#39;ve only ever worked on a Backbone project once, and it was a rapid prototype website for a major airline based in Asia. Given the tight timelines and the client’s high design expectations, we ultimately opted for static HTML in the end, as Backbone’s complexity wasn’t advantageous for a rapid prototyping. In hindsight, had it got to the production stage, then I could see the architecture of backbone being very useful.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-24&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;Backbone is nowhere near as popular as modern frameworks like React, Vue, or Angular, It is still mostly in use on legacy systems. The last version released was in April 1st 2025 v1.6.1, but looking through &lt;a href=&quot;https://github.com/jashkenas/backbone/tags&quot;&gt;the releases&lt;/a&gt;, it seems to only get 1 update per year. According to Wappalyzer it is still in use by around &lt;a href=&quot;https://www.wappalyzer.com/technologies/javascript-frameworks/backbone-js/&quot;&gt;521,000 websites&lt;/a&gt;. The biggest of those being &lt;a href=&quot;https://www.atlassian.com/&quot;&gt;Atlassian&lt;/a&gt;. So in my opinion I&#39;d say you should avoid using it and opt for a more popular framework with a more active community. Refer to the modern alternatives I listed for &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#modern-alternatives-16&quot;&gt;Dojo Toolkit Modern Alternatives&lt;/a&gt; as a starting point.&lt;/p&gt;&lt;h4 id=&quot;knockout-2010&quot;&gt;Knockout (2010)&lt;/h4&gt;&lt;p&gt;During the early 2010s, &lt;a href=&quot;https://knockoutjs.com/&quot;&gt;Knockout.js&lt;/a&gt; was a popular JS library for building dynamic UIs using the &lt;a href=&quot;https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel&quot;&gt;Model-View-ViewModel (MVVM)&lt;/a&gt; pattern. It offered features like declarative bindings and two-way data synchronisation, which made it easier to keep the UI in sync with underlying data without manually manipulating the DOM. Its simplicity, ease of learning, and lack of required tooling, e.g. just drop in a &lt;code&gt;&amp;lt;script&gt;&lt;/code&gt; tag into an HTML page and go, this made it especially appealing at a time when frontend complexity was just beginning to accelerate. Though now largely superseded by modern frameworks, Knockout played a key role in the evolution of reactive web development.&lt;/p&gt;&lt;p&gt;I only used Knockout once for a client in Australia, while I lived there. It was a project that had been passed between offices because it had become a maintenance nightmare. Had it been a better planned build, it would have been a more pleasant experience, but time constraints meant a rapid delivery was prioritised over build quality.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-25&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;The last release was way back in &lt;a href=&quot;https://github.com/knockout/knockout/releases/tag/v3.5.1&quot;&gt;November 2019&lt;/a&gt;, so there&#39;s no way it should be used on a modern website build. Although, according to &lt;a href=&quot;https://www.wappalyzer.com/&quot;&gt;Wappalyzer&lt;/a&gt; it is still used by almost &lt;a href=&quot;https://www.wappalyzer.com/technologies/javascript-frameworks/knockout-js/&quot;&gt;43,000 websites&lt;/a&gt;. So you are very unlikely to come across it, even in legacy systems.&lt;/p&gt;&lt;h5 id=&quot;modern-alternatives-21&quot;&gt;Modern Alternatives&lt;/h5&gt;&lt;p&gt;If you are looking a modern MVVM framework then &lt;a href=&quot;https://vuejs.org/&quot;&gt;Vue.js&lt;/a&gt;, &lt;a href=&quot;https://svelte.dev/&quot;&gt;Svelte&lt;/a&gt;, or &lt;a href=&quot;https://aurelia.io/&quot;&gt;Aurelia&lt;/a&gt; are the way to go as the all fully support the MVVM architectural pattern. But if you aren&#39;t bothered about MVVM then refer to the list of modern alternatives I listed for &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#modern-alternatives-16&quot;&gt;Dojo Toolkit&lt;/a&gt; earlier in the post.&lt;/p&gt;&lt;h2 id=&quot;6-css-workarounds-and-browser-quirks&quot;&gt;6. CSS Workarounds and Browser Quirks&lt;/h2&gt;&lt;h3 id=&quot;old-css-practices&quot;&gt;Old CSS practices&lt;/h3&gt;&lt;h4 id=&quot;sliding-doors-technique&quot;&gt;Sliding Doors Technique&lt;/h4&gt;&lt;p&gt;In the time before &lt;code&gt;border-radius:&lt;/code&gt; existed in CSS, this was a legitimate way of “faking” the look of rounded borders. The technique&#39;s name reflects its implementation and purpose. (nothing to do with the &lt;a href=&quot;https://www.imdb.com/title/tt0120148/&quot;&gt;1998 film starring Gwyneth Paltrow&lt;/a&gt; about getting cheated on!). It was actually a rather ingenious method of creating rounded corners that expanded and contracted both in width and height of the bounding box containing the content. It was also incredibly useful for making your site navigation more interesting. If you are interested in reading all about this technique, check out &quot;&lt;a href=&quot;https://alistapart.com/article/slidingdoors/&quot;&gt;Sliding Doors of CSS&lt;/a&gt;&quot; and &quot;&lt;a href=&quot;https://alistapart.com/article/slidingdoors2/&quot;&gt;Sliding Doors of CSS, Part II&lt;/a&gt;&quot; on &lt;a href=&quot;https://alistapart.com/&quot;&gt;alistapart.com&lt;/a&gt; both from October 2003!&lt;/p&gt;&lt;p&gt;If you are old like me, you can just jump back into full nostalgia mode by visiting the &lt;a href=&quot;https://alistapart.github.io/code-samples/slidingdoors/v1/ex5.html&quot;&gt;final code example here&lt;/a&gt;!&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-26&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;There&#39;s absolutely no need to use it on the modern web, simply use the &lt;code&gt;border-radius&lt;/code&gt; CSS property, it comes with all the positives of the sliding door&#39;s technique (rounded corners), and none of the negatives (higher maintenance, additional downloads, non-semantic wrapper DIVS used only for presentation). And given its &lt;a href=&quot;https://caniuse.com/border-radius&quot;&gt;browser support on the modern web&lt;/a&gt;, you can go rounded corner crazy!&lt;/p&gt;&lt;h4 id=&quot;image-sprites-for-icons&quot;&gt;Image Sprites for Icons&lt;/h4&gt;&lt;p&gt;The history of icon sprites is actually quite a fascinating one (for me at least!), since it is essentially rooted in web performance optimisation. The HTTP/1.1 specification (&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2616&quot;&gt;RFC 2616&lt;/a&gt;) published in June 1999, specifically says in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4&quot;&gt;section 8.1.4&lt;/a&gt; that:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;HTTP/1.1 introduced persistent connections, a departure from the one-off connections of HTTP/1.0 (as detailed in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2616#section-8.1&quot;&gt;section 8.1 of the HTTP/1.1 specification&lt;/a&gt;). This innovation understandably raised concerns within the Network Working Group regarding potential increases in server load across the web.&lt;/p&gt;&lt;p&gt;The web was becoming more and more popular every day around that time, with thousands of new websites being launched daily. Remember the Dot-com &lt;a href=&quot;https://www.tickerhistory.com/p/the-dot-com-bubble-explained&quot;&gt;Bubble that burst in March 2000&lt;/a&gt;, it gives you some indication of just how popular the internet had become across the world!&lt;/p&gt;&lt;p&gt;You&#39;re probably thinking &quot;what has any of this got to do with image sprites?&quot;, well, quite a lot, actually. With HTTP/1.1, every asset on a page meant opening a new TCP connection. So if your site had hundreds of icons, the browser would literally open hundreds of separate connections just to fetch them all (assuming it were allowed to)! And this is why browser connection limits were introduced into browsers. Different browsers chose different limits, but it was usually around 4 to 6 &lt;strong&gt;per domain&lt;/strong&gt;. So in order to reduce the number of TCP connections to download all these image assets, a technique called Image Sprites was invented.&lt;/p&gt;&lt;p&gt;Image Sprites, involved compiling all your small image assets down into a single bigger &lt;strong&gt;background&lt;/strong&gt; image file, then using CSS positioning (and width / height) to essentially mask the image and only show the single image you wanted to show in any particular place. It then only required a single TCP connection to download all the small images (one large image with all the icons), and you&#39;d use some CSS magic to just reposition the background image. This technique was mostly used for image icons, and the CSS for this method would look something like this:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.icon&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&#39;sprite.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; -20px -40px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 16px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 16px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we have an icon that is 16px by 16px and the icon we want to show is positioned at -20px -40px on the background image. Genius! A working example of image sprites can be found &lt;a href=&quot;https://codepen.io/chriscoyier/pen/rZPPOd&quot;&gt;here on CodePen&lt;/a&gt;.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-27&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;Again, no need. Image sprites fell out of favour as a technique when more modern techniques like &lt;a href=&quot;https://ryantrimble.com/blog/what-the-heck-is-an-svg-sprite-sheet.html&quot;&gt;SVG sprites&lt;/a&gt; and &lt;a href=&quot;https://fontawesome.com/&quot;&gt;webfont icons&lt;/a&gt; came along.&lt;/p&gt;&lt;p&gt;The technique was also outdated by new web performance technologies like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/#2-http-2-connection-coalescing&quot;&gt;HTTP/2 + Connection Coalescing&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/&quot;&gt;HTTP/3 + QUIC&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;But those are topics for another blog post, which I just so happen to have written a little about, so check them out (shameless self-promotion, sorry!)&lt;/p&gt;&lt;h4 id=&quot;vendor-prefixes-for-css&quot;&gt;Vendor Prefixes for CSS&lt;/h4&gt;&lt;p&gt;Now I&#39;m pretty sure some readers will be thinking, wait &quot;vendor prefixes for CSS are old best practice??&quot;. They really aren&#39;t as they are a vital technique to stop new CSS additions from &quot;breaking the web&quot;. By slapping on a vendor prefix to a CSS property allows browser vendors to experiment with new CSS features in isolation and then once stabilised in the CSS specifications, and supported by the major browsers, the prefix can be removed in favour of the non-prefixed version. For example, &lt;code&gt;-moz-border-radius&lt;/code&gt; changes to &lt;code&gt;border-radius&lt;/code&gt;. So just to clarify vendor prefixes &lt;strong&gt;aren&#39;t&lt;/strong&gt; old best practice, but the &lt;em&gt;way&lt;/em&gt; they are created within website code &lt;strong&gt;is&lt;/strong&gt;. In the distant past you used to have to manually write out each of the prefixed versions to ensure maximum compatibility with all browsers. Can you imagine writing this out manually for all new CSS features you want to use! Example:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.mybox&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;-webkit-border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* Chrome, Safari, iOS Safari (early versions) */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;-moz-border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;/* Firefox (pre-version 4) */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;-ms-border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;/* Not officially supported, but some old syntax examples include it */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;-o-border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;/* Old Opera (Presto engine, now obsolete) */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;         &lt;span class=&quot;token comment&quot;&gt;/* Standard syntax */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;What a waste of bytes and time! (I know compression pretty much fixes the bytes issue!) With the popularity of tools like &lt;a href=&quot;https://web.archive.org/web/20190815095909/http://compass-style.org/&quot;&gt;Compass&lt;/a&gt; (a Sass Framework), &lt;a href=&quot;https://lesscss.org/&quot;&gt;LESS&lt;/a&gt; mixins, and &lt;a href=&quot;https://www.bourbon.io/&quot;&gt;Bourbon&lt;/a&gt; (a Sass mixin library), or even &lt;a href=&quot;https://stylus-lang.com/&quot;&gt;Stylus&lt;/a&gt; with the &lt;code&gt;nib&lt;/code&gt; plugin, the issue started to be abstracted away from developers.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-them-2&quot;&gt;Can I still use them?&lt;/h5&gt;&lt;p&gt;On the modern web there&#39;s simply no need to &lt;strong&gt;manually&lt;/strong&gt; add prefixes. The best practice is to use &lt;a href=&quot;https://postcss.org/&quot;&gt;PostCSS&lt;/a&gt; with &lt;a href=&quot;https://github.com/postcss/autoprefixer&quot;&gt;Autoprefixer&lt;/a&gt;. Or if you’re using bundlers like &lt;a href=&quot;https://webpack.js.org/&quot;&gt;webpack&lt;/a&gt;, &lt;a href=&quot;https://vite.dev/&quot;&gt;Vite&lt;/a&gt;, or &lt;a href=&quot;https://parceljs.org/&quot;&gt;Parcel&lt;/a&gt;, they all offer out-of-the-box support for Autoprefixer. &lt;a href=&quot;https://rollupjs.org/&quot;&gt;Rollup&lt;/a&gt; also supports Autoprefixer but requires a plugin. With the use of these tools along with &lt;a href=&quot;https://browsersl.ist/&quot;&gt;Browserslist&lt;/a&gt; frontend developers don&#39;t even have to think about prefixes any more (once properly configured). An automation win for all developers!&lt;/p&gt;&lt;h4 id=&quot;heavy-use-of-important-in-css&quot;&gt;Heavy Use of !important in CSS&lt;/h4&gt;&lt;p&gt;In the early days of CSS the use of &lt;code&gt;!important&lt;/code&gt; was a common practice for the enforcement of style overrides. It was often considered a &quot;quick fix&quot; for specificity issues, and used to fix visual inconsistencies in cross-browser scenarios or when using third-party CSS code. However, the overuse of &lt;code&gt;!important&lt;/code&gt; is now widely considered a legacy practice that causes major issues with scalability, testability, and maintenance.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-28&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;Yes, It can still be used in &lt;strong&gt;certain circumstances&lt;/strong&gt; e.g. quick debugging, third-party overrides. But a rule of thumb I&#39;d recommend is, if you are using &lt;code&gt;!important&lt;/code&gt; for &lt;strong&gt;anything&lt;/strong&gt; outside a small set of circumstances, you likely have bigger problems that need to be addressed first. As having predictable specificity in CSS is critical for the long term &quot;health&quot; of any web UI. Its use is a clear red flag that the project may be difficult to work on, as it’s a distinct frontend code smell.&lt;/p&gt;&lt;p&gt;That being said, If you ever &lt;strong&gt;do&lt;/strong&gt; need to use it, you should clearly document the reason(s) why in the code, so future developers clearly understand why. Without clear documentation on why it is being used, a future developer is likely to remove this legacy CSS practice due to its reputation, potentially disrupting page styling elsewhere on the website.&lt;/p&gt;&lt;h3 id=&quot;oldie-hacks&quot;&gt;OldIE hacks&lt;/h3&gt;&lt;p&gt;For anyone lucky enough to have missed the joy of building websites for IE 5, 5.5, and 6 (the browser trilogy nobody asked for, released between 1999 and 2001), rest assured, that statement is absolutely, definitely not sarcastic at all. Promise! These versions of IE were just terrible browsers to work with. Microsoft in their infinite wisdom were following their own interpretation of the browser standards that were still in their infancy, and this often creating proprietary features that no other browser vendor followed or implemented.&lt;/p&gt;&lt;p&gt;Bundled with Windows XP, the popular IE6 unfortunately held back web progress for several years. Its standalone release for Windows 98, Windows ME, Windows NT 4.0, and Windows 2000 further cemented its widespread adoption and prolonged influence across the web. This forced Microsoft to innovate in the browser market, or face obsolescence, when a viable and revolutionary competitor finally emerged to finally move the web forwards! Initially launched in 2002 as &lt;a href=&quot;https://blog.mozilla.org/community/2013/05/13/milestone-phoenix-0-1-released-first-version-of-firefox/&quot;&gt;Phoenix 0.1&lt;/a&gt;, a name symbolising its rise from the ashes of &lt;a href=&quot;https://web.archive.org/web/20080228001822/http://moz.sillydog.org/archives/netscape_9006.php&quot;&gt;Netscape Navigator 9.0.0.6&lt;/a&gt;, this browser was renamed Firebird in early 2003, and later that same year became Firefox, the name it retains today.&lt;/p&gt;&lt;p&gt;And as an avid Firefox user for many years, I&#39;m still delighted it is still around as it has its own HTML engine called Gecko. As the modern browser market is very &lt;a href=&quot;https://webkit.org/&quot;&gt;WebKit&lt;/a&gt; / &lt;a href=&quot;https://www.chromium.org/blink/&quot;&gt;Blink&lt;/a&gt; dominated. Competition in this area is always a good thing to help drive innovation, and move the web forwards!&lt;/p&gt;&lt;p&gt;So, enough of the dusty old history books, what was so bad about IE6? Oh, dear, where do I even begin? It was less &quot;web browser&quot; and more &quot;digital dumpster fire&quot; with a whole host of issues and annoyances listed below. As for the &quot;Can I still use it?&quot; section, trust me you wouldn&#39;t want too! oldIE is thankfully about as common on the modern web as seeing a third-party script that improves web performance! Let us have a look at many oldIE issues in all their glory:&lt;/p&gt;&lt;h4 id=&quot;doctype-fragility&quot;&gt;DOCTYPE fragility&lt;/h4&gt;&lt;p&gt;If there was a single character before the DOCTYPE in IE6 it would trigger quirks mode (see the &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#quirks-mode-layouts&quot;&gt;Quirks Mode Layouts&lt;/a&gt; section earlier in the post for a more detailed explanation). This even included newline, comment, space, or invisible byte characters. As you can imagine this was a nightmare for debugging. And if you weren&#39;t in full control of your HTML coming from the server, you were in for a bad time! Quirks mode essentially set the browser into the legacy rendering mode (mostly IE5 behaviour). If that wasn&#39;t bad enough, it would also interpret the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Box_model&quot;&gt;Box model&lt;/a&gt; differently, causing chaos for layouts! Lastly CSS and layout behaviour would be inconsistent or in some cases, totally broken!&lt;/p&gt;&lt;h4 id=&quot;zoom-1-hack&quot;&gt;zoom: 1 hack&lt;/h4&gt;&lt;p&gt;The &lt;code&gt;zoom: 1&lt;/code&gt; hack forced an element into the &quot;hasLayout&quot; rendering mode, which was a proprietary internal rendering concept in IE6 and IE7. For any element that didn&#39;t “have layout”, it could:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Collapse when floated&lt;/li&gt;&lt;li&gt;Break rendering of child elements&lt;/li&gt;&lt;li&gt;Overflow improperly&lt;/li&gt;&lt;li&gt;Fail to clear floats&lt;/li&gt;&lt;li&gt;Interrupt margin collapse&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I distinctly remember using &lt;code&gt;zoom: 1&lt;/code&gt; a lot with float layouts, which was the main option for CSS layouts at the time. As mentioned above the element would then clear floating elements and also overflow elements correctly.&lt;/p&gt;&lt;h4 id=&quot;underscore-hack&quot;&gt;Underscore Hack&lt;/h4&gt;&lt;p&gt;This was a pretty simple hack to target only &lt;strong&gt;IE6&lt;/strong&gt; and &lt;strong&gt;IE7&lt;/strong&gt; in &lt;strong&gt;quirks mode&lt;/strong&gt;, any property prefixed by an underscore is ignored by modern browsers, but recognised by IE6 and IE7.&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.selector&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;_width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 500px &lt;span class=&quot;token comment&quot;&gt;/* targets IE6 / IE7qm */&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 500px &lt;span class=&quot;token comment&quot;&gt;/* modern browsers */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;asterisk-hack&quot;&gt;Asterisk Hack&lt;/h4&gt;&lt;p&gt;Another simple hack to target IE6 and IE7 that is ignored by modern browsers. It was especially useful for fixing box model issues.&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.selector&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	*&lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100px &lt;span class=&quot;token comment&quot;&gt;/* targets IE6 &amp; IE7 */&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100px &lt;span class=&quot;token comment&quot;&gt;/* modern browsers */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;star-html-hack&quot;&gt;Star HTML Hack&lt;/h4&gt;&lt;p&gt;A slightly different hack that was added to the start of the selector rather than a property within a selector. It would only target IE6 in &lt;strong&gt;standards mode&lt;/strong&gt; (not quirks mode).&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;html .selector&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* all browsers including IE6 */&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin-left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;* html .selector&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* IE6 only - order is important */&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin-left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The order of the above code is important since IE6 could read both selectors but the second one &quot;wins&quot; due to its position in the cascade.&lt;/p&gt;&lt;h4 id=&quot;child-selector-hack&quot;&gt;Child Selector hack&lt;/h4&gt;&lt;p&gt;Another straightforward hack to target ONLY modern browsers (not IE6) was:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;ul &gt; li&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* not understood by IE6 &amp; IE7 */&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; rebeccapurple&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* &quot;rebeccapurple&quot; not understood by IE6 &amp; IE7 */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;IE6 &amp; IE7 didn&#39;t understand the child selector, so it simply ignored it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: the colour is also historically significant in the web community as the &lt;code&gt;rebeccapurple&lt;/code&gt; CSS color keyword was added as a tribute to web pioneer &lt;a href=&quot;https://meyerweb.com/&quot;&gt;Eric Meyer&lt;/a&gt;&#39;s daughter &lt;a href=&quot;https://meyerweb.com/eric/thoughts/category/personal/rebecca/&quot;&gt;Rebecca&lt;/a&gt; who passed away of brain cancer at the age of six. Eric, if I’m ever lucky enough for you to see this post, please know that as a brain cancer survivor myself who has personally seen the impact this terrible disease has on friends and family, my heart truly goes out to you and your family. I am so sorry for what you’ve all been through. Rest in peace, Rebecca.&lt;/p&gt;&lt;h4 id=&quot;double-margin-float-bug&quot;&gt;Double Margin Float Bug&lt;/h4&gt;&lt;p&gt;This is a pretty aptly named CSS bug fix, as it does exactly what it says on the tin! Due to IE6&#39;s crazy box model interpretation it sometimes liked to double the margin on floated elements.&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.floated-element&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;left&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inline&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* fix the double margin bug in IE6 */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;peekaboo-bug-fix&quot;&gt;Peekaboo bug fix&lt;/h4&gt;&lt;p&gt;IE6 sometimes threw its toys out of the pram when content changed on the page, in these cases it would either render them incorrectly or just make them disappear, hence the name peekaboo!&lt;/p&gt;&lt;p&gt;To fix it, you had to apply &quot;hasLayout&quot; to the element:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.buggy-element&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* trigger hasLayout on the element to fix the peekaboo bug */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;transparent-png-fix&quot;&gt;Transparent PNG fix&lt;/h4&gt;&lt;p&gt;IE6 was unable to display the alpha channel (transparency) in PNG files, instead it just interpreted it as a grey, solid background. Combined with rounded corners on a non-solid background colour background, this was a real pain in the backside! Thankfully, IE6 supported CSS filters, allowing Microsoft to offer its proprietary AlphaImageLoader for PNG transparency. There were 2 methods to apply this filter:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.transparent-png&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;behavior&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;iepngfix.htc&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;.htc&lt;/code&gt; file contained JS logic that dynamically applied Microsoft’s proprietary filter to the PNG elements.&lt;/p&gt;&lt;p&gt;An alternative for background images was this:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.transparent-bg&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;progid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;DXImageTransform.Microsoft.&lt;span class=&quot;token function&quot;&gt;AlphaImageLoader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
	src=&lt;span class=&quot;token string&quot;&gt;&#39;image.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	sizingMethod=&lt;span class=&quot;token string&quot;&gt;&#39;crop&#39;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Yep, how horrible is that! 🤮&lt;/p&gt;&lt;p&gt;But even if you solved the transparency issue your problems weren&#39;t over as its usage came with a few caveats:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;AlphaImageLoader broke CSS &lt;code&gt;background-position&lt;/code&gt; and &lt;code&gt;background-repeat&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;The fix didn&#39;t work on &lt;code&gt;&amp;lt;img&gt;&lt;/code&gt; tags unless additional &lt;code&gt;.htc&lt;/code&gt; behaviour hacks were applied!&lt;/li&gt;&lt;li&gt;&lt;code&gt;.htc&lt;/code&gt; files could be blocked fairly easily by companies with restrictive internet policies, so it wasn&#39;t always guaranteed to work. And if this happened to be one of your clients... well, there goes the whole design!&lt;/li&gt;&lt;li&gt;Lastly these hacks only worked in IE5.5 and IE6, thankfully IE7 supported transparent PNG&#39;s so these hacks had to be targeted at IE5.5 and IE6 only.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;lack-of-ie-developer-tools&quot;&gt;Lack of IE Developer Tools&lt;/h4&gt;&lt;p&gt;I know the bugs above sound frustrating, but one of the biggest issues was the fact that IE6 and IE7 completely lacked any &quot;sane&quot; developer tools to help resolve these issues. Initially, developers had to rely only on &lt;code&gt;alert()&lt;/code&gt; or &lt;code&gt;document.write()&lt;/code&gt; for debugging! This was where the infamous &lt;code&gt;[object Object]&lt;/code&gt; or &lt;code&gt;[object] [object]&lt;/code&gt; came from when debugging JS in IE6 and IE7. If you had a JS error that you wanted to investigate, that was just about all the information you were given from &lt;code&gt;alert()&lt;/code&gt;. Thankfully, &lt;a href=&quot;https://github.com/firebug/firebug-lite&quot;&gt;Firebug Lite&lt;/a&gt; was released later by the Firebug Working Group. It wasn&#39;t developed by Mozilla and it wasn&#39;t a browser extension. It was a JS file that you could include in your page or as a bookmarklet to mimic the Firebug debugging tools. It was only later in IE8 that Microsoft included the first native developer tool in IE. You enabled it by pressing F12, it finally allowed developers to use the &lt;code&gt;console&lt;/code&gt; functionality that we still use today. Interestingly, F12 to open the browser DevTools still remains the standard across most modern browsers!&lt;/p&gt;&lt;h4 id=&quot;ie-conditional-comments&quot;&gt;IE Conditional Comments&lt;/h4&gt;&lt;p&gt;Conditional comments were introduced into Internet Explorer in IE5, released in March 1999. They were introduced to Internet Explorer by Microsoft because IE had its own interpretation of HTML, CSS, and JS that diverged from W3C standards. This made it extremely difficult for Frontend developers to build consistent UI&#39;s that worked across all browsers, without resorting to complex and brittle CSS hacks as seen earlier in the post. Conditional comments allowed developers to have the best of both worlds, a fully W3C-compliant CSS file for modern browsers, and a CSS file that only applied to certain versions of Internet Explorer (that contained all the version-specific hacks!).&lt;/p&gt;&lt;p&gt;It&#39;s worth noting that each version of IE had its own specific hacks, so you&#39;d often see multiple Conditional Comments in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; of a website &lt;strong&gt;after&lt;/strong&gt; the W3C-compliant stylesheet (due to CSS loaded after the modern version taking precedence due to the cascade). The hacks were still there, but they were sectioned off into their own CSS file(s), ready to be removed when the browser was no longer used by the vast majority of site users. There were countless different ways to write conditional comments due to the version logic built into the interpreter. Note that modern browsers ignored them entirely as they were seen as standard HTML comments e.g. (&lt;code&gt;&amp;lt;!--&lt;/code&gt; and &lt;code&gt;--&gt;&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;Some example conditional comments can be found below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Target only Internet Explorer 6 --&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if IE 6]&gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;styles-ie6.css&quot;&gt;
&amp;lt;![endif]--&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Target IE 7 and lower --&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if lte IE 7]&gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;styles-ie7-and-below.css&quot;&gt;
&amp;lt;![endif]--&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Target IE 8 only --&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if IE 8]&gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;styles-ie8.css&quot;&gt;
&amp;lt;![endif]--&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Target IE 9 and above --&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if gte IE 9]&gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;styles-ie9-and-up.css&quot;&gt;
&amp;lt;![endif]--&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Target any version of IE --&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if IE]&gt;
  &amp;lt;script src=&quot;polyfills-for-ie.js&quot;&gt;&amp;lt;/script&gt;
&amp;lt;![endif]--&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Exclude all versions of IE (i.e. target modern browsers only) --&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if !IE]&gt; --&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;modern-browser-script.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- &amp;lt;![endif]--&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Combine conditions: Target IE 6 to 8 --&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if (gte IE 6)&amp;(lte IE 8)]&gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;legacy-ie6-to-ie8.css&quot;&gt;
&amp;lt;![endif]--&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Explanation of the Condition Syntax:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;IE&lt;/code&gt; matches any version of Internet Explorer.&lt;/li&gt;&lt;li&gt;&lt;code&gt;IE 6&lt;/code&gt;, &lt;code&gt;IE 7&lt;/code&gt;, &lt;code&gt;IE 8&lt;/code&gt;, etc. match specific versions.&lt;/li&gt;&lt;li&gt;lte = less than or equal to&lt;/li&gt;&lt;li&gt;gte = greater than or equal to&lt;/li&gt;&lt;li&gt;!IE = not Internet Explorer&lt;ul&gt;&lt;li&gt;Note the use of the &lt;code&gt;&amp;lt;!--&gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;![endif]--&gt;&lt;/code&gt; to properly close in non-IE browsers. This was known as a downlevel-revealed comment.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;As you can see from the complexity of the above examples it was fairly simple to target very specific and multiple versions of Internet Explorer. A significant drawback was the increased maintenance burden and the clutter they introduced within the sites&#39; &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag.&lt;/p&gt;&lt;p&gt;Thankfully, Microsoft removed the parsing of conditional comments in IE10 and IE11 before they eventually introduced a whole new browser called Microsoft Edge. Edge initially used a proprietary rendering engine called &lt;a href=&quot;https://en.wikipedia.org/wiki/EdgeHTML&quot;&gt;EdgeHTML&lt;/a&gt;. However, the browser was subsequently rewritten to incorporate the same open-source engine as Google Chrome. This new version, based on Chromium 79, was released as &lt;a href=&quot;https://blogs.windows.com/msedgedev/2020/01/15/upgrading-new-microsoft-edge-79-chromium/&quot;&gt;Microsoft Edge 79&lt;/a&gt; on January 15, 2020.&lt;/p&gt;&lt;h4 id=&quot;ie-css-selector-limit&quot;&gt;IE CSS Selector Limit&lt;/h4&gt;&lt;p&gt;This issue, of all those detailed in this section, is arguably one of the most random, as well as one of the least noticeable! IE6 to IE9 had a selector limit of 4,095 selectors per stylesheet, now that may sound like a lot, but it was very straightforward to go over this limit, especially when grouping selectors. For Example:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* This counts as a single selector */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.my-selector&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&#39;s all straightforward, but then you look at something like this:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* This block counts as three selectors */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.button-primary, .button-secondary, .button-tertiary&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you started grouping selectors for easier maintenance it became far too easy to hit the limit, especially on large websites. If you were a user of &lt;a href=&quot;https://getbootstrap.com/&quot;&gt;Bootstrap&lt;/a&gt; or &lt;a href=&quot;https://get.foundation/&quot;&gt;Foundation&lt;/a&gt; at the time you could hit this limit unintentionally, without even knowing it!&lt;/p&gt;&lt;p&gt;And that brings me onto my next point: What happened when that limit was reached? Well... nothing really, IE just didn&#39;t parse &lt;strong&gt;any&lt;/strong&gt; CSS beyond the 4,095 selector limit. Would it warn you that this was happening? Absolutely not!&lt;/p&gt;&lt;p&gt;Developers were often extremely fortunate if this issue was discovered through testing pages styled later in the stylesheet. Internet Explorer itself, however, would simply fail without any error messages or warnings.&lt;/p&gt;&lt;p&gt;And, to make it even more confusing: it would only impact the specific stylesheet that had exceeded the limit, not the page as a whole. For example:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;base.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 2000 selectors --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;theme.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 4500 selectors --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;overrides.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 300 selectors --&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In IE6 - IE9 this is what would happen:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;code&gt;base.css&lt;/code&gt; loads perfectly fine, it is under the limit.&lt;/li&gt;&lt;li&gt;&lt;code&gt;theme.css&lt;/code&gt; only the first 4095 selectors are parsed, the rest are silently ignored.&lt;/li&gt;&lt;li&gt;&lt;code&gt;overrides.css&lt;/code&gt;would be load fully since it is under the limit.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This behaviour creates a partial styling issue. Elements relying on the &lt;code&gt;theme.css&lt;/code&gt; file won&#39;t be styled correctly beyond the 4095 selector limit. In such cases, most pages will appear normal until a page attempts to utilise style selectors 4096 through 4500 from the theme file, at which point it will fail without warning. And of course if you were unlucky enough to be working with IE6 or IE7, then you had no developer tools to even debug the issue either!&lt;/p&gt;&lt;h5 id=&quot;solution&quot;&gt;Solution&lt;/h5&gt;&lt;p&gt;So what was the solution? Well, with the invention preprocessors like Sass or tooling like Grunt, Gulp, or PostCSS they could automate the splitting of stylesheets at the 4065 limit.&lt;/p&gt;&lt;p&gt;Or another solution was to supply a simplified UI to IE browsers &lt;strong&gt;only&lt;/strong&gt; and serve those CSS files via IE&#39;s &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#ie-conditional-comments&quot;&gt;Conditional Comments&lt;/a&gt;. But can you imagine the maintenance involved in updating multiple different stylesheets? Just for the slightest UI change!&lt;/p&gt;&lt;p&gt;The final approach involved reducing reliance on external stylesheets by inlining critical CSS directly into the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; of the page, specifically for &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#above-the-fold-obsession&quot;&gt;above-the-fold&lt;/a&gt; content (we’ll come back to that strategy later, as it’s not as relevant today). Even thinking about these different maintenance options, and their implications gives me a headache!&lt;/p&gt;&lt;h4 id=&quot;oldie-hacks-summary&quot;&gt;OldIE hacks Summary&lt;/h4&gt;&lt;p&gt;As you can imagine CSS files around this time were a &lt;del&gt;bit&lt;/del&gt; lot of a mess with all these random cross browser hacks and workarounds! Thankfully Microsoft recognised it was an issue so decided to implement conditional comments in IE5-IE9 to make this madness a little easier (in terms of organisation, not coding)&lt;/p&gt;&lt;h2 id=&quot;7-markup-of-the-past&quot;&gt;7. Markup of the Past&lt;/h2&gt;&lt;h3 id=&quot;xhtml-1-1-and-2-0&quot;&gt;XHTML 1.1 and 2.0&lt;/h3&gt;&lt;p&gt;I remember having a conversation with a friend about how he was converting his website into a new standard that had just come out, this was around 2001 and the new standard was XHTML 1.1. The most obvious difference at the time was the DOCTYPE at the top of the page source. From HTML 4.01 Strict:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01//EN&quot;&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/html4/strict.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;to:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD XHTML 1.1//EN&quot;&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;what-did-xhtml-1-1-aim-to-achieve&quot;&gt;What did XHTML 1.1 aim to achieve?&lt;/h4&gt;&lt;p&gt;The goal of this new standard was to modularise XHTML and enforce stricter XML compliance. Its key features were the fact that it was based on XHTML 1.0 Strict but split into modules for better reusability and extensibility. It also required documents to be well-formed XML, and it enforced stricter syntax than HTML like all tags must close, and all attributes must be quoted.&lt;/p&gt;&lt;p&gt;Unfortunately, for XHTML 1.1 it came with a number of limitations that doomed the specification from the start. These included, very little browser support for serving XHTML 1.1 as &lt;code&gt;application/xhtml+xml&lt;/code&gt;, it also, more critically broke backwards compatibility in many real-world use cases. And lastly many developers continued to write XHTML but serve it as &lt;code&gt;text/html&lt;/code&gt; which entirely defeated the point of writing XHTML in the first place!&lt;/p&gt;&lt;p&gt;Because it never gained wide browser support and required a very strict syntax, it eventually became obsolete and is now mostly of interest for historical or academic reasons.&lt;/p&gt;&lt;h4 id=&quot;what-did-xhtml-2-0-aim-to-achieve&quot;&gt;What did XHTML 2.0 aim to achieve?&lt;/h4&gt;&lt;p&gt;XHTML 2.0 was never officially released or used in any production browsers. Had it done so, this would have been the DOCTYPE:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD XHTML 2.0//EN&quot;&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/MarkUp/DTD/xhtml2.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Development began on the specification in the early 2000s with the key goals its key goals being:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;a clean break from HTML 4 and XHTML 1.x.&lt;/li&gt;&lt;li&gt;planned to introduce entirely new ideas like replacing &lt;code&gt;&amp;lt;a&gt;&lt;/code&gt; with &lt;code&gt;&amp;lt;h&gt;&lt;/code&gt; for links.&lt;/li&gt;&lt;li&gt;high up on its priority list was for it to be device-agnostic and semantically pure.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The XHTML 2.0 specification ultimately failed for the following reasons:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It lacked practical browser support and Implementation.&lt;/li&gt;&lt;li&gt;HTML5 (developed by &lt;a href=&quot;https://whatwg.org/&quot;&gt;WHATWG&lt;/a&gt;) gained real traction by improving the existing HTML and maintaining compatibility.&lt;/li&gt;&lt;li&gt;And the gigantic final nail in the XHTML 2.0 specification was its complete lack of backwards compatibility. It broke the entire ecosystem of existing web pages and tools.&lt;/li&gt;&lt;li&gt;The &lt;a href=&quot;https://www.w3.org/News/Public/pnews-20090706?utm_source=chatgpt.com&quot;&gt;W3C officially halted XHTML 2.0 in July 2009&lt;/a&gt; and shifted efforts to HTML5.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This then brings us onto the modern standard still in use today, and finally a DOCTYPE that was easy to remember! Look how developer-friendly it is:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is so simple in fact it is case-insensitive and doesn&#39;t require a cumbersome URI to the Document Type Definition (DTD), which was there to define the structure, rules, and legal elements and attributes used on a page.&lt;/p&gt;&lt;h3 id=&quot;inline-javascript&quot;&gt;Inline JavaScript&lt;/h3&gt;&lt;p&gt;There are 2 distinct methods of using inline JS in an HTML document. They both have specifications in the &lt;a href=&quot;https://html.spec.whatwg.org/&quot;&gt;HTML Standard (WHATWG)&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;inline-script-block&quot;&gt;Inline Script Block&lt;/h4&gt;&lt;p&gt;The inline block is defined in &lt;a href=&quot;https://html.spec.whatwg.org/#the-script-element&quot;&gt;section 4.12.1&lt;/a&gt; The syntax is simple and familiar. An example usage is as follows:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;en&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;no-js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;utf-8&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Title here&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;
			document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documentElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;className &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documentElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;className&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;no-js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- The script can also be placed in the body too --&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s perfectly fine to use the inline script block in this way, and it in no way a legacy / outdated technique. But it does come with a few things worth considering before using it. Inline script blocks like this cannot be run &lt;code&gt;async&lt;/code&gt; or &lt;code&gt;defer&lt;/code&gt;, these attributes only apply when loading external scripts using the &lt;code&gt;src&lt;/code&gt; attribute.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-29&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Yes, you can, but it comes with a few caveats. A script in this position in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; executes immediately and &lt;strong&gt;synchronously&lt;/strong&gt;, and it will &lt;strong&gt;block page rendering&lt;/strong&gt; until the JS code completes. So make sure you don&#39;t overload an inline script block as your website will pay the price in terms of frontend web performance.&lt;/p&gt;&lt;h4 id=&quot;inline-event-handler-or-html-event-attribute&quot;&gt;Inline Event Handler or HTML Event Attribute&lt;/h4&gt;&lt;p&gt;Inline event handlers are defined in &lt;a href=&quot;https://html.spec.whatwg.org/#global-attributes&quot;&gt;section 3.2.6&lt;/a&gt; and &lt;a href=&quot;https://html.spec.whatwg.org/#scripting&quot;&gt;section 8.1&lt;/a&gt; of the &lt;a href=&quot;https://html.spec.whatwg.org/&quot;&gt;HTML Standard (WHATWG)&lt;/a&gt; Inline event handlers are now considered a legacy pattern in modern web development. This is due to several reasons, including the security risk posed by JS execution in the global scope, the potential for Cross-Site Scripting (XSS) vulnerabilities, and the way they clutter the code. An example of an inline event handler is below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- other page code here --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;onclick&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-javascript javascript value&quot;&gt;&lt;span class=&quot;token function&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;About clicked&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/about&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;About Us&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- other page code here --&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The simple example above captures the &lt;code&gt;onclick&lt;/code&gt; event from the anchor, and instead of taking you to the about page which you would expect it simply brings up an alert box with the &quot;About clicked&quot; string.&lt;/p&gt;&lt;p&gt;There are other reasons why this technique is considered legacy too since they conflict with the principle of &lt;a href=&quot;https://en.wikipedia.org/wiki/Separation_of_concerns#HTML,_CSS,_JavaScript&quot;&gt;separation of concerns&lt;/a&gt;, and also make debugging, testing, and the implementation of accessibility best practices harder. Lastly, Content Security Policy (CSP) can also disallow inline event handlers unless explicitly allowed (another big security risk!).&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-30&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;No! Don&#39;t use this outdated technique to add JS interactivity to your page. Instead you should move towards external scripts and &lt;a href=&quot;https://en.wikipedia.org/wiki/Unobtrusive_JavaScript&quot;&gt;unobtrusive JavaScript&lt;/a&gt;. An example of which is given below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- other page code here --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;myclass&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/about&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;About Us&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- other page code here --&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// assuming the element.myclass is already in the DOM&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; el &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.myclass&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
el&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;click&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// Do JS stuff here!&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This assumes that the &lt;code&gt;myclass&lt;/code&gt; element is already in the DOM. If it isn&#39;t &lt;code&gt;document.querySelector&lt;/code&gt; will return null and result in a TypeError. The safest way to get around this would be to use the &lt;code&gt;DOMContentLoaded&lt;/code&gt; event. An example of this given below:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;DOMContentLoaded&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; el &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.myclass&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;el&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    el&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;click&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Do JS stuff here!&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you&#39;re thinking that&#39;s a lot of code just to add a click event to a single element! Then you&#39;d be correct, hence why libraries like jQuery were so incredibly popular for &lt;a href=&quot;https://api.jquery.com/category/events/&quot;&gt;event additions&lt;/a&gt; and basic &lt;a href=&quot;https://api.jquery.com/category/manipulation/&quot;&gt;DOM manipulation&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;document-write&quot;&gt;Document.write()&lt;/h3&gt;&lt;p&gt;I must admit, I don&#39;t think I&#39;ve every actually used &lt;code&gt;Document.write()&lt;/code&gt; on a webpage, that&#39;s probably because I&#39;ve never seen a sane reason to use it! It&#39;s a JS method provided by the browser’s DOM, that allows you to write HTML or text directly to the page. A simple example is given below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;
	document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;h1&gt;Hello, world!&amp;lt;/h1&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Wherever this code is placed in the page, it will simply output a h1 with the content of Hello, world! Now there&#39;s a reason I was so harsh on the method above, and that&#39;s because it comes with some horrible side effects. These include the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;As with any Inline Script Block, it blocks page rendering until the content is written to the page.&lt;/li&gt;&lt;li&gt;It runs synchronously and can block scripts and other resources from loading efficiently.&lt;/li&gt;&lt;li&gt;Lastly, and this has to be the best (and most horrifying feature!). If used after the page has fully loaded, it can erase the entire DOM and replace it with whatever was passed to the method. This would be &lt;code&gt;&amp;lt;h1&gt;Hello, world!&amp;lt;/h1&gt;&lt;/code&gt; in the example given above.&lt;/li&gt;&lt;li&gt;It&#39;s important to consider the security implications of its usage as it is similar to &lt;code&gt;eval()&lt;/code&gt;(&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/(https:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eva)&quot;&gt;MDN link&lt;/a&gt; in some ways (but not all). Both methods can enable cross-site scripting (XSS) if user input is injected without sanitisation.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;when-should-it-be-used&quot;&gt;When should it be used?&lt;/h4&gt;&lt;p&gt;On the modern web the simple answer is &lt;strong&gt;never!&lt;/strong&gt; It&#39;s best seen as a historical curiosity that some legacy systems may still use that haven&#39;t been modernised yet. It could also be used in elementary educational examples or demos. There are a number of much safer (and robust) modern alternatives that should be used instead. These include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;element.innerHTML&lt;/code&gt; (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML&quot;&gt;MDN Link&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;&lt;code&gt;element.textContent&lt;/code&gt; (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent&quot;&gt;MDN Link&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;&lt;code&gt;document.createElement()&lt;/code&gt; (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement&quot;&gt;MDN link&lt;/a&gt;) in conjunction with &lt;code&gt;appendChild&lt;/code&gt; and &lt;code&gt;insertBefore&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Modern frameworks or libraries for manipulating the DOM and updating the UI.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;fixed-viewport-meta-tags&quot;&gt;Fixed Viewport Meta Tags&lt;/h3&gt;&lt;p&gt;Fixed viewport meta tags were used in early mobile responsive development. An example of what it looks like is below:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;My Fixed width Mobile Site&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;viewport&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;width=320, user-scalable=no&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the example above the &lt;code&gt;viewport&lt;/code&gt; meta tag is telling the browser that:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;This website should be rendered at a fixed width of 320px&lt;/li&gt;&lt;li&gt;It should disable user scaling, so the layout is locked to a specific dimension, regardless of the actual screen size.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;why-use-this-approach&quot;&gt;Why use this approach?&lt;/h4&gt;&lt;p&gt;In the early days of mobile development, desktop websites were very difficult to view and interact with on small mobile screens. In order to &quot;fix&quot; this issue, developers often built mobile specific websites, that would sit alongside the desktop website. 320px was a popular width at the time because the first iPhone (iPhone 3G) had a 320px size screen. In order to maintain maximum control over the layout appearance on these mobile devices, developers frequently prevented users from zooming into these sites. These restrictions also helped avoid layout shifts when loading in dynamic viewport sizes e.g. device orientation (portrait vs landscape), zoom or pitch gestures, changes in browser UI elements (address bar or toolbars).&lt;/p&gt;&lt;h4 id=&quot;why-was-this-bad&quot;&gt;Why was this bad?&lt;/h4&gt;&lt;p&gt;There were a number of reasons as to why this technique was bad. The number one being it was terrible for accessibility. A user with a visual impairment, on a mobile site that disabled pinch-to-zoom (&lt;code&gt;user-scalable=no&lt;/code&gt;), had no way to read the site&#39;s content. Secondly, by dictating a screen width, you are harming both adaptability and making assumptions about a user&#39;s device. Devices come in all shapes, sizes, and pixel densities. Mobile, tablet, desktop, and every resolution in-between. There are literally an infinite number of screen dimensions possible, obviously many of those would be impractical for users beyond a certain range, but it&#39;s impossible to maintain all these fixed versions so this technique quickly become outdated. Lastly, fixed sizes can lead to performance issues, as they may cause unnecessary UI reflows and repaints when used with older layout methods such as tables or fixed-position elements.&lt;/p&gt;&lt;h4 id=&quot;modern-best-practice&quot;&gt;Modern Best Practice&lt;/h4&gt;&lt;p&gt;With the development of responsive design practices by &lt;a href=&quot;https://follow.ethanmarcotte.com/@beep&quot;&gt;Ethan Marcotte&lt;/a&gt; in 2010 (&lt;a href=&quot;https://alistapart.com/article/responsive-web-design/&quot;&gt;Responsive Web Design: A List Apart&lt;/a&gt;), fixed layouts quickly fell out of fashion, as with responsive design, you could develop a single UI that worked on all devices, no matter what their screen size or pixel density. One UI to rule them all! It comes with the huge advantages of much less maintenance for developers, and more importantly a huge usability for all users, no matter what device they are viewing a website on. The recommended &lt;code&gt;viewport&lt;/code&gt; meta tag for modern websites is this:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;viewport&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;width=device-width, initial-scale=1&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This tells a user&#39;s browser:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;to match the screen’s actual width (&lt;code&gt;width=device-width&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;to set the base zoom level (&lt;code&gt;initial-scale=1&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;to allow users to zoom the viewport (&lt;code&gt;user-scalable=yes&lt;/code&gt; this is default if not set)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;You may come across fixed layouts in legacy applications, and if you do, you should seriously consider:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Replacing fixed tags with scalable ones.&lt;/li&gt;&lt;li&gt;Refactoring CSS layout logic to use &lt;a href=&quot;https://css-tricks.com/books/greatest-css-tricks/flexible-grids/&quot;&gt;flexible grids&lt;/a&gt;, &lt;a href=&quot;https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/&quot;&gt;fluid typography&lt;/a&gt;, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries&quot;&gt;media queries&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Ensuring accessibility standards are upheld, especially zooming support (up to 200% as specified in &lt;a href=&quot;https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-scale.html&quot;&gt;WCAG Success Criterion 1.4.4&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;web-safe-fonts-only-before-font-face&quot;&gt;Web Safe Fonts Only (before @font-face)&lt;/h3&gt;&lt;p&gt;Fonts are arguably the most crucial component of the web. Without them, there would be no content, and consequently, no internet. This fundamental importance explains the extensive nature of the &lt;a href=&quot;https://drafts.csswg.org/css-fonts-4/&quot;&gt;CSS Fonts Module Level 4&lt;/a&gt; documentation. Fonts present a challenge due to their vast variety and subjective nature. What one person finds legible, another may not.&lt;/p&gt;&lt;p&gt;Web-safe fonts are typefaces that are broadly supported and consistently rendered across most web browsers and operating systems, eliminating the need for users to install additional fonts. They are distinguished by three primary characteristics:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;They come pre-installed on most devices across all operating systems (Windows, macOS, Linux, iOS, and Android).&lt;/li&gt;&lt;li&gt;They render consistently across browsers, devices, and operating systems.&lt;/li&gt;&lt;li&gt;If a font isn&#39;t available on a certain device, there&#39;s a viable alternative that can be used by default, this is called &quot;fallback safety&quot;.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;A common fallback font family using the &lt;code&gt;font-family&lt;/code&gt; CSS property is displayed below:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.class&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Arial&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Helvetica Neue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Helvetica&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;According to &lt;a href=&quot;https://www.cssfontstack.com/Arial&quot;&gt;CSS Font Stack&lt;/a&gt; this combination of fonts is supported by 99.84% of devices on Windows, and 98.74% on Mac. Notice how it gives the browser a list of fonts, the primary choice being Arial, and if Arial isn&#39;t available then Helvetica Neue will be used, all the way down to &lt;code&gt;sans-serif&lt;/code&gt;. This is basically saying &quot;if none of the preceding fonts are available, then choose any &lt;a href=&quot;https://en.wikipedia.org/wiki/Sans-serif&quot;&gt;sans-serif&lt;/a&gt; font on the device&quot;. This guarantees that a font will always be available on any device, so even though different fonts will be used depending on the operating system, the page content will still be rendered and readable for all users.&lt;/p&gt;&lt;h4 id=&quot;common-web-safe-fonts&quot;&gt;Common Web Safe Fonts&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;Arial&lt;/li&gt;&lt;li&gt;Times New Roman&lt;/li&gt;&lt;li&gt;Verdana&lt;/li&gt;&lt;li&gt;Georgia&lt;/li&gt;&lt;li&gt;Courier New&lt;/li&gt;&lt;li&gt;Trebuchet MS&lt;/li&gt;&lt;li&gt;Lucida Console&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The issue with these fonts is that they are very limiting, especially for the Design community. Designers have very strong opinions on fonts, it is their &quot;bread and butter&quot;, after all, so that&#39;s to be expected! For years, both developers and designers have strived to integrate all fonts, not just web-safe ones, into web development. In doing so many people on the web came up with different ways to allow them to use non-web safe fonts. These include the methods I mentioned earlier in the post:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#scalable-inman-flash-replacement-sifr&quot;&gt;Scalable Inman Flash Replacement (Sifr)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#cufon&quot;&gt;Cufón&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#gif-text-replacements&quot;&gt;GIF Text Replacements&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;As mentioned earlier all of these methods worked, but they had limitations, be that with Accessibility, Performance, Maintenance, Security, or SEO.&lt;/p&gt;&lt;p&gt;In order to mitigate thes limitations, a modern, standardised method was required for browsers to load custom fonts.&lt;/p&gt;&lt;h4 id=&quot;enter-font-face&quot;&gt;Enter @font-face&lt;/h4&gt;&lt;p&gt;&lt;code&gt;@font-face&lt;/code&gt; is a CSS rule that allows web developers to load custom fonts on a webpage. Unlike the methods listed above it&#39;s a &lt;strong&gt;native browser feature&lt;/strong&gt; that brings typographic control to the web, while also preserving Accessibility, SEO, Maintenance, Security, and Performance (if implemented correctly).&lt;/p&gt;&lt;p&gt;The &lt;code&gt;@font-face&lt;/code&gt; rule has a notable history, as it was initially implemented by Microsoft in&lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_Explorer_4&quot;&gt; Internet Explorer 4&lt;/a&gt; in 1997. At the time it used &lt;a href=&quot;https://en.wikipedia.org/wiki/Embedded_OpenType&quot;&gt;Embedded OpenType (EOT)&lt;/a&gt; fonts. This was a proprietary solution by Microsoft and not a part of the CSS standard at the time, so adoption outside of Microsoft browsers was non-existent. It wasn&#39;t until the W3C developed and standardised the CSS Fonts Module Level 3, that browser support across different vendors started to improve.&lt;/p&gt;&lt;p&gt;Although CSS Fonts Module Level 3 work began in the early 2000s, true standardisation took time as browser vendors adopted open formats like &lt;a href=&quot;https://en.wikipedia.org/wiki/TrueType&quot;&gt;TTF&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/OpenType&quot;&gt;OTF&lt;/a&gt;, and later &lt;a href=&quot;https://en.wikipedia.org/wiki/Web_Open_Font_Format&quot;&gt;WOFF and WOFF2&lt;/a&gt;. The CSS Fonts Module Level 3 was not released as a &lt;a href=&quot;https://www.w3.org/TR/css-fonts-3/&quot;&gt;W3C recommendation until September 2018&lt;/a&gt;. The first CSS Fonts Module Level 3 working draft was &lt;a href=&quot;https://www.w3.org/TR/2001/WD-css3-fonts-20010731/&quot;&gt;published in July 2001&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;usage&quot;&gt;Usage&lt;/h4&gt;&lt;p&gt;So how do we actually use &lt;code&gt;@font-face&lt;/code&gt;? Well, it&#39;s pretty straightforward:&lt;/p&gt;&lt;h5 id=&quot;declaring-the-font&quot;&gt;Declaring the font&lt;/h5&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;MyFont&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&#39;/fonts/myfont.woff2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;woff2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	   &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&#39;/fonts/myfont.woff&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;woff&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	   &lt;span class=&quot;token comment&quot;&gt;/* other font formats here */&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Although you can define other font formats, this is no longer recommended, since the combination of WOFF and WOFF2 will cover all popular browsers. In fact, depending on your user analytics data, you may even be able to drop to only listing WOFF2, since it is now supported by 96.2% of browsers used on the internet according to &lt;a href=&quot;https://caniuse.com/woff2&quot;&gt;Can I Use&lt;/a&gt;.&lt;/p&gt;&lt;h5 id=&quot;using-the-font&quot;&gt;Using the Font&lt;/h5&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;MyFont&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here is where we apply the custom font to the page elements using standard CSS selectors.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT&lt;/strong&gt;: note how &lt;code&gt;sans-serif&lt;/code&gt; has also been set as a fallback e.g. later in the font list. This is best practice because we are loading an external font file to render the text on the page. If this font no longer exists on the server (or simply fails to load), users will be left with no text, since the external font is missing. This ensures that even if the custom font isn&#39;t available it will &quot;fallback&quot; to an appropriate web safe font.&lt;/p&gt;&lt;p&gt;Now there are a number of web performance points that should be considered when using web fonts, but I won&#39;t go into them here. Instead, I will point you towards &lt;a href=&quot;https://www.zachleat.com/&quot;&gt;Zach Leat&lt;/a&gt;&#39;s excellent &quot;The Five Whys of Web Font Loading Performance&quot; article from November 2018, and it also links to his &lt;a href=&quot;https://perfnow.nl/&quot;&gt;Performance.now()&lt;/a&gt; conference talk on the same subject. Well worth a watch if you have a spare 46 minutes!&lt;/p&gt;&lt;h2 id=&quot;8-tools-and-workflow-relics&quot;&gt;8. Tools and Workflow Relics&lt;/h2&gt;&lt;h3 id=&quot;svn-subversion-largely-replaced-by-git&quot;&gt;SVN (subversion, largely replaced by Git)&lt;/h3&gt;&lt;p&gt;SVN (Subversion) is a centralised version control system that was widely used in the 2000s and early 2010s. It was the first versioning system I used at one of the digital agencies I worked at in the late 2000s. The memories that stick with me most about SVN are:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Every folder and sub-folder had an annoying &lt;code&gt;.svn&lt;/code&gt; directory within them. This directory contains all the metadata needed by SVN to manage the versioned files.&lt;/li&gt;&lt;li&gt;Branching and merging in SVN was a painful experience!&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Although in all honesty, I haven&#39;t used it in over a decade, so both these points may have changed and now be invalid? Actually, I doubt it for backwards compatibility with older SVN repositories.&lt;/p&gt;&lt;p&gt;The key word in the top paragraph above is &quot;centralised&quot;. In a version control context that means that with SVN there&#39;s a single central repository that all version history and file management operations are built around.&lt;/p&gt;&lt;p&gt;In comparison, Git / GitHub are decentralised repositories. When you clone a repository onto your local machine you have the whole history of all the files, you can modify them while offline then synchronise with other developers code modifications once you&#39;re back online.&lt;/p&gt;&lt;h4 id=&quot;legacy-development-practices&quot;&gt;Legacy Development Practices&lt;/h4&gt;&lt;p&gt;If SVN is being used in 2025, it could imply certain things about the codebase and teams working practices. These include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The tooling being used is likely to be old (e.g. Eclipse plugins, shell scripts).&lt;/li&gt;&lt;li&gt;Continuous Integration (CI) / Continuous Development (CD) is likely to be very basic or missing entirely.&lt;/li&gt;&lt;li&gt;Due to the complexity of the branching and merging process in SVN, this type of workflow will likely be minimal if used at all!&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;team-cultural-indicators&quot;&gt;Team Cultural Indicators&lt;/h4&gt;&lt;p&gt;There are also red flags in terms of engineering culture if SVN is still being used. It typically indicates that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The engineering team has a conservative engineering culture.&lt;/li&gt;&lt;li&gt;The team have a risk-averse attitude to change.&lt;/li&gt;&lt;li&gt;The team may have a backlog of technical debt that has accumulated over many years.&lt;/li&gt;&lt;li&gt;Recruitment of developers wanting to use SVN is likely to be challenging, as &lt;a href=&quot;https://rhodecode.com/blog/156/version-control-systems-popularity-in-2025&quot;&gt;recent surveys&lt;/a&gt; indicate that SVN has 5.18% of the Version Control System (VCS) market share. It is second to Git that dominates with a 93.87% market share. This is also likely to impact retention of developers too, since Git / GitHub are the dominant tools in use in most industries (although, &lt;a href=&quot;https://get.assembla.com/blog/is-subversion-svn-still-used/&quot;&gt;not all&lt;/a&gt;) in 2025.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;what-to-look-out-for&quot;&gt;What to look out for&lt;/h4&gt;&lt;p&gt;Should you happen to encounter a project that still uses SVN for version control, you should:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Expect resistance to adopt modern workflows (e.g., GitFlow, CI/CD).&lt;/li&gt;&lt;li&gt;Investigate whether the tooling supports migration to Git e.g. &lt;code&gt;git svn&lt;/code&gt; or if a full rewrite might be needed.&lt;/li&gt;&lt;li&gt;Evaluate whether SVN is tightly embedded in the build and deployment process.&lt;/li&gt;&lt;li&gt;Prepare yourself for recruitment and retention issues as mentioned above.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;migration&quot;&gt;Migration&lt;/h4&gt;&lt;p&gt;Assuming you&#39;ve stumbled across a legacy project that uses SVN, and modernisation and migration is a goal for the project. It&#39;s worth knowing that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;SVN to Git migration tools exist (&lt;a href=&quot;https://git-scm.com/docs/git-svn&quot;&gt;git svn&lt;/a&gt; or tools like &lt;a href=&quot;https://subgit.com/&quot;&gt;SubGit&lt;/a&gt;), but edge cases can be painful.&lt;/li&gt;&lt;li&gt;You will likely need to retrain teams and completely refactor deployment automation.&lt;/li&gt;&lt;li&gt;Start with a small component or project to check see if modularisation is a feasible option.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-it-31&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;TL;DR: &lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt; / &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; is the way to go, for modern web development best practice.&lt;/p&gt;&lt;h3 id=&quot;chrome-frame&quot;&gt;Chrome Frame&lt;/h3&gt;&lt;p&gt;Chrome Frame was an ambitious project to bridge the gap between modern web standards and the many limitations of old IE versions (IE6 - IE8). It was released by Google as a plugin for IE in late 2009. It essentially embedded the Chrome browser engine into older IE versions, thus allowing the IE / Chrome hybrid to use modern web standards, modern JS frameworks, HTML5 features, all while still being compatible with an IE-centric environment. This proved particularly beneficial for larger enterprise organisations, which were reliant on older versions of IE due to legacy infrastructure and were unable to adopt more modern browsers.&lt;/p&gt;&lt;p&gt;While it sounded great in theory, unfortunately it came with a whole host of downsides, especially when it came to adoption. This included requiring admin access to machines in order to install the plugin, many companies locked-down the use of plugins due to the security risk involved in installation of 3rd-party code, lastly it introduced complexity for IT support teams and Quality Assurance (QA) teams due to the hybrid nature of the rendering engine.&lt;/p&gt;&lt;h4 id=&quot;can-i-still-use-it-32&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;No, as it was ultimately deprecated by Google in 2013, and support ended in 2014. The reasons for this were that web standards had improved and IE itself had improved with the release of IE9 and later. A significant change in browser releases was the move to &quot;evergreen browsers&quot;. These browsers update automatically in the background, without user intervention. These browser releases were untethered from specific Operating system versions, apart from Safari being a notable exception.&lt;/p&gt;&lt;p&gt;Although Chrome Frame only saw limited success, it certainly helped initiate discussions on migrating from legacy browsers in large enterprise environments.&lt;/p&gt;&lt;p&gt;I distinctly remember when it was announced I thought it would solve all our IE problems (finally!). It was only when it was released that I realised there was no way it would be able to solve the issue because:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;It was complex to install (e.g. required admin access)&lt;/li&gt;&lt;li&gt;The majority of users at the time using older versions of IE were likely neither technically capable nor even interested in installing it as a plugin.&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Note&lt;/strong&gt;: This isn’t meant to sound elitist, but at the time, most people would have likely identified the internet as the &quot;blue ‘e’ icon&quot; on their desktop. Outside the web development community, few knew (or cared) what a web browser was, let alone which one they used! And I&#39;d say this statement is most likely true with the modern web too!&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&quot;css-resets&quot;&gt;CSS Resets&lt;/h3&gt;&lt;p&gt;Before we get into the details, what is a CSS reset? It&#39;s essentially a set of CSS selectors and properties that are used to &quot;reset&quot; all styles across browsers to a common baseline. Think of it as a solid foundation on which to build your website off. In theory if all browsers render elements identically from the start, then the site will be easier to build and maintain because all those nasty minor cross-browser CSS differences will have been dealt with, that&#39;s the theory anyway.&lt;/p&gt;&lt;p&gt;Just to be clear, CSS resets &lt;strong&gt;are&lt;/strong&gt; still around, but they have evolved into something more forward-thinking, minimal, and only focussed on common pain points. The first CSS Reset to be released in January 2007 was &lt;a href=&quot;https://meyerweb.com/eric/tools/css/reset/&quot;&gt;Eric Meyer’s classic CSS Reset&lt;/a&gt;, it quickly became one of the most widely adopted resets to try to standardise styling across the modern browsers at the time, these were Internet Explorer, Firefox, and Safari. It did this by removing all margin, padding, borders, and fonts to a common baseline. It could either be included within your own CSS file, or added as a separate CSS file at the start of your CSS in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag. The order is crucial because you&#39;re establishing a standardised foundation. This allows subsequent CSS files to override these resets, either through direct duplication, leveraging the cascade, or by increasing specificity. For example:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* Basic Reset of the body styling: Specicifity score 0,0,1 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;line-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.5&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; system-ui&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #fff&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Here this selector is overriding the one above because it comes after it in the cascade: Specicifity score 0,0,1 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Comic Sans MS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Impact&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Here we are using CSS Specicifity to override the page background color: Specicifity score 0,1,1  */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;body.colored-background&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ff0000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is why ordering your CSS correctly is best to do right at the start of a project. If you bring in a reset file at the end, it will either do nothing at all due to higher specificity CSS selectors before it, or it will completely undo lots of your styling, simply because it “wins” (by coming last in the cascade e.g. it’s the last CSS file loaded).&lt;/p&gt;&lt;p&gt;As mentioned above, CSS resets have evolved over the years. &lt;a href=&quot;https://necolas.github.io/normalize.css/&quot;&gt;Normalize.css&lt;/a&gt; is a very common one in use on the modern web, as it works differently by preserving useful default browser styles and only fixing CSS styles that need to be fixed to maximise CSS consistency across modern browsers.&lt;/p&gt;&lt;p&gt;Other notable mentions are more modern, minimal resets that only focus on certain pain points in cross-browser rendering like box-sizing, responsive images, and font inheritance. These include &lt;a href=&quot;https://piccalil.li/author/andy-bell/&quot;&gt;Andy Bell&#39;s&lt;/a&gt;: &lt;a href=&quot;https://piccalil.li/blog/a-more-modern-css-reset/&quot;&gt;A (more) Modern CSS Reset&lt;/a&gt; and &lt;a href=&quot;https://www.joshwcomeau.com/about-josh/&quot;&gt;Josh Comeau&lt;/a&gt;&#39;s: &lt;a href=&quot;https://www.joshwcomeau.com/css/custom-css-reset/&quot;&gt;A Modern CSS Reset&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Kudos to the authors of CSS Resets! Their dedication makes CSS authoring significantly smoother for millions of developers across the world.&lt;/p&gt;&lt;h3 id=&quot;hover-only-interactions&quot;&gt;Hover-Only Interactions&lt;/h3&gt;&lt;p&gt;Hover-only interactions are a legacy practice that suited desktop-only contexts but fail in today’s multi-device environment. An example of what a hover-only Interaction is:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.button:hover&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ff0000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hover-only Interactions come with the following issues:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Not accessible on touch devices&lt;/strong&gt;: Touchscreens do not have a hover state. This means hover-only functionality becomes inaccessible on phones and tablets, leading to broken user experiences.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Not accessible on touch devices&lt;/strong&gt;: Touchscreens do not have a hover state. This means hover-only functionality becomes inaccessible on phones and tablets, leading to broken user experiences.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Lack of fallback interaction&lt;/strong&gt;: Many legacy implementations didn&#39;t provide alternative means (like a click or focus) to trigger the same behaviour, effectively hiding essential UI or functionality.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Keyboard accessibility problems&lt;/strong&gt;: Hover interactions are not always accessible via keyboard unless explicitly paired with &lt;code&gt;:focus&lt;/code&gt; or JS handling.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Poor &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;progressive enhancement&lt;/a&gt;&lt;/strong&gt;: Relying solely on hover effects often ignored the principle of progressive enhancement, especially when essential content was hidden using CSS unless hovered.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Inconsistent browser behaviour&lt;/strong&gt;: Legacy browsers had quirks in how they handled hover states, particularly with complex layouts or when mixing JS and CSS interactions.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;modern-best-practice-2&quot;&gt;Modern Best Practice&lt;/h4&gt;&lt;p&gt;UI&#39;s need to be device-agnostic and align with inclusive design principles. Hover-based interactions should be a &lt;strong&gt;supplementary&lt;/strong&gt; interaction, not the &lt;strong&gt;primary&lt;/strong&gt; interaction. In order to align with modern best practice you should:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Avoid hover-only interactions for essential functionality.&lt;/li&gt;&lt;li&gt;Use &lt;code&gt;:focus&lt;/code&gt; alongside &lt;code&gt;:hover&lt;/code&gt;, and consider adding &lt;code&gt;:focus-visible&lt;/code&gt; to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible&quot;&gt;better support keyboard navigation&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Support click or tap events explicitly for mobile compatibility&lt;/li&gt;&lt;li&gt;Provide visible indicators or alternative access methods (e.g. always-visible menus on small screens)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;An example in CSS is:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;.&lt;span class=&quot;token property&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;hover&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* mouse users */&lt;/span&gt;
.&lt;span class=&quot;token property&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;focus&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* element focused via click or tab */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.button:focus-visible&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* user likely on keybord, or other assistive technology */&lt;/span&gt;
	 background-color&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ff0000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above can be simplified to avoid redundant styling as combining &lt;code&gt;:focus&lt;/code&gt; and &lt;code&gt;:focus-visible&lt;/code&gt; can sometimes cause overlapping or unnecessary duplication of visual effects. The recommended approach is to use the following as it keeps your styling clean and scoped, applying just what’s needed based on the user’s input method:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.button:hover,
.button:focus-visible&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ff0000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By avoiding redundant styling means you reduce:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Overlapping in CSS rules.&lt;/li&gt;&lt;li&gt;Maintenance complexity.&lt;/li&gt;&lt;li&gt;Risk of inconsistent behaviour between browsers.&lt;/li&gt;&lt;li&gt;Slightly less to download.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;9-legacy-web-strategies&quot;&gt;9. Legacy Web Strategies&lt;/h2&gt;&lt;h3 id=&quot;blackhat-seo&quot;&gt;Blackhat SEO&lt;/h3&gt;&lt;p&gt;Blackhat SEO refers to a collection of techniques that people tried to use to manipulate search engine rankings, mostly in ways that violate search engine guidelines, especially for guidelines laid out by Google.&lt;/p&gt;&lt;h4 id=&quot;intent&quot;&gt;Intent&lt;/h4&gt;&lt;p&gt;So why would people want to use Blackhat SEO? Well, its sole focus was prioritising rapid results over sustainable growth. Consultants selling these techniques were focussing on exploiting weaknesses in search engine algorithms, rather than creating genuine value for users. Being at the top of the Google results page was the primary goal, and companies were willing to try these techniques to get an edge on their competition. That was until search engines got wise to what was happening, and started to penalise sites that employed these techniques.&lt;/p&gt;&lt;h4 id=&quot;examples&quot;&gt;Examples&lt;/h4&gt;&lt;p&gt;Let&#39;s look over a few outdated examples and how they worked:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Keyword stuffing&lt;/strong&gt;: This was essentially stuffing as many keywords into a page in an attempt to trick the search engine into pushing a page to the top of the results for more search engine searches. So even if the keywords weren&#39;t at all related to the actual content, they were included anyway. Thankfully, search engines got wise to this tactic and cracked down on sites that used it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cloaking&lt;/strong&gt;: This is where you show different content to search engines than you do to users. Search engines would be shown pages with detailed, keyword-rich content about a specific product in order to trick a search engine into ranking the page highly. But the page shown to users was minimal and mainly promotional with very little or no helpful information on the page related to what it was being ranked on.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Hidden text and links&lt;/strong&gt;: This is the one I remember the most, using CSS or HTML to hide text on a page that was only intended for search engines. Think white colour text on a white background, it was that simple! It was also straightforward to spot, as you&#39;d get pages where the scrollbar was huge, but the content on the page was very short. The overflow in the vertical direction was the hidden text that you could easily reveal by highlighting the text with your cursor!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Link farms and paid link schemes&lt;/strong&gt;: This is where companies would create hundreds, or thousands of low-quality content linking back to a specific page, in the hope that the search engine would rank the page highly because of all the backlinks to it. There were (and most likely still are) whole business&#39;s setup that promised to get you to the top of search results by essentially spamming the web like this. If you ever had a WordPress blog without the &lt;a href=&quot;https://akismet.com/&quot;&gt;Akismet&lt;/a&gt; plug-in, you&#39;d see this rapidly! WordPress was highly vulnerable due to its support for &lt;a href=&quot;https://en.wikipedia.org/wiki/Trackback&quot;&gt;Trackbacks&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Pingback&quot;&gt;Pingbacks (XML-RPC)&lt;/a&gt;. Have a look at any old unmaintained WordPress blog, and you are likely to see this. They were so common at one point that a new term called &quot;splogs&quot; (spam blogs) was created for them. Wired.com wrote a blog post all about them back in September 2006 &quot;&lt;a href=&quot;https://www.wired.com/2006/09/splogs/&quot;&gt;Spam + Blogs = Trouble&lt;/a&gt;&quot;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Duplicate content&lt;/strong&gt;: This is a simple strategy, copy another site&#39;s high-quality content and pass it off as your own. Thankfully, this now triggers de-ranking in search engines.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Automated content&lt;/strong&gt;: Basically automating the production of low-quality and spammy content. I anticipate that this technique will see a resurgence, given the recent surge in AI tools.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&quot;This is why we can&#39;t have nice things!&quot; The phrase echoes in my ears as I recall all the techniques mentioned above.&lt;/p&gt;&lt;h4 id=&quot;why-are-they-outdated&quot;&gt;Why are they Outdated?&lt;/h4&gt;&lt;p&gt;There are a number of reasons why these techniques are no longer used:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Google and other search engines have significantly enhanced their algorithms to identify and penalise such manipulative tactics.&lt;/li&gt;&lt;li&gt;Modern SEO is more geared towards user-first metrics, content relevance, quality, user experience, and even &lt;a href=&quot;https://developer.chrome.com/docs/lighthouse/seo/meta-description&quot;&gt;web performance&lt;/a&gt; are now taken into account when ranking a web page.&lt;/li&gt;&lt;li&gt;Sites found to be using these Blackhat tactics are almost certain to be heavy penalised and may even be de-indexed completely from search engines.&lt;/li&gt;&lt;li&gt;Companies found to be using these tactics on the modern web are very likely to suffer reputational and credibility damage. Some sectors that are heavily regulated will likely have legal implications too.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it-33&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;Fortunately, these blackhat techniques are no longer effective on the web. They are detrimental to both users and the internet, yet some individuals persist in attempting to use them.&lt;/p&gt;&lt;p&gt;For example, Google now ranks web pages on their usability issues, one of these is called &lt;a href=&quot;https://web.dev/articles/cls&quot;&gt;Cumulative Layout Shift (CLS)&lt;/a&gt;. This metric measures the stability of a page while a website is loading, websites that &quot;shift around&quot; while loading (called &quot;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Jank&quot;&gt;jank&lt;/a&gt;&quot;) aren&#39;t scored as highly as those that are more stable.&lt;/p&gt;&lt;p&gt;I recently saw a new SEO technique using JS that would mask an entire page with a transparent element in order to trick the browsers CLS metric into thinking the page was completely stable. After page load this element would be deleted and the page could be interacted as usual. Basically, a modern day cloaking technique aimed at improving a pages &lt;a href=&quot;https://wicg.github.io/layout-instability/&quot;&gt;Layout Instability API&lt;/a&gt; score.&lt;/p&gt;&lt;p&gt;So yes, it still happens and &quot;this is why we &lt;strong&gt;still&lt;/strong&gt; can&#39;t have nice things!&quot;.&lt;/p&gt;&lt;h3 id=&quot;above-the-fold-obsession&quot;&gt;“Above the Fold” obsession&lt;/h3&gt;&lt;p&gt;The concept of &quot;above the fold&quot; is an outdated technique. The fold&#39;s position is not fixed; rather, it varies depending on the device used to view a web page. If we take this to the extreme. Viewing a website on a desktop widescreen device vs a mobile device, there&#39;s never going to be a common &quot;fold&quot; in this situation. Consider the vast number of device widths, ranging from a large desktop widescreen to a mobile device—literally thousands along the x-axis. If you then factor in the viewport height (y-axis), you&#39;re looking at millions of possible viewport permutations. Past assumptions are no longer true:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;User behaviour&lt;/strong&gt;: Users scroll instinctively now. The old belief that users don’t scroll is no longer valid.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Web performance evolution&lt;/strong&gt;: Modern performance metrics (like &lt;a href=&quot;https://web.dev/articles/lcp&quot;&gt;Largest Contentful Paint (LCP)&lt;/a&gt; and &lt;a href=&quot;https://web.dev/articles/inp&quot;&gt;Interaction to Next Paint (INP)&lt;/a&gt;) reward real user-perceived speed, not just fast above-the-fold content.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Lazy loading and streaming&lt;/strong&gt;: The web has moved towards prioritising meaningful content dynamically, rather than front-loading everything visible “above the fold”.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;can-i-still-use-it-34&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;It depends. “Above the fold optimisation” is an older performance technique that focuses on rendering the visible portion of the page as quickly as possible. When used thoughtfully, it can still improve perceived load speed, especially in critical user flows. However, relying on it too heavily can narrow the focus to just a fragment of the overall experience.&lt;/p&gt;&lt;p&gt;Today, the more effective and sustainable approach is to optimise for end-to-end, user-centric performance. This includes not only what appears first on-screen, but also how quickly the page becomes usable and interactive. A strategy focused on delivering a consistently fast page experience will naturally improve the content visible without scrolling, regardless of the device.&lt;/p&gt;&lt;h3 id=&quot;superseded-compatibility-approaches&quot;&gt;Superseded compatibility approaches&lt;/h3&gt;&lt;h4 id=&quot;graceful-degradation&quot;&gt;Graceful Degradation&lt;/h4&gt;&lt;p&gt;The technique of graceful degradation involves building a website to take advantage of all the modern features of a browser, and once completed add &quot;fall backs&quot; for browsers that don&#39;t support modern features.&lt;/p&gt;&lt;h5 id=&quot;examples-2&quot;&gt;Examples&lt;/h5&gt;&lt;p&gt;An example of graceful degradation is: a developer builds a website where the main layout is using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/grid&quot;&gt;CSS Grid&lt;/a&gt;. But if a browser doesn&#39;t support Grid it will &quot;fall back&quot; to a simpler layout system like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Flexbox&quot;&gt;Flexbox&lt;/a&gt; or even a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Floats&quot;&gt;float-based layout&lt;/a&gt; (depending on the site&#39;s browser support requirements).&lt;/p&gt;&lt;p&gt;Another example is a feature-rich, JS-enhanced input form may fall back to a basic HTML form if JS is disabled or &lt;a href=&quot;https://www.kryogenix.org/code/browser/why-availability/&quot;&gt;fails to load&lt;/a&gt;, for example, due to a poor or unstable network connection. In this case, core functionality (such as form submission) remains available, even though advanced features (like real-time validation or dynamic UI elements) are unavailable.&lt;/p&gt;&lt;h5 id=&quot;why-is-it-legacy&quot;&gt;Why is it Legacy?&lt;/h5&gt;&lt;p&gt;Graceful degradation is increasingly being considered a legacy approach in modern web development as it has largely been superseded by &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;progressive enhancement&lt;/a&gt; which takes the opposite approach.&lt;/p&gt;&lt;h5 id=&quot;why-is-it-outdated-6&quot;&gt;Why is it outdated?&lt;/h5&gt;&lt;p&gt;This assumption of a modern baseline fails to acknowledge the true diversity of browsers and devices currently in use by users. Furthermore, comprehensive testing is challenging due to the difficulty of covering all scenarios involving older or limited browsers. Third, adding &quot;fallbacks&quot; further increases the complexity of an already intricate full-featured initial build. Most importantly, Graceful Degradation negatively impacts accessibility and resilience. Pages employing this technique frequently fail in low-capability environments, such as older browsers, devices, or poor connections.&lt;/p&gt;&lt;h5 id=&quot;can-i-still-use-it-35&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;There are a few scenarios where it may still be useful, these include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Legacy enterprise environments&lt;/strong&gt;: for example a company that mandates the use of older browsers like Internet Explorer. A notable example of this is &lt;a href=&quot;https://web.archive.org/web/20220708090533/http://www.nytimes.com/2022/07/08/business/korea-internet-explorer.html/&quot;&gt;Banks and other financial institutions in South Korea&lt;/a&gt;. For a country, that &lt;a href=&quot;https://explodingtopics.com/blog/countries-internet-users#internet-adoption-statistics-by-nation&quot;&gt;ranks 12th on the internet adoption rate for its citizens&lt;/a&gt; (97.4% it 2025), it&#39;s a pretty surprising legacy issue they are still trying to tackle!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Modernisation&lt;/strong&gt;: If a website is in a transition phase of being modernised, and it still needs to support older browsers for a limited period.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Non-critical enhancements&lt;/strong&gt;: If a site has non-critical enhancements like animations or media features that are optional and don&#39;t impact access to the site&#39;s core content.&lt;/li&gt;&lt;/ul&gt;&lt;h5 id=&quot;what-should-i-use-instead&quot;&gt;What should I use instead?&lt;/h5&gt;&lt;p&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;Progressive Enhancement&lt;/a&gt; is now the preferred approach for modern web development, offering a more robust, inclusive, and future-proof way to build websites and web applications. While Graceful Degradation was a useful technique for older browsers, it has now been superseded.&lt;/p&gt;&lt;h4 id=&quot;browser-sniffing&quot;&gt;Browser Sniffing&lt;/h4&gt;&lt;p&gt;This is the practice of detecting more information about a user&#39;s browser, like its specific version number or the operating system it is currently running on. Once detected a developer can use this information to “fork” their code, e.g. make decisions as to what bug workarounds should, or shouldn&#39;t be applied. Or even tailor the user experience for a specific version of a browser. Two very common uses of this technique in the past were to redirect a user to the mobile version of the site (when mobile and desktop sites built separately), or even blocking the usage of a site on &quot;unsupported&quot; browsers. An example of how you&#39;d do this in JS is below:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userAgent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Chrome&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Apply Chrome-specific behaviour or simply block other browsers if you are feeling malicious&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This code highlights a significant problem with browser sniffing, demonstrating why it&#39;s considered an outdated technique. In the code, the whole functionality is decided by the fact that the User-Agent variable in the browser happens to include the string &quot;Chrome&quot;. But what happens if Google one day decides to change this string to lowercase &quot;chrome&quot;, or even change it completely? Well, the code depending on this detection will break!&lt;/p&gt;&lt;p&gt;Now, you could modify the above code to tackle the case issue like so:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userAgent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;chrome&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Apply Chrome-specific behaviour&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But as you can see, this has only made the code more &lt;strong&gt;complex&lt;/strong&gt; and &lt;strong&gt;fragile&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It&#39;s also worth mentioning that this code won&#39;t do what you expect it to either, as all Chrome based browser will return true. For example:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Google Chrome&lt;/li&gt;&lt;li&gt;Microsoft Edge (Chromium-based)&lt;/li&gt;&lt;li&gt;Opera (also Chromium-based)&lt;/li&gt;&lt;li&gt;Brave&lt;/li&gt;&lt;li&gt;Vivaldi&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;At the time of writing each of the User-Agent strings for the above browsers contain: &lt;code&gt;Chrome/115.0.0.0&lt;/code&gt; (as well as other information, that I have removed for the example).&lt;/p&gt;&lt;p&gt;All contain &quot;Chrome&quot; in their User-Agent, so will all run the code.&lt;/p&gt;&lt;p&gt;What&#39;s worse is that Chrome on iOS will return &lt;code&gt;false&lt;/code&gt; and &lt;strong&gt;not&lt;/strong&gt; run the code. On iOS, all browsers, including what appears to be Chrome on the home screen, are actually forced to use WebKit (Safari). Consequently, &quot;Chrome&quot; in this instance isn&#39;t truly Chrome and is not reflected in the User-Agent String.&lt;/p&gt;&lt;h5 id=&quot;other-issues&quot;&gt;Other issues&lt;/h5&gt;&lt;p&gt;Fragility isn&#39;t the only issue seen when using this technique. It can also:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;add a maintenance burden for developers, as this logic will need to be updated as browsers evolve.&lt;/li&gt;&lt;li&gt;create browser feature mismatch, two versions of the same browser don&#39;t always support the same features.&lt;/li&gt;&lt;li&gt;cause accessibility risks leading to user exclusion. A user on a less-common browser or assistive technologies, could inadvertently receive a degraded experience, or even blocked completely.&lt;/li&gt;&lt;/ul&gt;&lt;h5 id=&quot;can-i-still-use-it-36&quot;&gt;Can I still use it?&lt;/h5&gt;&lt;p&gt;Realistically, no, you should aim to avoid browser sniffing entirely and instead of asking &lt;em&gt;which browser&lt;/em&gt; a user is using, ask &lt;em&gt;what can their browser do?&lt;/em&gt; Essentially, you want to be detecting the features that the user&#39;s browser can support. For example, to detect if a browser supports the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API&quot;&gt;Service Worker API&lt;/a&gt;, you can do this:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;serviceWorker&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// The users browser supports the &#39;serviceWorker&#39; API, so do Service Worker stuff!&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h5 id=&quot;browser-sniffing-summary&quot;&gt;Browser Sniffing Summary&lt;/h5&gt;&lt;p&gt;In summary, browser sniffing is a legacy technique that should be avoided on the modern web. In order to create a more resilient and inclusive web, you should use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Testing/Feature_detection&quot;&gt;Feature Detection&lt;/a&gt;, &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#graceful-degradation&quot;&gt;Graceful Degradation&lt;/a&gt;, and &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#11-what-still-matters-progressive-enhancement&quot;&gt;Progressive Enhancement&lt;/a&gt; instead.&lt;/p&gt;&lt;h3 id=&quot;modernizr&quot;&gt;Modernizr&lt;/h3&gt;&lt;p&gt;I was a big fan of &lt;a href=&quot;https://github.com/Modernizr/Modernizr&quot;&gt;Modernizr&lt;/a&gt; (with its very Web 2.0 name!). For readers who&#39;ve not used or heard of it, Modernizr is a HTML5 and CSS3 feature detection library, It does this via browser feature detection, rather than browser user-agent strings, which can be unreliable and misleading. Modernizr actually tests to see if the browser being used supports a whole host of features.&lt;/p&gt;&lt;p&gt;It was released in 2009 at version 1.0, since then, it has had &lt;a href=&quot;https://github.com/Modernizr/Modernizr/releases&quot;&gt;27 releases&lt;/a&gt;, and &lt;a href=&quot;https://github.com/Modernizr/Modernizr/graphs/contributors&quot;&gt;300 contributors&lt;/a&gt;. So how does it work, and how exactly do you use it? Here&#39;s an example of how it detects &lt;code&gt;flexbox&lt;/code&gt; support in a user&#39;s browser.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Adds a new test to the Modernizr object under the key &#39;flexbox&#39;&lt;/span&gt;
Modernizr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addTest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;flexbox&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Create a new HTML div element to test CSS properties on&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; testElement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;div&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Attempt to set the display property to &#39;flex&#39;&lt;/span&gt;
  testElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;display &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;flex&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Check if the browser retains the value &#39;flex&#39; for the display property&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// If supported, the style will remain &#39;flex&#39;; otherwise, it may remain empty or be changed&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; testElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;display &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;flex&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here&#39;s how you would use that in your website. There are 2 methods for how you use it:&lt;/p&gt;&lt;h4 id=&quot;css&quot;&gt;CSS&lt;/h4&gt;&lt;p&gt;Modernizr adds a class to the &lt;code&gt;&amp;lt;html&gt;&lt;/code&gt; element, in our case above it would be:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;flexbox&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- If the browser supports it. --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;no-flexbox&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &amp;lt;!-- If the browser doesn&#39;t supports it.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Thus, you could hang any styles you needed off these classes and tweak the CSS layout for both scenarios, flex support and no flex support.&lt;/p&gt;&lt;h4 id=&quot;javascript&quot;&gt;JavaScript&lt;/h4&gt;&lt;p&gt;The other method is to use it in JS by examining the Modernizr object:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Modernizr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flexbox&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Flexbox is supported, do Flexbox JS stuff here&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Flexbox is not supported, no non Flexbox JS stuff here&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The great thing about Modernizr is that even thought it supports over 250 feature tests for HTML5, CSS3, and JS APIs, it allows developers to create custom-builds that &lt;strong&gt;only&lt;/strong&gt; detect the features they intend to use. This improves performance in 2 ways:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Less JS to download, parse, then execute.&lt;/li&gt;&lt;li&gt;Fewer bytes sent over the network.&lt;/li&gt;&lt;/ol&gt;&lt;h4 id=&quot;use-cases&quot;&gt;Use cases:&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;Applying fallback CSS or JS for unsupported features.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement&quot;&gt;Progressive enhancement&lt;/a&gt; strategies.&lt;/li&gt;&lt;li&gt;Conditional &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Polyfill&quot;&gt;polyfill&lt;/a&gt; loading.&lt;/li&gt;&lt;li&gt;Responsive design tweaks based on feature support (via JS)&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;can-i-still-use-it-37&quot;&gt;Can I still use it?&lt;/h4&gt;&lt;p&gt;It&#39;s still useful, but it isn&#39;t as essential as it once was, this is because:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Browser standardisation has vastly improved since 2009.&lt;/li&gt;&lt;li&gt;Native CSS now support &lt;a href=&quot;https://caniuse.com/css-featurequeries&quot;&gt;feature queries&lt;/a&gt; (&lt;code&gt;@supports&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;Feature detection is often built into some JS frameworks.&lt;/li&gt;&lt;li&gt;Support for ancient browsers has declined.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Ironically, Modernizr, despite detecting over 250 features, does not detect JS support itself. This isn&#39;t a problem, though, because if you require this functionality it can be added via a single line of JS in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; of your page:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Default setup --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;no-js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documentElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;className &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documentElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;className&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;no-js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By default, the script assumes that the browser doesn&#39;t support JS (&lt;code&gt;class=&quot;no-js&quot;&lt;/code&gt;). When it gets to the inline script tag, the script executes. And as this proves that JS is supported it swaps the &lt;code&gt;no-js&lt;/code&gt; class to a &lt;code&gt;js&lt;/code&gt; class that can then be used in CSS styling, as you would any other Modernizr CSS class as demonstrated above.&lt;/p&gt;&lt;h2 id=&quot;10-tests-and-standards-of-yesteryear&quot;&gt;10. Tests and Standards of Yesteryear&lt;/h2&gt;&lt;h3 id=&quot;acid2-and-acid3-tests&quot;&gt;Acid2 and Acid3 Tests&lt;/h3&gt;&lt;p&gt;The Acid2 and Acid3 tests were really clever ways to test a browser&#39;s compliance with the ever-evolving rendering standards at the time. They were both created by the &lt;a href=&quot;https://www.webstandards.org/&quot;&gt;Web Standards Project (WaSP)&lt;/a&gt;which was founded in 1998, when the web was a battleground of two main browsers:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Microsoft (Internet Explorer 4 at the time)&lt;/li&gt;&lt;li&gt;Netscape (Netscape Navigator 4.05 at the time)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The Web Standards Project, aimed to promote web standards that made development simpler, more accessible, and future-proofed, by working closely with browser vendors and development toolmakers to achieve this. When the team posted their &lt;a href=&quot;https://www.webstandards.org/2013/03/01/our-work-here-is-done/index.html&quot;&gt;final blog post in March 2013&lt;/a&gt;, their mission was largely complete. It resulted in successfully getting browser vendors to support the standards set by the &lt;a href=&quot;https://www.w3.org/&quot;&gt;World Wide Web Consortium (W3C)&lt;/a&gt;. As of 2025, the W3C remains active in setting new standards to ensure the web continues to support communication, commerce, and knowledge-sharing for all, with a strong focus on accessibility, diversity, and inclusion.&lt;/p&gt;&lt;h4 id=&quot;acid2-2005&quot;&gt;Acid2 (2005)&lt;/h4&gt;&lt;p&gt;The &lt;a href=&quot;http://acid2.acidtests.org/&quot;&gt;Acid2 test&lt;/a&gt; was created to test compliance with HTML 4.01, CSS 1 &amp; 2, and PNG rendering standards (e.g. alpha transparency). It did this by focussing on the following key areas of browser rendering:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Box model&lt;/li&gt;&lt;li&gt;Absolute and relative positioning&lt;/li&gt;&lt;li&gt;Float behaviour&lt;/li&gt;&lt;li&gt;Table layout&lt;/li&gt;&lt;li&gt;PNG alpha transparency&lt;/li&gt;&lt;li&gt;Data URLs&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This was achieved through a highly innovative browser test: creating a simple cartoon face, similar to a smiling emoji. Depending on how compliant the browser was, influenced how well the face was rendered. It&#39;s much simpler if I just show you!&lt;/p&gt;&lt;h5 id=&quot;rendering-reference&quot;&gt;Rendering reference&lt;/h5&gt;&lt;p&gt;This is what the output of the test is supposed to look like across &lt;strong&gt;all browsers&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A picture of a yellow pixelated smiling face with green eyes to indicate that the browser was compliant with the rendering standards of the day.&quot; decoding=&quot;async&quot; height=&quot;603&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/GMrcbbNVXJ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/GMrcbbNVXJ-300.webp 300w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/GMrcbbNVXJ-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;h5 id=&quot;internet-explorer-6&quot;&gt;Internet Explorer 6&lt;/h5&gt;&lt;p&gt;This &quot;face,&quot; rendered with IE6, appears as if the individual suffered a severe accident!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A version of the face rendered in IE6, it&#39;s basically a big red box with no discernible facial features at all!&quot; decoding=&quot;async&quot; height=&quot;618&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/xVLfj2QpPx-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/xVLfj2QpPx-300.webp 300w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/xVLfj2QpPx-546.webp 546w&quot; width=&quot;546&quot;&gt;&lt;/p&gt;&lt;h5 id=&quot;netscape-4-8&quot;&gt;Netscape 4.8&lt;/h5&gt;&lt;p&gt;Netscape performed similarly to IE6 at the time.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Another face accident rendered by Netscape 4.8 a big red box surrounded by a solid black border and broken image icon in the bottom left corner!&quot; decoding=&quot;async&quot; height=&quot;704&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/3hM2Dr7q53-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/3hM2Dr7q53-300.webp 300w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/3hM2Dr7q53-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;h5 id=&quot;mozilla-deer-park-alpha-2-later-firefox-3&quot;&gt;Mozilla Deer Park Alpha 2 (later Firefox 3)&lt;/h5&gt;&lt;p&gt;Finally a &lt;a href=&quot;https://www-archive.mozilla.org/projects/deerpark/releases/alpha1.html&quot;&gt;browser&lt;/a&gt; that actually rendered a face!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of a browser window it the face finally looking like the reference image shown on the test site.&quot; decoding=&quot;async&quot; height=&quot;417&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/XY-h1FMrnC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/XY-h1FMrnC-300.webp 300w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/XY-h1FMrnC-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;There are far too many variations and versions of browsers around at the time to list here, but if you are interested in how other browsers rendered the Acid2 test check out this &lt;a href=&quot;https://www.howtocreate.co.uk/acid/&quot;&gt;ancient blog post&lt;/a&gt; by author &lt;a href=&quot;https://www.howtocreate.co.uk/bio.html&quot;&gt;Mark &quot;Tarquin&quot; Wilton-Jones&lt;/a&gt;. Mark, thank you for safeguarding this significant and captivating piece of web standards history!&lt;/p&gt;&lt;h4 id=&quot;acid3-2008&quot;&gt;Acid3 (2008)&lt;/h4&gt;&lt;p&gt;After the success of the Acid2 browser test in putting pressure on browser vendors to improve standards support in their browsers, it was decided by the WaSP team to create another browser test, this time focussing on a different set of browser technologies. These included:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;DOM Level 2 and 3&lt;/li&gt;&lt;li&gt;ECMAScript (JS) behaviour&lt;/li&gt;&lt;li&gt;CSS 3 selectors&lt;/li&gt;&lt;li&gt;SVG rendering&lt;/li&gt;&lt;li&gt;Data URIs&lt;/li&gt;&lt;li&gt;Animation timing and rendering&lt;/li&gt;&lt;li&gt;WebFonts via &lt;code&gt;@font-face&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h5 id=&quot;rendering-reference-2&quot;&gt;Rendering reference&lt;/h5&gt;&lt;p&gt;The &lt;a href=&quot;http://acid3.acidtests.org/&quot;&gt;Acid3 test&lt;/a&gt; took a more traditional testing route and simply scored a browser taking the test between 1 (poor support) and 100 (perfect support).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Acid3 test comprised of a set of 6 rainbow coloured boxes (Red to Indigo). The score out of 100 was shown directly below these coloured boxes&quot; decoding=&quot;async&quot; height=&quot;543&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/8bKdUgCdEj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/8bKdUgCdEj-300.webp 300w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/8bKdUgCdEj-600.webp 600w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/8bKdUgCdEj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In my opinion, the Acid3 test, while practical and easy to interpret, lacked the entertainment value of the Acid2 facial disfigurement test!&lt;/p&gt;&lt;p&gt;The release of the more challenging Acid3 test coincided with a surge in browser competition, particularly among Firefox, Safari, Opera, and Google Chrome (which was released later in 2008).&lt;/p&gt;&lt;h5 id=&quot;scores&quot;&gt;Scores&lt;/h5&gt;&lt;p&gt;At the time of release (March 2008) the Acid3 scores for each major browser were as follows:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;IE7&lt;/strong&gt;: 12 / 100.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;IE8 Beta 1&lt;/strong&gt;: 18 / 100.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Firefox 2&lt;/strong&gt;: 50 / 100.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Firefox 3 Beta 4&lt;/strong&gt;: 71 / 100.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Opera 9.5 Beta&lt;/strong&gt;: between 60–70 / 100.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Safari 3.1&lt;/strong&gt;: between 75–90 / 100.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Google Chrome&lt;/strong&gt;: Not yet released in March 2008.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Google Chrome 0.2 Beta: first release&lt;/strong&gt;: 77 / 100.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Google Chrome 1.0&lt;/strong&gt;: 100 / 100.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Google Chrome quickly improved its Acid3 score shortly after its initial release. This rapid improvement was mainly due to its use of the Safari WebKit engine, which already scored 75–90 out of 100 at the time.&lt;/p&gt;&lt;h4 id=&quot;legacy&quot;&gt;Legacy&lt;/h4&gt;&lt;p&gt;Neither test is maintained or relevant to the modern web, but they played a key role in pushing browser vendors toward better standards support. Today, browser compliance is measured using the &lt;a href=&quot;https://web-platform-tests.org/&quot;&gt;Web Platform Tests (WPT)&lt;/a&gt;, a much broader and actively maintained suite developed by the vendors themselves with input from &lt;a href=&quot;https://whatwg.org/&quot;&gt;WHATWG&lt;/a&gt; and &lt;a href=&quot;https://www.w3.org/&quot;&gt;W3C&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;11-what-still-matters-progressive-enhancement&quot;&gt;11. What Still Matters – Progressive Enhancement&lt;/h2&gt;&lt;h3 id=&quot;not-legacy-but-often-forgotten&quot;&gt;Not legacy but often forgotten&lt;/h3&gt;&lt;p&gt;Congratulations! You&#39;ve made it! After discussing countless legacy approaches and techniques best left in the past, you&#39;ve finally arrived at a truly timeless and Incredibly important methodology. More than two decades after &lt;a href=&quot;https://www.linkedin.com/in/schampeo/&quot;&gt;Steve Champeon&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Nick_Finck&quot;&gt;Nick Finck&lt;/a&gt; introduced it in their talk &quot;&lt;a href=&quot;https://hesketh.com/publications/inclusive_web_design_for_the_future/&quot;&gt;Inclusive Web Design For the Future&lt;/a&gt;” at &lt;a href=&quot;https://www.sxsw.com/iconicmoments/2003/&quot;&gt;SXSW in 2003&lt;/a&gt;. The Progressive Enhancement (PE) methodology remains one of the most robust and future-ready methods for modern web development.&lt;/p&gt;&lt;h4 id=&quot;what-is-progressive-enhancement&quot;&gt;What is Progressive Enhancement?&lt;/h4&gt;&lt;p&gt;There&#39;s a ubiquitous diagram that is always shown whenever PE is mentioned in a blog post. And this blog post will be no different in using it, as it actually explains the concept incredibly well.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;This is the progressive enhancement pyramid, HTML is the bottom layer, CSS the 2nd layer, and lastly JavaScript is at the top of the pyramid.&quot; decoding=&quot;async&quot; height=&quot;694&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/SNN9uz1NBR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/SNN9uz1NBR-300.webp 300w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/SNN9uz1NBR-600.webp 600w, https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/SNN9uz1NBR-800.webp 800w&quot; width=&quot;800&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we have the well known Progressive Enhancement pyramid.&lt;/p&gt;&lt;h4 id=&quot;html&quot;&gt;HTML&lt;/h4&gt;&lt;p&gt;The HTML is at the bottom of the pyramid as it gives the website a solid foundation on which to build off. The HTML layer is the most resilient layer in the web development stack. Without the HTML there is no content, no links, no images, no website! This layer is by far the most important layer in the pyramid. Just to give you an idea of how resilient HTML is in web browsers, let&#39;s take a look at the &lt;a href=&quot;https://info.cern.ch/hypertext/WWW/TheProject.html&quot;&gt;very first website&lt;/a&gt;. Back on Tuesday, August 6, 1991, &lt;a href=&quot;https://en.wikipedia.org/wiki/Tim_Berners-Lee&quot;&gt;Sir Tim Berners-Lee&lt;/a&gt;, the inventor of the Web, published the very first website! Now, this statistic makes me feel ancient! The first website was published almost 34-years ago, at the time of writing! And what do you notice about the page? Well, most importantly, it still renders correctly, and the content can still be read perfectly well after all this time. If you take a peak at the page&#39;s source code, you will notice a few oddities, like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The complete lack of a DOCTYPE, and no &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag.&lt;/li&gt;&lt;li&gt;No links to external stylesheets or JavaScript (they weren&#39;t invented yet!)&lt;/li&gt;&lt;li&gt;Anchors existed to link to other pages, but they had a strange &lt;code&gt;NAME=[integer]&lt;/code&gt; attribute.&lt;/li&gt;&lt;li&gt;All elements were written in uppercase, e.g. &lt;code&gt;&amp;lt;HEADER&gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;BODY&gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;TITLE&gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;H1&gt;&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Lack of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/HTML#good_semantics&quot;&gt;Semantic Markup&lt;/a&gt;. This was to come later once the WWW had matured.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To put it into perspective, this website has been around longer than nearly &lt;a href=&quot;https://en.wikipedia.org/wiki/Demographics_of_the_world&quot;&gt;half of the entire global population&lt;/a&gt;, that’s over &lt;strong&gt;4 billion people younger&lt;/strong&gt; than this single page on the internet! Which other digital format on the planet can boast that form of robustness and ease of accessibility? Just think about all the storage media formats that have come and gone in that time:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;5.25-inch floppy disk (the disk that &lt;em&gt;was&lt;/em&gt; actually floppy)&lt;/li&gt;&lt;li&gt;3.5-inch floppy disk (the 3D &quot;save icon&quot;)&lt;/li&gt;&lt;li&gt;CD-ROM / CD-R / CD-RW&lt;/li&gt;&lt;li&gt;MiniDisc (data version)&lt;/li&gt;&lt;li&gt;CompactFlash (CF)&lt;/li&gt;&lt;li&gt;Zip disk&lt;/li&gt;&lt;li&gt;Jazz drive&lt;/li&gt;&lt;li&gt;DVD-ROM / DVD±R / DVD±RW&lt;/li&gt;&lt;li&gt;Blu-ray&lt;/li&gt;&lt;li&gt;HD DVD&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The list goes on… The point is clear: if you need a long-term, reliable storage solution that just works, plain HTML on the web is hard to beat! FYI: Of course, these web assets ultimately reside on physical hardware in data centres, but that’s not the point. What matters is the resilience and accessibility the web platform offers, regardless of the underlying infrastructure.&lt;/p&gt;&lt;h4 id=&quot;css-2&quot;&gt;CSS&lt;/h4&gt;&lt;p&gt;The second layer in the pyramid is the CSS or the Cascading Style Sheets. When the World Wide Web (W3) was first invented back in the early 90s, CSS simply didn&#39;t exist. It wasn&#39;t until 1996 / mid-1997 that browsers started to support the &lt;a href=&quot;https://www.w3.org/TR/CSS1/&quot;&gt;CSS Level 1 Specification&lt;/a&gt;. The browsers at the time were Internet Explorer 3 and Netscape Navigator 4, both had partial (and mostly buggy implementations). Up until this point the web had been completely &quot;naked&quot; in terms of design. Just pages full of text, images, and the odd animated GIF. Nothing at all like the modern web we see today.&lt;/p&gt;&lt;p&gt;CSS constitutes the second layer of the pyramid because, frankly, it is a &quot;nice to have.&quot; Browsers are equipped with default stylesheets (as previously discussed in the &lt;a href=&quot;https://nooshu.com/blog/2025/08/26/hack-to-the-future-frontend/#css-resets&quot;&gt;CSS Resets section&lt;/a&gt;), which enable HTML content to display correctly and remain readable even in the absence of a website&#39;s custom styles. A company or brand must ensure their CSS is available for browsers to download so that their website renders correctly. Without it, many users would assume the site is broken, especially given how modern websites are expected to appear. But in the unlikely event the CSS fails to load, users will still receive the HTML content in a perfectly readable format. While it may appear unappealing, it remains fully functional across all current (and all future) web browsers and assistive technologies.&lt;/p&gt;&lt;p&gt;The beauty of Progressive Enhancement lies in establishing a foundational layer (HTML) and then &lt;strong&gt;progressively&lt;/strong&gt; adding desired features. This method ensures that if any subsequent layer fails, the underlying content and functionality remain accessible to users.&lt;/p&gt;&lt;p&gt;A prime example of this browser feature in action is, since April 9, 2006, &lt;a href=&quot;https://css-naked-day.org&quot;&gt;CSS Naked Day&lt;/a&gt; has been observed. Where for a &lt;a href=&quot;https://css-naked-day.org/#timespan&quot;&gt;50-hour period&lt;/a&gt;, website owners disable their site&#39;s CSS, allowing users to experience the semantic HTML without styling. It started as a push for web standards and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/HTML#good_semantics&quot;&gt;semantic markup&lt;/a&gt;, and gave site owners an excuse to flaunt their sexy &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt;. Gotta love a good HTML pun!&lt;/p&gt;&lt;h4 id=&quot;javascript-2&quot;&gt;JavaScript&lt;/h4&gt;&lt;p&gt;The final layer of the pyramid and the final piece of the Web stack puzzle is JS, this is the interaction layer that is added to a site last after the foundation (HTML) and design (CSS) have been added. It&#39;s difficult to believe that just &lt;strong&gt;three&lt;/strong&gt; technologies, all of which have been discussed in this section, form the entirety of the web. There is truly nothing more to it than these three foundational components. Ultimately, the output of both frontend and backend development invariably consists of standard HTML, CSS, and JS. Although a multitude of tools and languages are available for web developers to use, with endless paths to choose from, they eventually all lead to identical HTML, CSS, and JS as their final output. It all comes down to using the right tool and technology for the job!&lt;/p&gt;&lt;p&gt;JS is deliberately placed as the final enhancement layer in the pyramid. This is not incidental. JS, while powerful, is the least resilient layer in the web stack. Its execution depends on multiple fragile components:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;the network&lt;/li&gt;&lt;li&gt;the parser&lt;/li&gt;&lt;li&gt;the runtime environment&lt;/li&gt;&lt;li&gt;the integrity of the code itself.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A single misplaced character, for example, an errant semicolon or an &lt;code&gt;undefined&lt;/code&gt; variable, can render entire swathes of interaction inoperable. This fragility is not a hypothetical risk. It manifests regularly across production environments all over the web, particularly where sites are heavily reliant on client-side code for core user journeys.&lt;/p&gt;&lt;h4 id=&quot;progressive-enhancement-summary&quot;&gt;Progressive Enhancement Summary&lt;/h4&gt;&lt;p&gt;The modern web has increasingly drifted away from the principles of Progressive Enhancement, often placing JS as the foundation rather than the finishing touch. Single Page Applications are a prime example, where even basic navigation and content rendering require full JS execution. This inversion of the pyramid not only risks total inoperability in degraded environments but also introduces avoidable accessibility and performance issues.&lt;/p&gt;&lt;p&gt;From a resilience and user experience standpoint, over-reliance on JS creates brittleness. Unlike HTML and CSS, which both degrade gracefully, JS fails noisily and catastrophically. If a CSS file fails to load, a page might look plain but will still remain usable. If a JS bundle fails, the entirety of the website&#39;s features may be lost, with little to no fallback available.&lt;/p&gt;&lt;p&gt;The web’s reach includes users with:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;unreliable networks&lt;/li&gt;&lt;li&gt;older devices&lt;/li&gt;&lt;li&gt;constrained data plans&lt;/li&gt;&lt;li&gt;assistive technologies&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A heavy dependence on JS frequently excludes these users or significantly worsens their experience. Progressive Enhancement is not about supporting “no JavaScript” users as a niche edge case. It’s about ensuring a robust baseline that works for everyone, every time, demonstrating empathy for all users regardless of how they access the internet.&lt;/p&gt;&lt;p&gt;While JS &lt;em&gt;is&lt;/em&gt; a vital tool in a web developer’s toolkit, it must be handled with care. Its position at the top of the Progressive Enhancement pyramid reflects its power, but also its fragility. It should be used responsibly, with the awareness that its failure often leads to a broken experience. True resilience comes from building upwards from stable foundations, not downwards from brittle interactions.&lt;/p&gt;&lt;h3 id=&quot;importance-in-government-services&quot;&gt;Importance in government services&lt;/h3&gt;&lt;p&gt;Having worked at GDS for 6-years, I can&#39;t tell you how many times I had to defend the frontend communities stance on Progressive Enhancement! Thankfully, it&#39;s all written in black and white in the &lt;a href=&quot;https://www.gov.uk/service-manual/technology/using-progressive-enhancement&quot;&gt;Service Manual for all to read&lt;/a&gt;. However, some departments and developers found ways to work around the methodology or opted for alternative approaches. This was most likely driven by two things:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;A team had made significant progress with their JS dependent service and were expressing concerns about meeting the requirements for their future &lt;a href=&quot;https://www.gov.uk/service-manual/service-assessments&quot;&gt;service assessment(s)&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Some Frontend Developers in the department were enthusiastic about adopting the latest client-side frameworks, with less emphasis on assessing their maturity or suitability for the service, and its users.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;For point 1 it always amazed me that teams were able to get so far into prototyping for it to become an issue. As depressing as it may be to me, maybe the Service Manual and its guidance isn&#39;t as well-known across government as I&#39;d like to believe?&lt;/p&gt;&lt;p&gt;For point 2, I 100% get it, new technology on the web is fun to play with and also to have on your CV / Resume! The real question is whether this &lt;strong&gt;new technology&lt;/strong&gt; is truly the right choice for a critical public service that &lt;strong&gt;every&lt;/strong&gt; UK taxpayer depends on and has a fundamental right to access?&lt;/p&gt;&lt;p&gt;Technology Suitability Check (Progressive Enhancement Focus):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Does the technologies core functionality allow the service to work without JS enabled?&lt;/li&gt;&lt;li&gt;Can the service still function reliably on low-powered or older devices when using this technology?&lt;/li&gt;&lt;li&gt;Is the final output from the technology easily accessible and usable with assistive technologies, regardless of the device used?&lt;/li&gt;&lt;li&gt;Does the technology degrade gracefully in poor network conditions, such as on a 3G connection or in rural areas?&lt;/li&gt;&lt;li&gt;Are all critical user journeys still functional when JS fails to load, or is blocked?&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If the answer to any of the questions above is &quot;no&quot;, then the technology probably isn&#39;t a great fit for a public service that needs to be maintained for years (or even decades!).&lt;/p&gt;&lt;p&gt;The last point in the list above is incredibly important:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Are all critical user journeys still functional when JS fails to load, or is blocked?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I think this is where there was a lot of misunderstanding in terms of Progressive Enhancement in government. I continually strive to highlight that a JavaScript-only journey doesn&#39;t require a direct 1-to-1 correlation with its progressively enhanced foundation. As long as for each user journey a user can complete their task, quickly and easily, then the use of JavaScript is fine.&lt;/p&gt;&lt;p&gt;For example, consider a feature-rich JavaScript dashboard built to enter user data into a backend database. If a simple HTML form with a submit button can achieve the same outcome (which it often can), then the dashboard is acceptable only if the HTML form provides a reliable fallback for situations where JavaScript is unavailable, such as when it fails to load due to a limited data plan, a poor connection, or a low specification device.&lt;/p&gt;&lt;p&gt;During a service assessment, the key question is whether the dashboard meaningfully improves data entry and user interaction, or whether it exists purely for the sake of using new technology. Adopting new tools without clear justification is not acceptable for a government service. I consider such an approach to be driven by a desire to boost a developer&#39;s CV or LinkedIn profile. E.g. CDD (CV-Driven Development) or LDD (LinkedIn-Driven Development).&lt;/p&gt;&lt;h2 id=&quot;12-lessons-for-the-future&quot;&gt;12. Lessons for the Future&lt;/h2&gt;&lt;h3 id=&quot;what-these-legacy-practices-teach-us-today&quot;&gt;What these legacy practices teach us today&lt;/h3&gt;&lt;p&gt;If there is one takeaway from this post, it is that Frontend Development has &lt;strong&gt;never&lt;/strong&gt; stood still. What counts as best practice today can feel outdated or even “legacy” tomorrow. That constant state of reinvention is what first drew me in back in the late 90s, and it is what continues to excite me now. Backend development always seemed a little too steady, too predictable. Frontend, on the other hand, has always lived on the edge of change.&lt;/p&gt;&lt;p&gt;But what is different today is the scale of change ahead of us. With the rise of &lt;a href=&quot;https://livebench.ai/#/&quot;&gt;Artificial Intelligence (AI)&lt;/a&gt;, we may be standing on the edge of a shift as significant as the birth of the modern Web itself. Just as the early internet reshaped how we live and work, the combination of AI and the Web could redefine what it even means to build, design, and interact online. The coming years are not just going to be interesting, they could mark a turning point in the history of our craft.&lt;/p&gt;&lt;h3 id=&quot;applying-lessons-to-modern-frontend-work&quot;&gt;Applying lessons to modern frontend work&lt;/h3&gt;&lt;h4 id=&quot;core-principle&quot;&gt;Core principle&lt;/h4&gt;&lt;p&gt;Choose optimal solutions for the enduring parts of the stack: HTML, CSS, and JavaScript are the stable contract. Prioritise the most straightforward and maintainable approach to delivering clean, accessible HTML, efficient CSS, and lightweight, well scoped JavaScript. The further you move away from those three, the harder accessibility, maintenance, and performance become. Let the browser and the web platform do the heavy lifting.&lt;/p&gt;&lt;h4 id=&quot;practical-rules-to-work-by&quot;&gt;Practical rules to work by&lt;/h4&gt;&lt;h5 id=&quot;1-start-from-the-web-platform&quot;&gt;1. Start from the Web Platform&lt;/h5&gt;&lt;p&gt;Prefer native elements and platform features before adding libraries. Use form controls, semantic HTML, CSS layout, media queries, inert, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details&quot;&gt;details&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/summary&quot;&gt;summary&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog&quot;&gt;dialog&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch&quot;&gt;fetch&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API&quot;&gt;URLPattern&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver&quot;&gt;IntersectionObserver&lt;/a&gt;, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components&quot;&gt;Web Components&lt;/a&gt; where they fit.&lt;/p&gt;&lt;h5 id=&quot;2-progressive-enhancement-as-a-default&quot;&gt;2. Progressive enhancement as a default&lt;/h5&gt;&lt;p&gt;Deliver meaningful HTML first, enhance with CSS, then layer JavaScript for interactivity. Critical journeys should still work when scripts fail or load slowly.&lt;/p&gt;&lt;h5 id=&quot;3-ship-less-code&quot;&gt;3. Ship less code&lt;/h5&gt;&lt;p&gt;Adopt a dependency diet. Each abstraction must earn its keep through measurable value. Small utilities over frameworks by default. If a framework is chosen, configure it to output lean HTML, CSS, and JS.&lt;/p&gt;&lt;h5 id=&quot;4-accessibility-first-not-last&quot;&gt;4. Accessibility first, not last&lt;/h5&gt;&lt;p&gt;Use semantic structure, proper labels, roles only when needed, visible focus, real buttons and links, reduced motion preferences, and test with keyboard and screen readers. Performance &lt;strong&gt;100% is&lt;/strong&gt; an accessibility feature.&lt;/p&gt;&lt;h5 id=&quot;5-performance-budgets-and-baselines&quot;&gt;5. Performance budgets and baselines&lt;/h5&gt;&lt;p&gt;Set budgets for bundle size, interaction latency, and memory. Track Core Web Vitals from real users. Fail builds that exceed budgets. Optimise for first input delay, input responsiveness, and low CPU use on mid-range devices.&lt;/p&gt;&lt;h5 id=&quot;6-keep-the-build-simple&quot;&gt;6. Keep the build simple&lt;/h5&gt;&lt;p&gt;Prefer standard tooling that converges to web standards. Use the minimum build steps required. Long pipelines increase failure modes and slow iteration.&lt;/p&gt;&lt;h5 id=&quot;7-design-for-resilience&quot;&gt;7. Design for resilience&lt;/h5&gt;&lt;p&gt;Favour server rendering for first paint, hydrate only what is interactive, cache well, and handle partial failure gracefully. Make error states explicit.&lt;/p&gt;&lt;h5 id=&quot;8-document-the-escape-hatches&quot;&gt;8. Document the escape hatches&lt;/h5&gt;&lt;p&gt;Where you choose abstractions, document how to reach the underlying HTML, CSS, and JS. Future teams should be able to debug without learning a bespoke stack.&lt;/p&gt;&lt;h5 id=&quot;9-measure-before-you-change&quot;&gt;9. Measure before you change&lt;/h5&gt;&lt;p&gt;Add observability. Use &lt;a href=&quot;https://en.wikipedia.org/wiki/Real_user_monitoring&quot;&gt;Real User Monitoring (RUM)&lt;/a&gt; to guide work. Optimise the slowest real user paths, not synthetic microbenchmarks.&lt;/p&gt;&lt;h5 id=&quot;10-plan-for-upgrades&quot;&gt;10. Plan for upgrades&lt;/h5&gt;&lt;p&gt;Last, but not least, prefer tools with clear deprecation policies and migration paths. Avoid lock in. Isolate framework code behind simple boundaries so you can replace parts without rewriting the product.&lt;/p&gt;&lt;h5 id=&quot;a-quick-decision-test&quot;&gt;A quick decision test&lt;/h5&gt;&lt;ol&gt;&lt;li&gt;Can this be done with native HTML or CSS alone?&lt;/li&gt;&lt;li&gt;If not, can a few lines of vanilla JS do it without a dependency?&lt;/li&gt;&lt;li&gt;If not, does a library reduce long term cost and keep output close to the platform?&lt;/li&gt;&lt;li&gt;If a framework is still justified, can it produce accessible HTML by default and degrade gracefully?&lt;/li&gt;&lt;/ol&gt;&lt;h5 id=&quot;closing-thought&quot;&gt;Closing thought&lt;/h5&gt;&lt;p&gt;Technologies come and go, but the contract with the browser remains. Choose the simplest path that produces high quality HTML, CSS, and JavaScript. The closer you stay to the platform, the easier your product will be to maintain, to make accessible, and to run fast at scale.&lt;/p&gt;&lt;h2 id=&quot;post-summary&quot;&gt;Post Summary&lt;/h2&gt;&lt;p&gt;I have to admit, my posts always seem to take on a life of their own and end up being longer than I plan. Concise writing might be a goal for another day. If you made it all the way here, congratulations, you’ve officially joined the “end of post club”! I really hope you found this journey as enjoyable to read as it was for me to write. Revisiting these ideas was a real trip down memory lane, and it reminded me of things I hadn’t thought about in years.&lt;/p&gt;&lt;p&gt;Your thoughts and feedback are always welcome. If I’ve overlooked a method or technique you think deserves a mention, let me know and I’ll happily credit you in the changelog. Thanks again for sticking with me to the very end, and if you’d like to &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;share your thoughts, you can do so here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;26/08/25: Initial post published.&lt;/li&gt;&lt;li&gt;27/08/25: Fixed number ordering of headers.&lt;/li&gt;&lt;li&gt;27/08/25: Added Table of Contents for easier navigation!&lt;/li&gt;&lt;li&gt;28/08/25: Added Silverlight and Java Applets to the post (Thanks to an AI hallucination, regarding browser plugins from the past)&lt;/li&gt;&lt;li&gt;17/09/25: Thanks to Sven Kannengiesser for drawing my attention to the use of ‘here’ anchors. I have now revised them to be contextual and accessible.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Configuring your Content-Security-Policy on your development environment in 11ty</title>
    <link href="https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/" />
    <updated>2025-02-04T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/</id>
    <content type="html">&lt;p&gt;This is just a short post to discuss how I improved my &lt;code&gt;Content-Security-Policy&lt;/code&gt; (CSP), on my local development environment in 11ty. This is essentially a follow-on post from my &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/&quot;&gt;Securing your static website with HTTP response headers&lt;/a&gt; I wrote last year.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: this post isn’t exclusive to 11ty—it’s just the static site generator I use for this site. The code can be easily adapted for any static site running on a Node.js-based server—or practically any local web server!&lt;/p&gt;&lt;h2 id=&quot;why-is-this-useful&quot;&gt;Why is this useful?&lt;/h2&gt;&lt;p&gt;&lt;code&gt;&amp;lt;rant&gt;&lt;/code&gt;It&#39;s incredibly useful for me because my Cloudflare build currently takes 7 minutes and 30 seconds to complete! I&#39;ve no idea why it takes this long, and as of yet I&#39;ve had absolutely no response from Cloudflare on why this is! What I &lt;em&gt;do&lt;/em&gt; know is that something happened on the 19th December 2024 between 12:33AM and 12:19PM because my Cloudflare Pages build time increased by a massive 460%! See the screenshots below for proof of that fact:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; &lt;img alt=&quot;Screenshot of my Cloudflare Pages build process taking 1 minute 30 seconds for a change I made with the git hash of efd9011&quot; decoding=&quot;async&quot; height=&quot;67&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/Inwi9dyaSd-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/Inwi9dyaSd-300.webp 300w, https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/Inwi9dyaSd-600.webp 600w, https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/Inwi9dyaSd-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Note the date, time, and git hash in the screenshot above: 12:33AM December 19, 2024, hash &lt;code&gt;efd9011&lt;/code&gt;. Now let&#39;s compare it to the screenshot below:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt; &lt;img alt=&quot;Screenshot of my Cloudflare Pages build process taking 7 minute 22 seconds! For a change I made with the git hash of efd9011, a 460% increase!&quot; decoding=&quot;async&quot; height=&quot;78&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/x7PPa9X04n-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/x7PPa9X04n-300.webp 300w, https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/x7PPa9X04n-600.webp 600w, https://nooshu.com/blog/2025/02/04/configuring-your-content-security-policy-on-your-development-environment-in-11ty/x7PPa9X04n-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So in this screenshot above, we have the same git hash: &lt;code&gt;efd9011&lt;/code&gt;, but 12 hours have passed, it is now 12:19PM December 19, 2024.&lt;/p&gt;&lt;p&gt;Clearly my code hasn&#39;t changed because the git hash is the identical and nothing else has been committed, but for some reason my build time has increased by 460%! It&#39;s my guess that something changed on Cloudflare&#39;s side in this 12-hour period to drastically increase the build time. Even though I posted about it on the &lt;a href=&quot;https://discord.com/invite/cloudflaredev&quot;&gt;Cloudflare Discord server&lt;/a&gt; and the &lt;a href=&quot;https://community.cloudflare.com/t/11ty-v3-0-0-build-time-jumped-from-1min-20-to-7-minutes/750917&quot;&gt;Cloudflare Community Forums&lt;/a&gt;, I&#39;ve had no response to tell me what was changed and if it is possible to fix it. So if anyone from Cloudflare is reading, I&#39;m begging you, please, can you investigate what changed in that 12-hours and tell me if there&#39;s something I can change to drop my build time back to 1 minute 30 seconds!&lt;code&gt;&amp;lt;/rant&gt;&lt;/code&gt;&lt;/p&gt;&lt;p&gt;So you&#39;re probably asking: &quot;What on earth has any of this got to do with a &quot;Content-Security-Policy&quot; response header? Well, a 1-minute 30-second rebuild time to update my site&#39;s CSP in the 11ty &lt;code&gt;_headers&lt;/code&gt; file is a reasonable time to wait to test for issues. But waiting 7 minutes 20 seconds for every little change just isn&#39;t efficient or workable! Quite frankly it&#39;s incredibly frustrating. And if you&#39;ve ever worked on writing a CSP before you will realise how convoluted and unreasonably lengthy they can become! There had to be a better way to do this, and there is! So let me take you through the node.js / 11ty solution I used below to make my latest CSP changes.&lt;/p&gt;&lt;h2 id=&quot;the-code&quot;&gt;The code&lt;/h2&gt;&lt;p&gt;Let&#39;s have a look at the code:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// This is my site&#39;s Content Security Policy.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Modify this CSP, don&#39;t just copy / paste it! It will break your site!&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// You can also use `var` and `let` depending on your coding syntax, they all work&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CSP&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
base-uri &#39;self&#39;;
child-src &#39;self&#39;;
connect-src &#39;none&#39;;
default-src &#39;none&#39;;
img-src &#39;self&#39; https://v1.indieweb-avatar.11ty.dev/;
font-src &#39;self&#39;;
form-action &#39;self&#39; https://webmention.io https://submit-form.com/DmOc8anHq;
frame-ancestors &#39;self&#39;;
frame-src &#39;self&#39; https://player.vimeo.com/ https://www.slideshare.net/ https://www.youtube.com/ https://giscus.app/ https://www.google.com/;manifest-src &#39;self&#39;;
media-src &#39;self&#39;;
object-src &#39;none&#39;;
script-src &#39;self&#39; https://ajax.cloudflare.com https://giscus.app/ https://www.google.com/ https://www.gstatic.com/;
style-src &#39;self&#39; https://giscus.app/;
worker-src &#39;self&#39;;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// This is the middleware for our 11ty development server&lt;/span&gt;
eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setServerOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;text/html; charset=UTF-8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Content-Security-Policy&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CSP&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you will see I have formatted the CSP using template literals to make the CSP more readable, and thus easier to modify and test on my local environment. You can find the &lt;a href=&quot;https://gist.github.com/Nooshu/472183d0586a52dd79e0c7d8140ddac6&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;public gist for the code above here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;A couple of notes on this code:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Notice how I removed newlines from the final CSP variable. Using &lt;code&gt;const&lt;/code&gt; signals to other developers that it shouldn&#39;t change, but since strings are immutable in JavaScript, modifying it using &lt;code&gt;.replace()&lt;/code&gt; creates a new valid CSP header string.&lt;/li&gt;&lt;li&gt;I&#39;ve added detection for both &quot;naked URLs&quot; and those ending in &lt;code&gt;/index.html&lt;/code&gt;. This is just for convenience really, as it saves having to add the &lt;code&gt;index.html&lt;/code&gt; to URLs before the response header is sent.&lt;/li&gt;&lt;li&gt;Lastly, the above CSP in its current form &lt;strong&gt;will&lt;/strong&gt; break 11ty&#39;s live reload functionality because the &lt;code&gt;connect-src&lt;/code&gt; in the CSP is set to &lt;code&gt;none&lt;/code&gt; which tells the browser to block all network requests initiated by the page.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The following network communication technologies are blocked by this directive:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API&quot;&gt;Fetch API&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest&quot;&gt;XMLHttpRequest&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebSocket&quot;&gt;WebSockets&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/EventSource&quot;&gt;EventSource (Server-Sent Events)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API&quot;&gt;WebRTC connections&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In order to fix point 3 there are 2 options:&lt;/p&gt;&lt;h3 id=&quot;1-modify-the-connect-src-directive&quot;&gt;1) Modify the &lt;code&gt;connect-src&lt;/code&gt; directive&lt;/h3&gt;&lt;p&gt;This is the obvious fix for the issue:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// add the following to your CSP variable&lt;/span&gt;
connect&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;ws&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; wss&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;localhost&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; ws&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;localhost&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By modifying the &lt;code&gt;connect-src&lt;/code&gt; directive, we are allowing connections on localhost via HTTP and WebSockets (which 11ty&#39;s live reload script relies on to push updates to the browser without having to reload the whole page). And we are also allowing WebSocket connections that are both encrypted (&lt;code&gt;wss:&lt;/code&gt;) and unencrypted (&lt;code&gt;ws:&lt;/code&gt;).&lt;/p&gt;&lt;h3 id=&quot;2-modify-the-default-src-directive&quot;&gt;2) Modify the &lt;code&gt;default-src&lt;/code&gt; directive&lt;/h3&gt;&lt;p&gt;This is a less obvious fix for the issue:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// add the following to your CSP variable&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;src &lt;span class=&quot;token string&quot;&gt;&#39;self&#39;&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;ws&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; wss&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;localhost&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; ws&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;localhost&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The reason this works is that &lt;code&gt;default-src&lt;/code&gt; is the &quot;fallback&quot; directive. So assuming your &lt;code&gt;connect-src&lt;/code&gt; is set to &lt;code&gt;self&lt;/code&gt; then the following &lt;code&gt;ws: wss: http://localhost:* ws://localhost:*;&lt;/code&gt; would still be followed because the directive is falling back to &lt;code&gt;default-src&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT&lt;/strong&gt;: don&#39;t use &lt;code&gt;default-src&lt;/code&gt; if your &lt;code&gt;connect-src&lt;/code&gt; is set to &lt;code&gt;none&lt;/code&gt;! The value of &lt;code&gt;none&lt;/code&gt; will still block all connections and override the &lt;code&gt;default-src&lt;/code&gt; settings.&lt;/p&gt;&lt;p&gt;The advantage of modifying the &lt;code&gt;default-src&lt;/code&gt; directive is that it will be applied to other directives like &lt;code&gt;script-src&lt;/code&gt; and &lt;code&gt;img-src&lt;/code&gt;. So it really depends on your CSP requirements for your local development environment! For most readers, I&#39;d guess modifying the &lt;code&gt;connect-src&lt;/code&gt; directive will be enough. But I&#39;ve added both options for completeness.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Believe it or not, I kept the blog post short this time! I hope this code snippet makes configuring your 11ty CSP a breeze! As always, thanks for reading and if you have any feedback or comments, please &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;04/02/25: Initial post published.&lt;/li&gt;&lt;li&gt;09/02/25: Thanks to vrugtehagel from discord for his couple of tweaks to the &lt;code&gt;const CSP&lt;/code&gt; code. The small changes can be seen in the &lt;a href=&quot;https://gist.github.com/Nooshu/472183d0586a52dd79e0c7d8140ddac6/revisions&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;gist revisions&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; if you are interested.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Lets create a plaintext RSS feed with 11ty</title>
    <link href="https://nooshu.com/blog/2025/01/29/lets-create-a-plaintext-rss-feed-with-11ty/" />
    <updated>2025-01-29T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/01/29/lets-create-a-plaintext-rss-feed-with-11ty/</id>
    <content type="html">&lt;p&gt;Before writing this blog post I had 3 types of RSS feeds for my blog posts:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/feed/feed-atom.xml&quot;&gt;Atom&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/feed/feed-rss.xml&quot;&gt;RSS&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/feed/feed.json&quot;&gt;JSON&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;In this post, I&#39;m going to add a 4th type: Plaintext.&lt;/p&gt;&lt;h2 id=&quot;origin&quot;&gt;Origin&lt;/h2&gt;&lt;p&gt;A few months ago I was browsing my contacts and past colleagues looking for people to follow for my &lt;a href=&quot;https://nooshu.com/blogroll/&quot;&gt;blogroll page&lt;/a&gt;. In doing so I just so happened to remember &lt;a href=&quot;https://mastodon.social/@edent&quot;&gt;Terence Eden&lt;/a&gt; who I worked with at Government Digital Service (GDS). Terence (or &lt;a href=&quot;https://edent.tel/&quot;&gt;edent&lt;/a&gt;, as he is known on most social networks), is a prolific blogger! I honestly don&#39;t know where he gets the time to blog so much &lt;strong&gt;and&lt;/strong&gt; work, eat, sleep, blink, and breathe! While looking at the source code of his blog for his RSS feed, I just so happened to notice he had a &lt;a href=&quot;https://shkspr.mobi/blog/.txt&quot;&gt;plaintext version of his RSS feed&lt;/a&gt;. At the time it was responding with a 404, so I let him know on &lt;a href=&quot;https://mastodon.social/@edent&quot;&gt;Mastodon&lt;/a&gt;, and he fixed it in a matter of minutes. Once fixed, I could see how it was formatted, and I set it as a personal challenge to replicate this feature on this blog (once all other more important functionality had been added). Now you may be asking, why would you need a plaintext version? So, let&#39;s answer that question first!&lt;/p&gt;&lt;h2 id=&quot;why-add-a-plaintext-rss-feed&quot;&gt;Why add a plaintext RSS feed?&lt;/h2&gt;&lt;p&gt;Other than a personal challenge, what are the reasons for adding a plaintext version of your RSS feed?&lt;/p&gt;&lt;h3 id=&quot;greater-syndication&quot;&gt;Greater syndication&lt;/h3&gt;&lt;p&gt;Now I have no idea how people like to read my content (I&#39;m just very glad they do!). So if there&#39;s any way I can make their lives easier by adding multiple formats of the same RSS feed, then I&#39;m happy to do it. After all, I&#39;m always looking for ways to make this blog more open and inclusive. Even if it&#39;s only for a handful of readers, then it&#39;s worth adding the functionality, as it really is &quot;set it and forget it&quot; once deployed!&lt;/p&gt;&lt;h3 id=&quot;command-line-feed-readers&quot;&gt;Command-line feed readers&lt;/h3&gt;&lt;p&gt;I had no idea until I started researching the topic that there are users who use their terminal window as an RSS feed reader. For this they use programs like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/newsboat/newsboat&quot;&gt;newsboat&lt;/a&gt;: &quot;An RSS/Atom feed reader for text terminals&quot; — It looks to have a very active repository on GitHub with 217 forks and 3,100 stars!&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://lzone.de/liferea&quot;&gt;Liferea&lt;/a&gt;: &quot;Liferea (Linux Feed Reader), a newsreader for GTK/GNOME&quot; — Again an active &lt;a href=&quot;https://github.com/lwindolf/liferea&quot;&gt;GitHub repository&lt;/a&gt; with 129 forks and 830+ stars. I also noticed the project is over 20 years old (looking at some of the commits)!&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/martinrotter/rssguard&quot;&gt;RSS Guard&lt;/a&gt;: &quot;Feed reader (podcast player and also Gemini protocol client) which supports RSS/ATOM/JSON and many web-based feed services.&quot; — another RSS client with an active GitHub repository with 130 forks and 1,800 stars. It&#39;s not surprising really considering the &lt;a href=&quot;https://rssguard.readthedocs.io/en/stable/supported-os.html&quot;&gt;number of operating systems it supports&lt;/a&gt;!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So if you were in any doubt that plaintext RSS feed users don&#39;t exist, then I think the numbers above prove otherwise! (before you get angry, I do realise that not all these three programs users will be plaintext users!).&lt;/p&gt;&lt;h3 id=&quot;privacy-and-security-concerns&quot;&gt;Privacy and Security Concerns&lt;/h3&gt;&lt;p&gt;You only have to look at the Web Almanac 2024&#39;s chapters on &lt;a href=&quot;https://almanac.httparchive.org/en/2024/privacy&quot;&gt;Privacy&lt;/a&gt; and &lt;a href=&quot;https://almanac.httparchive.org/en/2024/security&quot;&gt;Security&lt;/a&gt; to realise how important these two topics are on the modern web. So it&#39;s completely understandable that RSS users are concerned about this when they subscribe to RSS feeds. Thankfully, this is where plaintext RSS feeds excel. By using plaintext RSS feeds, users can avoid loading external content such as images or scripts, thus reducing the risk of tracking and also enhancing their privacy.&lt;/p&gt;&lt;h3 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h3&gt;&lt;p&gt;I&#39;ve seen accessibility brought up as a plus for plaintext RSS feeds, but honestly, I’m a little sceptical. Sure, a screen reader can read the content easily enough, but when it comes to navigation and overall user experience for someone with accessibility needs, navigating a plaintext feed seems like it’d be a step backwards. Wouldn’t something like HTML or XML, with its semantic markup and built-in navigation, be a way better option? That said, I’m totally open to being wrong here—if I’ve got this all incorrect, feel free to &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;data-efficiency&quot;&gt;Data Efficiency&lt;/h3&gt;&lt;p&gt;This one really doesn&#39;t need much discussion, really. You can&#39;t get much more minimal in terms of bandwidth usage than a plaintext RSS feed. No images, no JavaScript, no markup, just pure content. It certainly makes for an excellent source of content for users with limited internet access or those operating in low-bandwidth environments.&lt;/p&gt;&lt;h3 id=&quot;customisation-and-integration&quot;&gt;Customisation and Integration&lt;/h3&gt;&lt;p&gt;Tools like &lt;a href=&quot;https://github.com/Kombustor/rss-fulltext-proxy&quot;&gt;RSS Fulltext Proxy&lt;/a&gt; and &lt;a href=&quot;http://ftr.fivefilters.org/&quot;&gt;Five Filters&lt;/a&gt; allow for text extraction from any website to convert partial feeds into full-text. This allows for integration into any feed reader, without plugins, or additional configuration. These are exceptional options if you want to integrate a particular feed into one of your workflows.&lt;/p&gt;&lt;h3 id=&quot;reliability-and-readability&quot;&gt;Reliability and Readability&lt;/h3&gt;&lt;p&gt;You literally can&#39;t get a more robust format than plaintext. It is universally supported and small. And in terms of the resulting readability, there&#39;s no cookie banners, adverts, or newsletter signup forms that pop up halfway through an article. Thankfully, none of these annoyances are possible in plaintext, so you can consume all the content without interruption. It reminds me of how the World Wide Web used to be before it was commercialised beyond recognition! Take a read of the &lt;a href=&quot;https://info.cern.ch/hypertext/WWW/TheProject.html&quot;&gt;world&#39;s first website&lt;/a&gt;, and you get an idea of what &lt;a href=&quot;https://www.home.cern/science/computing/birth-web&quot;&gt;Sir Tim Berners-Lee&lt;/a&gt; had in mind when he invented it in 1989 at CERN.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The web was originally conceived and developed to meet the demand for automated information-sharing between scientists in universities and institutes around the world.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;The WorldWideWeb (W3) is a wide-area hypermedia information retrieval initiative aiming to give universal access to a large universe of documents.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Anyway, enough of the &quot;why&#39;s&quot; let&#39;s crack on with the 11ty implementation!&lt;/p&gt;&lt;h2 id=&quot;the-code&quot;&gt;The code&lt;/h2&gt;&lt;h3 id=&quot;eleventy-config-js&quot;&gt;eleventy.config.js&lt;/h3&gt;&lt;p&gt;First, let&#39;s modify the 11ty config file:&lt;/p&gt;&lt;p&gt;Here we are telling 11ty to process &lt;code&gt;.txt&lt;/code&gt; files as template files.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addTemplateFormats&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;txt&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next we need to configure 11ty and tell it how to handle &lt;code&gt;.txt&lt;/code&gt; files with a custom handler.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addExtension&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;txt&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property literal-property&quot;&gt;outputFileExtension&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;txt&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token function function-variable&quot;&gt;compile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;inputContent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; inputContent&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The code is fairly self-explanatory &lt;code&gt;outputFileExtension&lt;/code&gt; is obvious, the resulting output will have a &lt;code&gt;.txt&lt;/code&gt; extension. The &lt;code&gt;compile&lt;/code&gt; function is a simple pass-through. It takes the &lt;code&gt;txt&lt;/code&gt; input and returns the original content unchanged.&lt;/p&gt;&lt;h3 id=&quot;feed-txt-njk&quot;&gt;feed.txt.njk&lt;/h3&gt;&lt;p&gt;Next up is the template for the plaintext feed output. I&#39;m using Nunjucks as that&#39;s the main templating language I use across this blog, but I&#39;m sure you&#39;d be able to any other templating language 11ty supports too. This file sits in my &lt;code&gt;/content/feed/&lt;/code&gt; directory, so 11ty will process it as a &lt;code&gt;njk&lt;/code&gt; file in the output.&lt;/p&gt;&lt;pre class=&quot;language-njk&quot; tabindex=&quot;0&quot; data-gist=&quot;da04fd0322e45936fbbc5c663ebc9677&quot;&gt;&lt;code class=&quot;language-njk&quot;&gt;
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;permalink&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;feed&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;feed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;txt&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;eleventyComputed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token variable&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;
# &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
## &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;fulldescription&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;collections&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;posts&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;reverse&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;index0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;Published&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;readableDate&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token variable&quot;&gt;Main&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;Content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;templateContent&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;striptags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;decodeHtmlEntities&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;last&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;End&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;



&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endif&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endif&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endfor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A Gist for this template &lt;a href=&quot;https://gist.github.com/Nooshu/da04fd0322e45936fbbc5c663ebc9677&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;is here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;, if you find that easier to read.&lt;/p&gt;&lt;p&gt;There&#39;s a fair amount going on in this template file, but it is essentially:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Setting where I want the feed to sit in my final site output (&lt;code&gt;permalink&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Overriding the default layout dynamically during the build process (&lt;code&gt;layout: null&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Populating the top of the feed with my basic blog metadata.&lt;/li&gt;&lt;li&gt;Looping through all by blog posts in reverse (newest first).&lt;/li&gt;&lt;li&gt;Limiting the number of posts in the feed to 10.&lt;/li&gt;&lt;li&gt;Outputting the parts of each post I want in the feed using Nunjucks and cleaning up the output using various filters, which I will go through next.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;Code Notes&lt;/strong&gt;:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;You may be looking at this template and wondering what is going on with all the space between the logic. Well, since this template is outputting plaintext, this is all intentional spacing to make the final output more readable.&lt;/li&gt;&lt;li&gt;I&#39;m using &lt;code&gt;loop.index0&lt;/code&gt; which is the current iteration of the loop but 0 indexed because it was the only way that seemed to work for me to limit the number of posts. I think I must have a weird &lt;code&gt;collections.posts&lt;/code&gt; setup somewhere because regardless of what I tried, nothing worked. Likewise, I believe the native &lt;code&gt;slice(0, 10)&lt;/code&gt; &lt;em&gt;should&lt;/em&gt; do the same thing, but for me, the loop stopped working altogether. I tried debugging it for a couple of hours and eventually stuck with the &lt;code&gt;loop.index0&lt;/code&gt; setup because it actually worked. However, if anyone has any ideas on why the native &lt;code&gt;slice()&lt;/code&gt; function on a &lt;code&gt;collections.posts&lt;/code&gt; doesn&#39;t work, please &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;! I&#39;d love to get to the bottom of what the issue is!&lt;/li&gt;&lt;li&gt;You may notice my use of the &lt;code&gt;-%}&lt;/code&gt; in the template, this is me telling Nunjucks to trim any whitespace (spaces, tabs, newlines) directly following the tag. This was required because it was adding numerous new lines in the resulting output. It&#39;s when I discover little intricacies in Nunjucks like this, that I realise what a versatile templating language it actually is!&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&quot;filters&quot;&gt;filters&lt;/h3&gt;&lt;p&gt;Most of the filters used in the above template are all pretty standard to the Nunjucks language:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://mozilla.github.io/nunjucks/templating.html#safe&quot;&gt;safe&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://mozilla.github.io/nunjucks/templating.html#reverse&quot;&gt;reverse&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://mozilla.github.io/nunjucks/templating.html#striptags-value-preserve_linebreaks&quot;&gt;striptags(true)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I only added one custom 11ty filter for this functionality.&lt;/p&gt;&lt;h4 id=&quot;decodehtmlentities&quot;&gt;decodeHtmlEntities&lt;/h4&gt;&lt;p&gt;For some reason, I had some very odd character encoding issues, and the only way I could resolve the issue was to create a custom 11ty filter to search and replace them throughout the content. I looked through the &lt;a href=&quot;https://mozilla.github.io/nunjucks/templating.html#filters&quot;&gt;default Nunjucks filters&lt;/a&gt; for a solution, but filters like &lt;code&gt;escape&lt;/code&gt; and &lt;code&gt;forceescape&lt;/code&gt; didn&#39;t work. I could have probably used Nunjucks native &lt;a href=&quot;https://mozilla.github.io/nunjucks/templating.html#replace&quot;&gt;replace&lt;/a&gt; filter, but that would have cluttered up the template, so decided to move it to an 11ty filter instead:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// clean up the HTML entities in the RSS feed text&lt;/span&gt;
eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addFilter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;decodeHtmlEntities&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;&amp;([^;]+);&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;match&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entities &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token property string-property&quot;&gt;&#39;amp&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property string-property&quot;&gt;&#39;apos&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&#39;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property string-property&quot;&gt;&#39;lt&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property string-property&quot;&gt;&#39;gt&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property string-property&quot;&gt;&#39;quot&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token property string-property&quot;&gt;&#39;nbsp&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// Handle numeric entities&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entity&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;#&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; code &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; entity&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;x&#39;&lt;/span&gt;
							&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
							&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fromCharCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; entities&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;entity&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s basically just a fancy way to search and replace for multiple encoding issues it finds in the plaintext output.&lt;/p&gt;&lt;p&gt;Just to be clear, I wrote a really clunky version that did the same thing as this first. Basically, a chain of &lt;code&gt;text.replaceAll(&#39;&amp;ampamp;&#39;, &#39;&amp;&#39;).replaceAll(&#39;&amp;ampapos;&#39;, &quot;&#39;&quot;)...&lt;/code&gt; and so on, I think you get the idea.&lt;/p&gt;&lt;p&gt;It worked fine, but then I asked ChatGPT to optimise it, and this is the result it came up with! It&#39;s certainly not as readable, but it still does the job, and it also handles numeric entities too! Off the back of this example, I think AI used correctly can be a fantastic teaching tool, especially when it comes to random little filter functions like this.&lt;/p&gt;&lt;h3 id=&quot;adding-the-feed-to-your-head&quot;&gt;Adding the feed to your &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;Once you have the feed up and running, it&#39;s time to make sure you have it visible to your users by adding it to your pages &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;. It&#39;s dead simple, just like your other feeds, only the &lt;code&gt;type&lt;/code&gt; is set to &lt;code&gt;text/plain&lt;/code&gt;.&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;alternate&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/feed/feed.txt&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/plain&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Nooshu - Matt Hobbs blog post Plaintext Feed.&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I also have links to all my feed formats in my footer so they are easy to find. So, finally, why not look at the result of the above code now by looking at my &lt;a href=&quot;https://nooshu.com/feed/feed.txt&quot;&gt;plaintext RSS feed&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;sample-output&quot;&gt;Sample output&lt;/h2&gt;&lt;p&gt;Here&#39;s an example of the output from the code above:&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;# Nooshu - Matt Hobbs - Frontend web developer, turned engineering manager.
## This is the website of Matt Hobbs, who is a Frontend Engineering Manager from Oxfordshire, UK.
URL: https://nooshu.com

--- Start: The Speed Trifecta: 11ty, Brotli 11, and CSS Fingerprinting
Published on: 23 January 2025
https://nooshu.com/blog/2025/01/23/the-speed-trifecta-11ty-brotli-11-and-css-fingerprinting/

Main Content:
So recently, I have written two 11ty related blog posts:

Using an 11ty Shortcode to craft a custom CSS pipeline
Cranking Brotli up to 11 with Cloudflare Pro and 11ty

...more blogpost content here...

Post changelog:

23/01/25: Initial post published.

--- End: The Speed Trifecta: 11ty, Brotli 11, and CSS Fingerprinting&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Whew!&lt;/strong&gt; Another blog post in the books, and another shiny new feature added—this time, a working plaintext RSS feed! (That’s one more thing off the to-do list—always a win!) Thanks for stopping by! I hope you found this post interesting, maybe even &lt;em&gt;useful&lt;/em&gt;? As always, if you’ve got thoughts, or feedback, please &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;29/01/25: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>The Speed Trifecta: 11ty, Brotli 11, and CSS Fingerprinting</title>
    <link href="https://nooshu.com/blog/2025/01/23/the-speed-trifecta-11ty-brotli-11-and-css-fingerprinting/" />
    <updated>2025-01-23T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/01/23/the-speed-trifecta-11ty-brotli-11-and-css-fingerprinting/</id>
    <content type="html">&lt;p&gt;So recently, I have written two 11ty related blog posts:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/&quot;&gt;Using an 11ty Shortcode to craft a custom CSS pipeline&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/&quot;&gt;Cranking Brotli up to 11 with Cloudflare Pro and 11ty&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;If you haven&#39;t read them, no problem, they are always there if you&#39;re ever struggling to sleep!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;:&lt;/p&gt;&lt;p&gt;In the first post I look at how I&#39;ve automated CSS fingerprinting on production (Cloudflare), resulting in the ability to use long-life &lt;code&gt;Cache-Control&lt;/code&gt; directives like &lt;code&gt;max-age=31536000&lt;/code&gt; and the &lt;code&gt;immutable&lt;/code&gt; browser hint.&lt;/p&gt;&lt;p&gt;In the second post, I look at how you can improve Brotli compression by manually compressing your assets to Brotli setting 11 (max) then serving them via a Cloudflare Pro plan. This gives a significant improvement in file size over the Brotli compression setting of 4 that &lt;a href=&quot;https://blog.cloudflare.com/this-is-brotli-from-origin/&quot;&gt;Cloudflare uses for dynamic (on the fly) compression&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;While working on both posts, it dawned on me that blending the approaches they cover would result in the ideal CSS configuration for my little blog. In this post, I&#39;ll show you exactly how I&#39;ve done it. Let&#39;s get started!&lt;/p&gt;&lt;h2 id=&quot;original-code&quot;&gt;Original code&lt;/h2&gt;&lt;p&gt;So I&#39;m going to be using the code I wrote in the CSS fingerprinting blog post I wrote recently. You can see it below, or in &lt;a href=&quot;https://gist.github.com/Nooshu/edfc2e382bc249e92ab238779357c93e&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;this Gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;edfc2e382bc249e92ab238779357c93e&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; dotenv &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dotenv&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; CleanCSS &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;clean-css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; crypto &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;crypto&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
dotenv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// create a single instance of the CleanCSS function&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// to be used in file loops. Add additional optimisation settings in here.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cleanCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CleanCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeDuplicateRules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// turns on removing duplicate rules&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;manipulateCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addShortcode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;customCSS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cssPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// output the file with no fingerprinting if on the dev environment&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// (allows auto-reload when the CSS is modified)&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ELEVENTY_ENV&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;development&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Using path.join for better cross-platform compatibility&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputFile &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./public&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cssPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./.cache&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Check if input file exists first&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Input CSS file not found: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Ensure both cache and output directories exist&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dir &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Read the input CSS file&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Initialises a new hashing instance&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;sha256&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Feed CSS data into the hash function&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Specify the hash should be returned as a hexadecimal string&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;hex&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Only take the first 10 characters of the hash&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// Generate our CSS Cache name&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;[&#92;/&#92;&#92;]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// This is where the file will be written&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cachePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// store our manipulated CSS in this variable&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// check we have a cache directory&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// read the cached CSS file&lt;/span&gt;
				processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Use the memoized cleanCSS instance to minify the input CSS&lt;/span&gt;
				processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cleanCSS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;minify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;styles&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Split the input file path into its components (directory, filename, extension)&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parsedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Use path.join for output paths&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ext&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Write the optimised CSS to the final output location with the hash in the filename&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// path manipulation for final URL&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hashedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; finalFilename&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;&#92;&#92;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// return our final link element with optimised and fingerprinted CSS.&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hashedPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Error processing CSS:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;install-zlib&quot;&gt;Install zlib&lt;/h3&gt;&lt;p&gt;The first thing we shall do is install the &lt;a href=&quot;https://www.npmjs.com/package/zlib&quot;&gt;zlib&lt;/a&gt; package from npm, as this is what we are going to use to compress the CSS file all the way up to 11 on production.&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; zlib&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we import it into our ESM code above:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; zlib &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;zlib&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Don&#39;t worry, I&#39;m not going to go this slow all the way through the code. Feel free to &lt;a href=&quot;https://nooshu.com/blog/2025/01/23/the-speed-trifecta-11ty-brotli-11-and-css-fingerprinting/#final-code&quot;&gt;skip straight to the final code&lt;/a&gt; if you so wish!&lt;/p&gt;&lt;h3 id=&quot;setting-the-default-compression-level&quot;&gt;Setting the default compression level&lt;/h3&gt;&lt;p&gt;This is a completely optional step, it really depends on how, and where you store all your build variables. But I&#39;m going to be storing the Brotli Compression level I want to use on production both in the JS (as a default to fall back on if the environment variable doesn&#39;t exist), and also an environment variable on production. In the JavaScript below, set at 6 to show it can be any value between 0-11 (3 or 4 is what most CDN use for dynamic compression). Cloudflare&#39;s compression is &lt;a href=&quot;https://blog.cloudflare.com/results-experimenting-brotli/&quot;&gt;set to 4&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Default Brotli compression level if not set in the environment&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And also in my &lt;code&gt;.env&lt;/code&gt; file in development:&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;BROTLI_COMPRESSION_LEVEL=11&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we set our compression level as a constant in our JavaScript for use later in the code:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliCompressionLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since our environment variable is currently set, compression will be set to 11, but if that variable isn&#39;t set, it will fall back to 6, the default value set above.&lt;/p&gt;&lt;h3 id=&quot;cloudflare-and-brotli&quot;&gt;Cloudflare and Brotli&lt;/h3&gt;&lt;p&gt;Before we go through the Brotli compression code, it&#39;s a good time to mention that Cloudflare does something automatically that is very useful for compressed files. Cloudflare Pages will automatically serve Brotli-compressed files (&lt;code&gt;.br&lt;/code&gt;) if available, with no additional configuration required on the Cloudflare side. See the &lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/compression/&quot;&gt;Content Compression Documentation&lt;/a&gt; on Cloudflare for more information.&lt;/p&gt;&lt;p&gt;This means we don&#39;t need to rename the compressed CSS file from &lt;code&gt;index-hash.css.br&lt;/code&gt; back to &lt;code&gt;index-hash.css&lt;/code&gt; for a user&#39;s browser to recognise it as a CSS file. Handy huh! Don&#39;t worry if you don&#39;t use Cloudflare Pages, I also have a version written that renames the CSS file back to &lt;code&gt;index-hash-compressed.css&lt;/code&gt; for non-Cloudflare Pages, readers.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: While testing the documented method above, I could not find a way to confirm that my static compressed version was being served &lt;strong&gt;automatically&lt;/strong&gt;. So in the code below I have removed this assumption and explicitly added my statically compressed version to the &lt;code&gt;&amp;lt;link&gt;&lt;/code&gt; tag. Please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt; if I&#39;m interpreting the documentation for this functionality wrong! As I&#39;d love to see it in action, and also how you verify the &lt;code&gt;.br&lt;/code&gt; file is actually being served automatically?&lt;/p&gt;&lt;h3 id=&quot;brotli-compression-code&quot;&gt;Brotli Compression code&lt;/h3&gt;&lt;p&gt;Now that I&#39;ve covered that bit of (potential) Cloudflare Pages automation, let&#39;s have a look at the actual compression code:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Brotli compression&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// The output filename for the compressed file&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.br&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Only compress to Brotli if the file doesn&#39;t exist&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// Set our zlib options here e.g. compression&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliOptions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; brotliCompressionLevel &lt;span class=&quot;token comment&quot;&gt;// Use the level specified in the environment&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// zlib does it&#39;s compression magic!&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; zlib&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;brotliCompressSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliOptions&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// Write the compressed code to the output filename defined above&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;generate-the-css-link-tag&quot;&gt;Generate the CSS &lt;code&gt;&amp;lt;link&gt;&lt;/code&gt; tag&lt;/h3&gt;&lt;p&gt;The last thing to do now is mostly the same as what I had in the original code set out above:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// path manipulation for final URL&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hashedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;&#92;&#92;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// return our final link element with optimised and fingerprinted CSS.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hashedPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Only this time I&#39;m modifying the path to the Brotli compressed filename rather than the original uncompressed CSS file! Simple!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;On Development&lt;/strong&gt;: Once added to the 11ty config as per my &lt;a href=&quot;https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/#usage&quot;&gt;previous post&lt;/a&gt;, my development CSS looks like this:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;// uncompressed CSS with no fingerprinting, auto-reload still functioning
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/css/index.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;On Production&lt;/strong&gt;: The CSS looks like this:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;// Brotli 11 compressed, minified and fingerprinted CSS
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/css/index-b9fcfe85ef.css.br&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is the version I have running on the site at the moment, why not view the page source and take a look.&lt;/p&gt;&lt;h3 id=&quot;finishing-touches&quot;&gt;Finishing touches&lt;/h3&gt;&lt;p&gt;That&#39;s the bulk of the work done, but if you were to run this code on production at present you&#39;d get a page full of naked HTML and no styling. This is because the Cloudflare Pages &lt;code&gt;_headers&lt;/code&gt; file needs to be modified, as Cloudflare Pages is now serving my Brotli 11 compressed CSS file, not the uncompressed version we had before.&lt;/p&gt;&lt;p&gt;Simply add the following to your &lt;code&gt;_headers&lt;/code&gt; file:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/css/*
Content-Encoding: br
Vary: Accept-Encoding
Content-Type: text/css&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s important to remember that this headers file is looking at the &lt;strong&gt;path&lt;/strong&gt; from which the file is served on the production website! It isn&#39;t (and can&#39;t) use an extension wildcard, as Cloudflare pages doesn&#39;t support it. It took me a while to figure this out I must admit!&lt;/p&gt;&lt;p&gt;So for example, The following &lt;strong&gt;doesn&#39;t work&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;*.css
Content-Encoding: br
Vary: Accept-Encoding
Content-Type: text/css&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I first migrated to Cloudflare Pages, I tried it and couldn&#39;t work out why it wasn&#39;t working. &lt;code&gt;/css/*&lt;/code&gt; is basically saying: &quot;Any file that resides in the &lt;code&gt;css&lt;/code&gt; directory on production will be served with the following headers&quot;.&lt;/p&gt;&lt;p&gt;Lastly, remember to add the following to your Cloudflare Pages environment variables. This can be plaintext if you like, since the variable is only storing a single integer (not a secret API key):&lt;/p&gt;&lt;pre class=&quot;language-shell&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;BROTLI_COMPRESSION_LEVEL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once added, you should be good to go!&lt;/p&gt;&lt;h2 id=&quot;final-code&quot;&gt;Final code&lt;/h2&gt;&lt;p&gt;As promised, below is the final code that I just described above:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;218b9855c099f368f99560d2fd02c61e&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; zlib &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;zlib&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; dotenv &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dotenv&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; CleanCSS &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;clean-css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; crypto &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;crypto&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
dotenv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// An example of how you could add additional CleanCSS settings if required&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cleanCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CleanCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeDuplicateRules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Default Brotli compression level if not set in the environment&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;manipulateCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addShortcode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;customCSS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cssPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ELEVENTY_ENV&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;development&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputFile &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./public&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cssPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./.cache&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Get compression level from the environment or use the default&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliCompressionLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Input CSS file not found: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dir &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;sha256&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;hex&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;[&#92;/&#92;&#92;]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cachePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cleanCSS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;minify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;styles&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parsedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ext&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// Brotli compression&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// The output filename for the compressed file&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.br&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// Only compress to Brotli if the file doesn&#39;t exist&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Set our zlib options here e.g. compression&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliOptions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; brotliCompressionLevel &lt;span class=&quot;token comment&quot;&gt;// Use the level specified in the environment&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// zlib does it&#39;s compression magic!&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; zlib&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;brotliCompressSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliOptions&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Write the compressed code to the output filename defined above&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hashedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;&#92;&#92;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hashedPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Error processing CSS:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here&#39;s a &lt;a href=&quot;https://gist.github.com/Nooshu/218b9855c099f368f99560d2fd02c61e&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Gist for the code&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; as well!&lt;/p&gt;&lt;h2 id=&quot;with-file-renaming&quot;&gt;With file renaming&lt;/h2&gt;&lt;p&gt;Now I understand not everyone will want to serve their CSS files using the &lt;code&gt;.br&lt;/code&gt; extension. So there&#39;s a version below that also renames the file for you back to &lt;code&gt;.css&lt;/code&gt;, and adds the &lt;code&gt;-compressed&lt;/code&gt; suffix to the filename as well. The rest of the code is identical to the version above:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; zlib &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;zlib&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; dotenv &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dotenv&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; CleanCSS &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;clean-css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; crypto &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;crypto&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
dotenv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// An example of how you could add additional CleanCSS settings if required&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cleanCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CleanCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeDuplicateRules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Default Brotli compression level if not set in the environment&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;manipulateCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addShortcode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;customCSS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cssPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ELEVENTY_ENV&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;development&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputFile &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./public&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cssPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./.cache&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// Get compression level from the environment or use the default&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliCompressionLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Input CSS file not found: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dir &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;sha256&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;hex&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;[&#92;/&#92;&#92;]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cachePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cleanCSS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;minify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;styles&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parsedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ext&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;// Brotli compression with renaming&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; compressedFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-compressed&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ext&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Only compress to Brotli if the file doesn&#39;t exist&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;compressedFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Set our zlib options here e.g. compression&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliOptions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; brotliCompressionLevel
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// zlib does it&#39;s compression magic!&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; zlib&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;brotliCompressSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliOptions&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;// Write the compressed code to the output filename defined above&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;compressedFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hashedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; compressedFilename&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;&#92;&#92;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hashedPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Error processing CSS:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The Gist for the code above &lt;a href=&quot;https://gist.github.com/Nooshu/e8a4b27496cd077f3ab7836e57705142&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;is here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The above code will do the following:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;On Development&lt;/strong&gt;: It will simply output:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/css/index.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Exactly as it did in the previous version.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;On Production&lt;/strong&gt;: This is the output that will be seen:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/css/index-b9fcfe85ef-compressed.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this instance, the CSS file has been Brotli compressed to 11, but it has also been renamed with the &lt;code&gt;-compressed&lt;/code&gt; suffix and the &lt;code&gt;.br&lt;/code&gt; extension has been removed. Technically, you don&#39;t need the suffix, but I&#39;ve added it to emphasise (and remind myself) that it isn&#39;t a &quot;standard&quot; CSS file in plain text.&lt;/p&gt;&lt;p&gt;And as I mentioned in the &lt;a href=&quot;https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/&quot;&gt;previous CSS Shortcode blog post&lt;/a&gt;. You&#39;ll now be able to use long cache life headers like:&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;/[css]/*
Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Safe in the knowledge that updating your CSS will generate a brand-new filename, effectively nullifying the existing cache. Check out the &lt;a href=&quot;https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/#headers-file&quot;&gt;previous post&lt;/a&gt; if you want more details on the above &lt;code&gt;_headers&lt;/code&gt; file code for Cloudflare.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Fantastic! You made it to the end of yet another 11ty blog post of mine, congrats! I&#39;m not sure if there&#39;s anything else I can do to optimise my CSS delivery to users at the moment?&lt;/p&gt;&lt;p&gt;Maybe preloading or the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API&quot;&gt;Speculation Rules API&lt;/a&gt;? Which Cloudflare already supports, and is currently enabled on my site. This functionality is called &quot;Speed Brain&quot; in the Cloudflare dashboard (under the &quot;Speed&quot; then &quot;Content Optimization&quot;). It&#39;s worth noting that this functionality is currently in Beta. But I don&#39;t think I need to do anything manually to use it, since Cloudflare rolled it out to all plans (including the free plan!), &lt;a href=&quot;https://community.cloudflare.com/t/speculation-rules-api-rollout-to-free-plans/708111&quot;&gt;back in September 2024&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;But perhaps there are other optimisations I can make? Unknown, unknowns etc… As always, thanks for reading, I hope you found the post useful and if you have any feedback or comments &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;23/01/25: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Using an 11ty Shortcode to craft a custom CSS pipeline</title>
    <link href="https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/" />
    <updated>2025-01-12T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/</id>
    <content type="html">&lt;p&gt;I know I&#39;m still a bit of a 11ty n00b, so I hope this isn&#39;t frowned upon in the community, but on rebuilding my blog using 11ty, I decided not to use the standard &lt;a href=&quot;https://www.11ty.dev/docs/plugins/bundle/&quot;&gt;Bundle plugin&lt;/a&gt; that was added in &lt;a href=&quot;https://www.11ty.dev/blog/eleventy-v3/&quot;&gt;v3.0.0&lt;/a&gt;. Instead, I decided to write a custom &lt;a href=&quot;https://www.11ty.dev/docs/shortcodes/&quot;&gt;Shortcode&lt;/a&gt; to customise my CSS output. In this blog post, I will go through the code I have written in the hope it will help others and more importantly gather feedback from the community, to see if any improvements can be made to the code. Please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt; if you have any feedback.&lt;/p&gt;&lt;h2 id=&quot;my-requirements&quot;&gt;My Requirements&lt;/h2&gt;&lt;p&gt;First, what am I hoping to achieve with this Shortcode solution?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Maintain the auto-reload functionality that comes with 11ty when you modify your CSS locally (this is a super helpful feature I&#39;ve been a big fan of since &lt;a href=&quot;https://web.archive.org/web/20220317001804/https://livereload.com/&quot;&gt;LiveReload&lt;/a&gt; was released back in February 2011, and later &lt;a href=&quot;https://browsersync.io/&quot;&gt;BrowserSync&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;Filename &lt;a href=&quot;https://en.wikipedia.org/wiki/Fingerprint_(computing)#Virtual_uniqueness&quot;&gt;fingerprinting&lt;/a&gt; to allow me to use long-life &lt;code&gt;Cache-Control&lt;/code&gt; response headers without having to worry about cache becoming outdated. (e.g. &lt;code&gt;max-age=31536000, immutable&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;The ability to optimise my CSS output using the excellent &lt;a href=&quot;https://www.npmjs.com/package/clean-css&quot;&gt;clean-css&lt;/a&gt; Node plugin. It does more than just minify! Check out &lt;a href=&quot;https://www.npmjs.com/package/clean-css#optimization-levels&quot;&gt;all the optimisations&lt;/a&gt; it can help you if you haven&#39;t seen them.&lt;/li&gt;&lt;li&gt;Minimal impact on the 11ty build process by leveraging build caching in some way.&lt;/li&gt;&lt;li&gt;Set it and forget it, once it&#39;s added It requires minimal (or no ongoing maintenance), apart from updating dependencies, every so often!&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;the-code&quot;&gt;The Code&lt;/h2&gt;&lt;p&gt;I&#39;ll quit the waffling and just show you the code I currently have building my CSS for this site. This code sits in its own file called &lt;code&gt;css-manipulation.js&lt;/code&gt; in a &lt;code&gt;_helpers&lt;/code&gt; directory in my 11ty root (i.e. the same level as &lt;code&gt;_data&lt;/code&gt;).&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * CSS Manipulation Module for Eleventy
 *
 * This module provides CSS processing, minification, and compression functionality
 * for an Eleventy static site generator. It handles:
 * - CSS minification using CleanCSS
 * - Content-based file hashing for cache busting
 * - Brotli compression for optimized delivery
 * - Build-time caching to avoid redundant processing
 * - Concurrent request handling to prevent duplicate work
 */&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Node.js built-in modules for file system operations, path manipulation, and hashing&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; crypto &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;crypto&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Used to create SHA-256 hashes of CSS content for cache busting&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// File system operations (reading/writing files, checking existence)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Cross-platform path manipulation and joining&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Node.js zlib module for Brotli compression&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Brotli is a modern compression algorithm that provides better compression ratios than gzip&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; brotliCompressSync &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;zlib&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// CleanCSS library for CSS minification and optimization&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// This performs a wide range of optimizations to reduce file size&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; CleanCSS &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;clean-css&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Environment configuration to check if we&#39;re in local development mode&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; env &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../_data/env.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * CleanCSS Configuration
 *
 * CleanCSS performs multi-level CSS optimization. This configuration enables
 * comprehensive minification while maintaining CSS functionality.
 *
 * Level 1 optimizations (basic cleanups):
 * - cleanupCharsets: Remove unnecessary @charset declarations
 * - normalizeUrls: Normalize and optimize URL() references
 * - optimizeBackground: Optimize background properties
 * - optimizeBorderRadius: Shorten border-radius values
 * - optimizeFilter: Optimize filter() functions
 * - optimizeFont: Optimize font-family declarations
 * - optimizeFontWeight: Convert font-weight to numeric values
 * - optimizeOutline: Optimize outline properties
 * - removeEmpty: Remove empty CSS rules
 * - removeNegativePaddings: Remove invalid negative padding values
 * - removeQuotes: Remove unnecessary quotes from URLs and identifiers
 * - removeWhitespace: Remove unnecessary whitespace
 * - replaceMultipleZeros: Shorten multiple zero values
 * - replaceTimeUnits: Optimize time unit values (e.g., 0s → 0)
 * - replaceZeroUnits: Remove units from zero values (e.g., 0px → 0)
 * - roundingPrecision: Round numeric values to 2 decimal places
 * - selectorsSortingMethod: Sort selectors in a standard order
 * - specialComments: Remove all comments (set to &quot;none&quot;)
 * - tidyAtRules: Clean up @-rules
 * - tidyBlockScopes: Clean up block scopes
 * - tidySelectors: Clean up and optimize selectors
 * - transform: Custom transformation function (empty, no custom transforms)
 *
 * Level 2 optimizations (advanced restructuring):
 * - mergeAdjacentRules: Combine adjacent CSS rules with same selectors
 * - mergeIntoShorthands: Convert long-form properties to shorthand
 * - mergeMedia: Combine @media rules with same conditions
 * - mergeNonAdjacentRules: Merge duplicate rules even if not adjacent
 * - mergeSemantically: Disabled to avoid potentially breaking semantic merges
 * - overrideProperties: Remove overridden properties
 * - removeEmpty: Remove empty rules at level 2
 * - reducePadding: Optimize padding values
 * - reducePositions: Optimize position values
 * - reduceTimingFunctions: Optimize animation timing functions
 * - reduceTransforms: Optimize transform functions
 * - restructureRules: Disabled to avoid aggressive restructuring that might break CSS
 * - skipProperties: Empty array means optimize all properties
 *
 * Format options (output formatting):
 * - All breaks set to false: Output as single line (no line breaks)
 * - indentBy: 0 (no indentation for minimal file size)
 * - indentWith: &quot;space&quot; (if indentation were enabled)
 * - All spaces set to false: Remove unnecessary spaces
 * - wrapAt: false (no line wrapping)
 *
 * Other options:
 * - inline: [&quot;none&quot;] - Don&#39;t inline any @import rules
 * - rebase: false - Don&#39;t rebase URLs (keep original paths)
 * - returnPromise: false - Use synchronous API (we handle async ourselves)
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cleanCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CleanCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;cleanupCharsets&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;normalizeUrls&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;optimizeBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;optimizeBorderRadius&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;optimizeFilter&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;optimizeFont&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;optimizeFontWeight&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;optimizeOutline&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeEmpty&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeNegativePaddings&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeQuotes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeWhitespace&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;replaceMultipleZeros&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;replaceTimeUnits&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;replaceZeroUnits&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;roundingPrecision&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;selectorsSortingMethod&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;standard&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;specialComments&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;none&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;tidyAtRules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;tidyBlockScopes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;tidySelectors&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token function function-variable&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;mergeAdjacentRules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;mergeIntoShorthands&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;mergeMedia&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;mergeNonAdjacentRules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;mergeSemantically&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;overrideProperties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;removeEmpty&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;reducePadding&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;reducePositions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;reduceTimingFunctions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;reduceTransforms&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;restructureRules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;skipProperties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;breaks&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;afterAtRule&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;afterBlockBegins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;afterBlockEnds&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;afterComment&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;afterProperty&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;afterRuleBegins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;afterRuleEnds&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;beforeBlockEnds&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;betweenSelectors&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;indentBy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;indentWith&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;space&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;spaces&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;aroundSelectorRelation&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;beforeBlockBegins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token literal-property property&quot;&gt;beforeValue&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token literal-property property&quot;&gt;wrapAt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;inline&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;none&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;rebase&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;returnPromise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Default Brotli Compression Level
 *
 * Brotli compression levels range from 0-11:
 * - 0: Fastest, least compression
 * - 11: Slowest, best compression (maximum)
 *
 * Level 11 is used by default for production builds where file size matters
 * more than compression time. This can be overridden via BROTLI_COMPRESSION_LEVEL
 * environment variable.
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * In-Memory Build Cache
 *
 * This Map caches CSS processing results during a single build run.
 * It serves two purposes:
 * 1. Prevents re-processing the same CSS file when multiple pages reference it
 * 2. Handles concurrent requests by storing Promises during processing
 *
 * Structure:
 * - Key: cssPath (the original CSS file path)
 * - Value: Either a Promise (if processing is in progress) or an object with:
 *   - hash: Content hash of the CSS file
 *   - processedCSS: The minified CSS string
 *   - htmlOutput: The final HTML &amp;lt;link&gt; tag string
 *
 * This cache is cleared between builds (per-process, not persisted).
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cssBuildCache &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Directory Creation Tracking
 *
 * Tracks which directory combinations have been created during this build.
 * This prevents redundant fs.existsSync() and fs.mkdirSync() calls when
 * multiple CSS files are processed.
 *
 * Structure: Set of strings like &quot;cacheDir:outputDir&quot;
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; directoriesCreated &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Main Function: manipulateCSS
 *
 * Registers a custom Eleventy shortcode that processes CSS files.
 * The shortcode can be used in templates like: 
 *
 * Processing Pipeline:
 * 1. Check if in local development mode (skip processing)
 * 2. Check in-memory cache (avoid duplicate processing)
 * 3. Read and hash the CSS file
 * 4. Check disk cache for minified CSS
 * 5. Minify CSS if not cached
 * 6. Write processed CSS with hash-based filename
 * 7. Compress with Brotli
 * 8. Return HTML &amp;lt;link&gt; tag with hashed filename for cache busting
 *
 * @param {object} eleventyConfig - The Eleventy configuration object
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;manipulateCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;/**
	 * Register the &quot;customCSS&quot; shortcode with Eleventy
	 * This makes it available in all templates as 
	 *
	 * @param {string} cssPath - The relative path to the CSS file (from /public directory)
	 * @returns {Promise&amp;lt;string&gt;} - HTML string containing a &amp;lt;link&gt; tag pointing to the processed CSS
	 */&lt;/span&gt;
	eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addShortcode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;customCSS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cssPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;/**
		 * Stage 1: Local Development Short-Circuit
		 *
		 * In local development, skip all processing to speed up builds.
		 * Just return a simple &amp;lt;link&gt; tag pointing to the original file.
		 * This allows for faster iteration during development.
		 */&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isLocal&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;/**
		 * Stage 2: In-Memory Cache Check
		 *
		 * Check if we&#39;ve already processed this CSS file during this build.
		 * This handles the common case where multiple pages reference the same CSS file.
		 *
		 * The cache can contain:
		 * - A Promise: Processing is currently in progress, wait for it
		 * - An object: Processing is complete, return the cached HTML output
		 */&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssBuildCache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cached &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cssBuildCache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// If it&#39;s a promise (in progress), wait for it&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// This handles concurrent requests - multiple pages calling this shortcode&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// simultaneously for the same CSS file will all wait for the same processing&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cached &lt;span class=&quot;token keyword&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; cached&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// Otherwise return cached result immediately (already HTML string)&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;// This is the fast path for subsequent page builds&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cached&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;htmlOutput&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;/**
		 * Stage 3: Initialize Processing
		 *
		 * Set up file paths and configuration for processing.
		 */&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// Construct full paths for input/output/cache directories&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputFile &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;./public&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cssPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Source CSS file location&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;./_site&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;css&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Where processed CSS goes (build output)&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;./.cache&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;css&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Where minified CSS cache is stored&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;/**
		 * Stage 4: Get Brotli Compression Level
		 *
		 * Read compression level from environment variable or use default.
		 * This allows fine-tuning compression vs. speed trade-off per environment.
		 * Higher levels = better compression but slower processing.
		 */&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliCompressionLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
			process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BROTLI_COMPRESSION_LEVEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_BROTLI_COMPRESSION_LEVEL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Base 10 parsing&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;/**
		 * Stage 5: Create Processing Promise
		 *
		 * Wrap all processing in an async IIFE (Immediately Invoked Function Expression).
		 * This allows us to:
		 * 1. Handle concurrent requests by caching the Promise itself
		 * 2. Catch errors at the processing level
		 * 3. Return the same Promise to multiple concurrent callers
		 */&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; processingPromise &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.1: Validate Input File Exists
				 *
				 * Check if the source CSS file exists before attempting to process it.
				 * If missing, log an error and return empty string (fails gracefully).
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.2: Ensure Directories Exist
				 *
				 * Create output and cache directories if they don&#39;t exist.
				 * Uses a Set to track which directory combinations have been created
				 * during this build to avoid redundant checks and operations.
				 *
				 * recursive: true ensures parent directories are created if needed.
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dirKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;outputDirectory&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;directoriesCreated&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dirKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dir &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
						&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
							fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
						&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
					directoriesCreated&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dirKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.3: Read and Hash CSS File
				 *
				 * Read the source CSS file and generate a content-based hash.
				 * The hash is used for:
				 * - Cache busting (filename changes when content changes)
				 * - Disk cache key (identify if we&#39;ve seen this content before)
				 *
				 * SHA-256 is used for strong collision resistance.
				 * Only first 10 characters of hash are used (sufficient for cache busting).
				 *
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;utf8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; crypto
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;sha256&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Use SHA-256 algorithm&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Hash the CSS content&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;hex&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Get hexadecimal representation&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Take first 10 chars (sufficient for uniqueness)&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.4: Generate Cache Key and Path
				 *
				 * Create a unique cache key combining:
				 * - The content hash (identifies file contents)
				 * - The file path (normalized to avoid path separator issues)
				 *
				 * This allows different CSS files with same content to share cache,
				 * but also handles edge cases where paths matter.
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;[/&#92;&#92;]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cachePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.5: Minify CSS (or Load from Disk Cache)
				 *
				 * Check if we&#39;ve already minified this exact CSS content before.
				 * Disk cache persists between builds, so unchanged CSS files don&#39;t
				 * need re-minification even after restarting the build process.
				 *
				 * If cached:
				 * - Load the pre-minified CSS from disk
				 * - Skip the expensive minification step
				 *
				 * If not cached:
				 * - Run CleanCSS minification (expensive operation)
				 * - Save result to disk cache for next time
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token comment&quot;&gt;// Cache hit - load pre-minified CSS&lt;/span&gt;
					processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;utf8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token comment&quot;&gt;// Cache miss - minify and save&lt;/span&gt;
					processedCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cleanCSS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;minify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;styles&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.6: Write Processed CSS to Output Directory
				 *
				 * Write the minified CSS to the build output directory with a
				 * hash-based filename. The hash in the filename enables:
				 * - Cache busting (browser cache invalidates when content changes)
				 * - Long-term caching (files with same hash are unchanged)
				 *
				 * Filename format: original-name-hash.css
				 * Example: main-a1b2c3d4e5.css
				 *
				 * Only write if file doesn&#39;t exist to avoid redundant disk I/O
				 * (useful when multiple pages reference the same CSS).
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parsedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Parse original filename&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
					outputDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hash&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;parsedPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ext&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// name-hash.css&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.7: Brotli Compression
				 *
				 * Compress the minified CSS using Brotli algorithm.
				 * Brotli provides better compression than gzip, especially for text.
				 *
				 * The .br extension indicates Brotli-compressed files.
				 * Web servers can serve these directly to browsers that support Brotli
				 * (most modern browsers do via Accept-Encoding header).
				 *
				 * Compression happens synchronously (brotliCompressSync) because:
				 * - It&#39;s fast enough for build-time processing
				 * - Simplifies error handling
				 * - Build processes typically prefer synchronous operations
				 *
				 * Only compress if file doesn&#39;t exist (avoid redundant compression).
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;finalFilename&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.br&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Add .br extension&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliOptions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
						&lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; brotliCompressionLevel&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Compression level (0-11)&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; brotliBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;brotliCompressSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
						Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;processedCSS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Convert string to Buffer&lt;/span&gt;
						brotliOptions&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
					&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;promises&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;brotliFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; brotliBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.8: Generate Final HTML Output
				 *
				 * Create the HTML &amp;lt;link&gt; tag that will be inserted into the page.
				 * The path is relative to the site root and uses forward slashes
				 * (normalized for web URLs, works on all platforms).
				 *
				 * Note: The HTML references the .br file, assuming the web server
				 * can serve Brotli-compressed files when the browser supports it.
				 * If your server doesn&#39;t handle .br files, you may need to modify
				 * this to point to the uncompressed file or configure your server.
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hashedPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; brotliFilename
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;./_site&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Remove build directory prefix&lt;/span&gt;
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;&#92;&#92;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Normalize path separators for web URLs&lt;/span&gt;

				&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;hashedPath&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Stage 5.9: Cache Result in Memory
				 *
				 * Store the processing result in the in-memory cache for subsequent
				 * calls during this build. This includes:
				 * - hash: For reference/debugging
				 * - processedCSS: In case we need the CSS string later
				 * - htmlOutput: The final HTML tag (what we return)
				 *
				 * Replace the Promise in cache with the actual result object.
				 */&lt;/span&gt;
				cssBuildCache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; hash&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processedCSS&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;htmlOutput&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; result &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				&lt;span class=&quot;token comment&quot;&gt;/**
				 * Error Handling
				 *
				 * Catch any errors during processing and fail gracefully.
				 * Return empty string to prevent one CSS file error from breaking the entire build.
				 */&lt;/span&gt;
				&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;/**
		 * Stage 6: Handle Concurrent Requests
		 *
		 * Store the processing Promise in the cache BEFORE awaiting it.
		 * This ensures that if multiple pages call this shortcode simultaneously
		 * for the same CSS file, they all get the same Promise and wait for
		 * the same processing to complete (no duplicate work).
		 *
		 * Once processing completes, the Promise in cache is replaced with
		 * the result object (see Stage 5.9).
		 */&lt;/span&gt;
		cssBuildCache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cssPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; processingPromise&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; processingPromise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s all a little clumsy to have the code only in the blog post, so I&#39;ve created a &lt;a href=&quot;https://gist.github.com/Nooshu/edfc2e382bc249e92ab238779357c93e&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;gist on GitHub here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; for easier reading &amp; copying and pasting (should you wish to use or modify it yourself).&lt;/p&gt;&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;&lt;p&gt;So how would you use this code in 11ty? Let&#39;s go through each of the files you&#39;d need to modify.&lt;/p&gt;&lt;h3 id=&quot;eleventy-config-js&quot;&gt;eleventy.config.js&lt;/h3&gt;&lt;p&gt;Just your standard ESM 11ty config file with an import and the execution.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Custom CSS manipulation&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; manipulateCSS &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;./_helpers/css-manipulation.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// All your other 11ty config above...&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// execute the CSS manipulation&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;manipulateCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// All your other 11ty config below...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;env&quot;&gt;.env&lt;/h3&gt;&lt;p&gt;It sits in the 11ty root directory and is added to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;ELEVENTY_ENV=development&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;html-template&quot;&gt;HTML template&lt;/h3&gt;&lt;p&gt;I, personally, have my HTML &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; separate and sitting in its own Nunjucks partial, but it will work with any template setup really. This Shortcode is using Nunjucks because that&#39;s what I use on this blog. Spaces added in the Nunjucks to stop 11ty just printing the output. The &lt;code&gt;index.css&lt;/code&gt; is my one and only CSS file I use on this blog, and it is passed into the Shortcode. It is either used unmodified (in development), or manipulated (in production).&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	{ % customCSS &quot;/css/index.css&quot; % }
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;output&quot;&gt;Output&lt;/h3&gt;&lt;p&gt;For my development environment the HTML output is:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/css/index.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&#39;s no fingerprinting until it is built on production (in my case Cloudflare Pages). Where the output looks like this:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/css/index-b9fcfe85ef.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you can see the &lt;code&gt;index.css&lt;/code&gt; file that contains all the CSS for this site, now has a unique 10 character hash suffix. If I were to change a single byte of data in the CSS file it will generate an entirely new hash. For those wondering, a sha256 hash using only the first 10 characters still has 1,099,511,627,776 unique combinations, so the likelihood of a collision is pretty slim!&lt;/p&gt;&lt;p&gt;Since the name of my CSS file is now guaranteed to have a unique name I can use the following response headers for my CSS:&lt;/p&gt;&lt;h3 id=&quot;headers-file&quot;&gt;_headers file&lt;/h3&gt;&lt;p&gt;We can now start to use &lt;code&gt;Cache-Control&lt;/code&gt; directives that maximise the time that the CSS is stored in a user&#39;s browser cache.&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;/[css]/*
Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we are telling the user&#39;s browser that:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;public&lt;/strong&gt;: This response can be cached by any cache, it isn&#39;t restricted to only a private cache (like a user&#39;s browser cache). Examples of other caches are CDNs, and proxies.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;max-age=31536000&lt;/strong&gt;: 31536000 seconds is the number of seconds in a year. So, this directive tells caches that they can store and reuse this response for up to a year without the need for revalidation.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;immutable&lt;/strong&gt;: This is a hint to the browser or cache system that the content will never change for the lifetime of the cache. Even if the user reloads the page, the browser won&#39;t bother checking with the server for updates.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;There&#39;s a lot involved in caching on the web, so if you want to know a lot more about this fascinating topic, check out &lt;a href=&quot;https://csswizardry.com/2019/03/cache-control-for-civilians/&quot;&gt;Cache-Control for Civilians by Harry Roberts&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;For those wondering about what happens to the old &quot;orphaned&quot; files in the cache, in this instance:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The old file version will stay in the cache for up to 1 year (until the cache duration expires).&lt;/li&gt;&lt;li&gt;But it&#39;s more than likely that the browser will clean up the cache before the year expiry time to free up storage space. This is more likely on devices with limited resources like older phones, since devices with limited resources will need to be more aggressive with their resource management.&lt;/li&gt;&lt;li&gt;In either case, this isn&#39;t anything a user has to worry about. The browsers will take care of this automatically, although a user can manually clear their browser cache should they wish too.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;There we go, another 11ty blog post! We&#39;ve reviewed my current CSS setup, which works well for now, but as mentioned earlier, feel free to suggest improvements or point out anything that strays from the &quot;11ty way&quot;. As, the path to true enlightenment starts with uncovering the unseen gaps in our (my) understanding. I read that last sentence in Master Yoda&#39;s voice! As always, thanks for taking the time to read the post. I hope you found it useful and informative, and if you have any comments or feedback, please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;12/01/25: Initial post published.&lt;/li&gt;&lt;li&gt;02/11/25: Updated post and code after shortcode was found to run for every page generation in the build process. This was inefficient and is now limited to a single run per build.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Cloudflares Mirage 2.0 broke my images on all mobile devices</title>
    <link href="https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/" />
    <updated>2025-01-07T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/</id>
    <content type="html">&lt;p&gt;I&#39;m writing this (hopefully short) blog post to warn others about the situation I found myself in the other day. As I&#39;ve mentioned in my previous posts, I recently migrated everything from Jekyll and GitHub Pages to &lt;a href=&quot;https://nooshu.com/blog/2024/12/22/migrating-to-11ty/&quot;&gt;11ty and Cloudflare Pages&lt;/a&gt;. This was quite a significant migration that took a while to complete! When I started the migration, I was on &lt;a href=&quot;https://www.cloudflare.com/en-gb/plans/free/&quot;&gt;Cloudflare&#39;s Free plan&lt;/a&gt;. As I have been for several years. This is important, and I will revisit this point later!&lt;/p&gt;&lt;p&gt;Once the initial migration was completed, I tested the new code, design, and functionality on every device I could get my hands on, just to make sure there weren&#39;t any obvious bugs I&#39;d missed. In doing so, I actually found a cross-browser layout bug where the latest Firefox and latest Chrome differed. I was quite surprised by this, but it shows you should always test on different browsers! Especially ones with differing rendering engines! Anyway, I&#39;m rambling off-topic again!&lt;/p&gt;&lt;p&gt;By this point, I thought everything was looking pretty stable. I had the basics in place, the site was performing well, and accessible (according to Lighthouse anyway!), I&#39;d locked it down in terms of &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/&quot;&gt;security&lt;/a&gt; (another important point!).&lt;/p&gt;&lt;h2 id=&quot;new-year-new-cloudflare-plan&quot;&gt;New year, New Cloudflare Plan&lt;/h2&gt;&lt;p&gt;Unknown to me, I&#39;d set up an unfortunate chain of decisions that would lead to my images being broken on all mobile devices for exactly 3 weeks without me knowing (I know, a bit dramatic, and a very 1st world problem!).&lt;/p&gt;&lt;p&gt;In December, I&#39;d decided to pay annually for the Pro Cloudflare plan for 2025. Unfortunately for me, what they don&#39;t mention when this happens is that certain &quot;Pro-only&quot; functionality is enabled with this upgrade process. One of those features happens to be a performance optimisation called &lt;a href=&quot;https://blog.cloudflare.com/mirage2-solving-mobile-speed/&quot;&gt;Mirage 2.0&lt;/a&gt;. I do love the image that they have used in that blog post, and its clever play on words.&lt;/p&gt;&lt;h2 id=&quot;mirage-2-0&quot;&gt;Mirage 2.0&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://blog.cloudflare.com/mirage2-solving-mobile-speed/&quot;&gt;Mirage 2.0&lt;/a&gt; is Cloudflare&#39;s latest version of the image optimisation library. Mirage 1.0 was &lt;a href=&quot;https://blog.cloudflare.com/introducing-mirage-intelligent-image-loading/&quot;&gt;released in June 2012&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Cloudflare&#39;s Mirage 2.0 is a web performance optimisation tool focused on enhancing image delivery for &lt;strong&gt;mobile users&lt;/strong&gt; by reducing page load times and bandwidth usage. It uses techniques like lazy loading and adaptive image resizing to serve images tailored to users&#39; devices and network conditions, ensuring faster load times even on slow connections. Mirage dynamically prioritises image loading, starting with low-resolution placeholders and upgrading to higher-quality versions as needed, optimising user experience while conserving data. Integrated into Cloudflare&#39;s CDN, Mirage 2.0 provides seamless optimisation &lt;strong&gt;without extra effort from website operators&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;The emphasis in the above paragraph is mine. Just to be clear, I had no idea Mirage 2.0 even existed until the day all my images were randomly broken on mobile devices!&lt;/p&gt;&lt;h2 id=&quot;event-timeline&quot;&gt;Event timeline&lt;/h2&gt;&lt;p&gt;So what exactly happened to get to this point? Well, I:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Migrated from Jekyll and GitHub Pages to &lt;a href=&quot;https://nooshu.com/blog/2024/12/22/migrating-to-11ty/&quot;&gt;11ty and Cloudflare Pages&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Completed the site migration, optimised web performance, accessibility, and &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/&quot;&gt;security&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Tested my shiny new site in numerous browsers (including mobile browsers).&lt;/li&gt;&lt;li&gt;Upgraded to the Cloudflare Pro Plan.&lt;/li&gt;&lt;li&gt;Mistakenly (maybe?), didn&#39;t retest the site across browsers after the upgrade (since I assumed nothing would change regarding the platform and environment!).&lt;/li&gt;&lt;li&gt;Made sure site still rendered perfectly on Desktop (yay!), and discovered the Pro plan even allowed me to &lt;a href=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/&quot;&gt;enable Brotli compression for all assets&lt;/a&gt;! Winning!&lt;/li&gt;&lt;li&gt;Was then informed by &lt;a href=&quot;https://matteosonoio.it/&quot;&gt;Matteo Contrini&lt;/a&gt; that my images were broken on mobile off the back of &lt;a href=&quot;https://bsky.app/profile/matteosonoio.it/post/3lezdlpqlwc2l&quot;&gt;announcing my new blog post on Bluesky&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Panicked, confusion… argh!&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;It&#39;s point 8 where the self-doubt creeps in:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&quot;Have the images ever worked on the site??&quot;&lt;/li&gt;&lt;li&gt;&quot;I&#39;m 100% sure I tested on mobile!&quot;&lt;/li&gt;&lt;li&gt;&quot;Surely someone would have mentioned this to me if they&#39;d never worked??&quot;&lt;/li&gt;&lt;li&gt;&quot;Why does it work on desktop but not on mobile??&quot;&lt;/li&gt;&lt;li&gt;&quot;Why do the images work when I request the desktop site &lt;strong&gt;on mobile!!&lt;/strong&gt;&quot;&lt;/li&gt;&lt;li&gt;&quot;Is it the new &lt;code&gt;eleventyImageTransformPlugin&lt;/code&gt;? If so, surely, it would have been spotted and fixed already!&quot;&lt;/li&gt;&lt;li&gt;&quot;How on earth am I going to debug and resolve this issue?&quot;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That list of quotes above was basically my thought process for a couple of hours after the image problem was reported!&lt;/p&gt;&lt;h2 id=&quot;web-inspector-for-ios&quot;&gt;Web Inspector for iOS&lt;/h2&gt;&lt;p&gt;Thankfully, Matteo jumped in and helped with the final quote on the list above when he pointed me in the direction of the fantastic &lt;a href=&quot;https://apps.apple.com/us/app/web-inspector/id1584825745&quot;&gt;Web Inspector&lt;/a&gt; App for iOS. Once installed and enabled as a Safari extension, it allows you to easily debug web pages on iOS Safari. It comes with tabs for the:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;DOM&lt;/li&gt;&lt;li&gt;Elements&lt;/li&gt;&lt;li&gt;Console&lt;/li&gt;&lt;li&gt;Network&lt;/li&gt;&lt;li&gt;Resources&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;Screenshot from my mobile showing the really handy Web Inspector App on show.&quot; decoding=&quot;async&quot; height=&quot;1036&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/csRSYNzj-x-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/csRSYNzj-x-300.webp 300w, https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/csRSYNzj-x-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;In fact, if you look at the code seen in the inspector in the image, you will see how I finally worked out what was going on!&lt;/p&gt;&lt;h2 id=&quot;script-injection&quot;&gt;Script Injection&lt;/h2&gt;&lt;p&gt;On upgrading to Cloudflare Pro, unbeknown to me, the Mirage functionality is automatically enabled within the Cloudflare dashboard! So every image on every page has the mirage 2.0 script injected into the DOM (but only on mobile! Remember my added emphasis in the &lt;a href=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/#mirage-2-0&quot;&gt;Mirage 2.0 section&lt;/a&gt; earlier). Also remember how I&#39;d locked down my blog&#39;s security using &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/&quot;&gt;HTTP response headers&lt;/a&gt;. Which just so happened to include a Content Security Policy (CSP)!&lt;/p&gt;&lt;h2 id=&quot;script-injection-csp-equals-pain&quot;&gt;Script Injection + CSP equals pain&lt;/h2&gt;&lt;p&gt;I think most readers may realise where I&#39;m going with this now! On the one hand, it&#39;s fantastic to know that the CSP did its job and blocked the 3rd party script from &lt;code&gt;ajax.cloudflare.com&lt;/code&gt;! I mean, that&#39;s what it&#39;s there for, after all! But what&#39;s not so great is the fact that this injection &lt;strong&gt;only&lt;/strong&gt; happens on a mobile device where there are no console errors to be seen (by default anyway). It&#39;s also worth mentioning that at this point it was still a complete guess that this was the issue, as even with the Web Inspector Extension installed. There was no usual CSP error (&lt;code&gt;Content Security Policy: The page&#39;s settings blocked the loading of a resource at...&lt;/code&gt;) in the console, like you get in a desktop browser! So I was just hoping that this was the issue!&lt;/p&gt;&lt;h2 id=&quot;disabling-mirage&quot;&gt;Disabling Mirage&lt;/h2&gt;&lt;p&gt;So, the easiest way to resolve the issue is to disable Mirage 2.0 from the Cloudflare Dashboard, easier said than done at times! Cloudflare I adore your platform and the features you offer, but your dashboard navigation is just nuts! Please consider a major overhaul of the whole UI! Furthermore, don&#39;t hesitate to put me down as a beta tester as well!&lt;/p&gt;&lt;p&gt;So to disable Mirage, first select the domain you want to change, then look for the &quot;Speed&quot; dropdown on the left, then click the &quot;Optimization&quot; link:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The left navigation in the Cloudflare dashboard with the Optimization link highlighted.&quot; decoding=&quot;async&quot; height=&quot;296&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/0PFCqcGmF1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/0PFCqcGmF1-300.webp 300w, https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/0PFCqcGmF1-512.webp 512w&quot; width=&quot;512&quot;&gt;&lt;/p&gt;&lt;p&gt;Next, look for the &quot;Image Optimization&quot; tab on the page:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The &amp;#34;Image Optimization&amp;#34; tab highlighted within the Speed / Optimization menu.&quot; decoding=&quot;async&quot; height=&quot;190&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/QYEHB_a-Qk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/QYEHB_a-Qk-300.webp 300w, https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/QYEHB_a-Qk-600.webp 600w, https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/QYEHB_a-Qk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Lastly, scroll to the bottom of this tab, and you will find the Mirage setting you can toggle on and off:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Mirage setting you can toggle on and off.&quot; decoding=&quot;async&quot; height=&quot;258&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/RtGAI4pJum-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/RtGAI4pJum-300.webp 300w, https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/RtGAI4pJum-600.webp 600w, https://nooshu.com/blog/2025/01/07/cloudflares-mirage-2-0-broke-my-images-on-all-mobile-devices/RtGAI4pJum-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Unsurprisingly, the setting is now turned off on my dashboard! Also note the &quot;Beta&quot; tag in orange next to the title…&lt;/p&gt;&lt;h2 id=&quot;what-have-we-learned&quot;&gt;What have we learned?&lt;/h2&gt;&lt;p&gt;So, what have we learned from my stressful little adventure?&lt;/p&gt;&lt;ol&gt;&lt;li&gt;CSP&#39;s actually work really well for blocking third-party JavaScript code injection!&lt;/li&gt;&lt;li&gt;Be sure to check your website often, especially if you happen to change your Cloudflare subscription plan.&lt;/li&gt;&lt;li&gt;Never fully trust anyone, not even your CDN or hosting provider! Even they may choose to meddle with your site&#39;s code!&lt;/li&gt;&lt;li&gt;The &lt;a href=&quot;https://apps.apple.com/gb/app/web-inspector/id1584825745&quot;&gt;Web Inspector App&lt;/a&gt; for iOS is really useful for debugging mobile Safari issues.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;steps-cloudflare-can-take-to-prevent-this&quot;&gt;Steps Cloudflare can take to prevent this&lt;/h2&gt;&lt;p&gt;So, I&#39;m pretty sure this isn&#39;t my fault, right?? I&#39;d like to think I&#39;ve just been unlucky with having a particular setup and a unique set of circumstances. Saying that, I&#39;m certain that Cloudflare can introduce &lt;em&gt;some&lt;/em&gt; changes to stop this from happening to others! My recommendations would be:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Ensure that Mirage 2.0 is &lt;strong&gt;Opt-in&lt;/strong&gt; only and not Opt-out. Especially for a feature that is still in Beta!&lt;/li&gt;&lt;li&gt;When a user upgrades their plan, consider emailing them, or pointing out the new features that are available to them with the new plan (maybe within their dashboard?)&lt;/li&gt;&lt;li&gt;Consider some sort of automation or logging to check to see if Mirage (or other functionality) is causing, or could cause issues if the site has a CSP in place.&lt;/li&gt;&lt;li&gt;Don&#39;t assume that your paying customers know everything about every product on your platform.&lt;/li&gt;&lt;li&gt;Finally, it&#39;s also worth mentioning to users that there have been similar image issues caused by Mirage in the past. Documented &lt;a href=&quot;https://github.com/vercel/next.js/issues/23774&quot;&gt;here&lt;/a&gt; and &lt;a href=&quot;https://github.com/vercel/next.js/issues/28810&quot;&gt;here&lt;/a&gt;. Although, admittedly, these are related to Vercel and Next.js, so a fairly different setup!&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;I hope that this blog post isn&#39;t coming across too harshly. As I&#39;ve said before, I really love Cloudflare as a company, and the services they provide. It&#39;s just when they overstep the mark and make assumptions about the websites they are hosting!&lt;/p&gt;&lt;p&gt;One final point, I briefly chatting to &lt;a href=&quot;https://bsky.app/profile/andydavies.me&quot;&gt;Andy Davies&lt;/a&gt; from &lt;a href=&quot;https://bsky.app/profile/speedcurve.com&quot;&gt;Speedcurve&lt;/a&gt; on Bluesky. He mentioned they (Speedcurve) recommend all clients that use Cloudflare disable both &lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/images/mirage/&quot;&gt;Mirage&lt;/a&gt; AND &lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/rocket-loader/&quot;&gt;Rocket Loader&lt;/a&gt; — &lt;a href=&quot;https://bsky.app/profile/andydavies.me/post/3lf2swztmek2g&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;If you made it to the end of this post, congratulations—you deserve a medal! Meanwhile, I&#39;ve learned I couldn&#39;t write a short blog post if my life depended on it!&lt;/p&gt;&lt;p&gt;Nevertheless, I hope you found it a useful read, and it helps someone who stumbles across this completely unrelated set of circumstances! As always, thanks for reading, and if you have any feedback or comments, please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;07/01/25: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Cranking Brotli up to 11 with Cloudflare Pro and 11ty</title>
    <link href="https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/" />
    <updated>2025-01-05T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/</id>
    <content type="html">&lt;p&gt;As with the past few blog posts I&#39;ve written, this post is about my migration from GitHub Pages and Jekyll to Cloudflare Pages and 11ty. Once the migration was completed, I decided to have a look at how I could optimise the web performance of my blog. As with all web performance projects I start, I begin with the basics. For me, that&#39;s the 3 C&#39;s:&lt;/p&gt;&lt;p&gt;For assets being delivered to your user, make sure they are properly:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;ached&lt;/li&gt;&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;oncatenated&lt;/li&gt;&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;ompressed&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;before&quot;&gt;Before&lt;/h2&gt;&lt;p&gt;Upon looking at the DevTools Network Panel in Firefox (my main browser of choice 😍), I could see that only my HTML document was being Brotli compressed:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Screenshot from Firefox DevTools Network panel where in the far right where &amp;#34;br&amp;#34; Content-Encoding can only be seen on the HTML document. All other assets only show &amp;#34;gzip&amp;#34; compression.&quot; decoding=&quot;async&quot; height=&quot;181&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/_Kedh6lLd1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/_Kedh6lLd1-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/_Kedh6lLd1-600.webp 600w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/_Kedh6lLd1-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Before I get into the details, let&#39;s discuss what the difference between Gzip and Brotli is. If you already know all this, &lt;a href=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/#compression&quot;&gt;feel free to skip this section&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;what-is-brotli-compression&quot;&gt;What is Brotli compression?&lt;/h2&gt;&lt;p&gt;Brotli is a &quot;new&quot; compression algorithm that was released by &lt;a href=&quot;https://opensource.googleblog.com/2015/09/introducing-brotli-new-compression.html&quot;&gt;Google in September 2015&lt;/a&gt; (hence the quotes around the new). Brotli came with the following key features:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Lossless Compression — No data is lost during compression / decompression.&lt;/li&gt;&lt;li&gt;High Compression Ratios — It achieves higher compression ratios compared to other popular compression algorithms used on the web, like &lt;a href=&quot;https://www.gnu.org/software/gzip/&quot;&gt;gzip&lt;/a&gt;, and &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc1951&quot;&gt;deflate&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Optimal for Web Performance — Brotli is specifically optimised for HTTP compression, its predefined static dictionary is optimised for patterns often found in web content. It&#39;s high compression ratios, and fast decompression, make it a perfect choice for HTTP compression.&lt;/li&gt;&lt;li&gt;Static and Dynamic Compression — Brotli supports both dynamic (real-time) and static (pre-compressed) data compression, offering a balance between server CPU usage and optimal file size. Dynamic compression suits real-time needs, while static compression, using the highest setting (11), maximises asset compression during the build process. Pre-compressed assets can then be delivered with minimal CPU overhead.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&#39;s these key features among many others that have made Brotli one of the preferred compression algorithms on the modern web. According to Can I Use, as of today, &lt;a href=&quot;https://caniuse.com/brotli&quot;&gt;97.79% of browsers on the web support Brotli Accept-Encoding/Content-Encoding&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I, personally, love that Brotli has compression settings ranging from 0 to 11, where 0 is the fastest compression speed but the lowest compression ratio, and 11 has the highest compression ratio but requires more computational resources and time. I&#39;d love to know if the Google developers got the idea for a compression level of 11 from the &lt;a href=&quot;https://youtu.be/uMSV4OteqBE?si=lwgRpgsfKOSXwHbB&quot;&gt;very famous scene&lt;/a&gt; from the film 1984 film &quot;This Is Spinal Tap&quot;.&lt;/p&gt;&lt;p&gt;If you are interested in learning more about Brotli compression, here&#39;s some further reading for you:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7932&quot;&gt;RFC 7932&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/google/brotli&quot;&gt;Google&#39;s Brotli Repository&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://dev.to/kinsta/brotli-the-new-era-of-data-compression-41kb&quot;&gt;Brotli: The New Era of Data Compression on Dev.to&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://hacks.mozilla.org/2015/11/better-than-gzip-compression-with-brotli/&quot;&gt;Better than Gzip Compression with Brotli&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://kinsta.com/blog/brotli-compression/&quot;&gt;Brotli Compression: A Fast Alternative to GZIP Compression from Kinsta&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;brotli-and-cloudflare&quot;&gt;Brotli and Cloudflare&lt;/h2&gt;&lt;p&gt;Cloudflare has been a strong advocate for Brotli for many years, introducing support for it on September 21, 2016. Remarkably, this was just over a year after Google open-sourced Brotli on September 22, 2015, and only a few months after its formal standardisation in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7932&quot;&gt;RFC 7932&lt;/a&gt; in July 2016. Demonstrating impressive speed, Cloudflare rolled out Brotli support in just over two months, significantly ahead of one of their main competitors, Akamai, which implemented support on March 19, 2018—almost 18 months later.&lt;/p&gt;&lt;p&gt;At first, they were very explicit with their support, offering their users the option to toggle it on and off via the &quot;Optimization&quot; menu in their site dashboard:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Brotli support enabled in the Optimization menu in the Cloudflare dashboard.&quot; decoding=&quot;async&quot; height=&quot;514&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/LjR2QGKu5e-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/LjR2QGKu5e-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/LjR2QGKu5e-600.webp 600w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/LjR2QGKu5e-800.webp 800w&quot; width=&quot;800&quot;&gt;&lt;/p&gt;&lt;p&gt;Since then, Brotli has become so ubiquitous for web performance on the web that this toggle was removed from the dashboard in May 2024, as announced on their &lt;a href=&quot;https://community.cloudflare.com/t/important-brotli-on-by-default-and-brotli-toggle-removal/652757&quot;&gt;community forum here&lt;/a&gt;. It is now enabled by default for the following &lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/brotli/content-compression/#compression-between-cloudflare-and-website-visitors&quot;&gt;file types&lt;/a&gt;. Unfortunately, when moving from beta to live, Cloudflare decided that Brotli compression wouldn&#39;t be the default compression used on the &lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/compression/#between-visitors-and-cloudflare&quot;&gt;free plan&lt;/a&gt;. Although, saying that, the free plan is still incredible for web performance! This sadly means that if you want all your website assets dynamically compressed (on the fly), by Cloudflare, then you will need to be on the &lt;a href=&quot;https://www.cloudflare.com/en-gb/plans/pro/&quot;&gt;Pro plan&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Another point to know about the Pro plan and Brotli compression is that it uses dynamic &quot;on the fly&quot; compression. It therefore uses a reduced compression setting. A happy medium between compression size and CPU usage happens to be around the 3 or 4 setting (out of 11). Cloudflare has chosen 4 to be the setting they use for dynamic compression. Depending on the content, Brotli&#39;s compression ratio at level 4 is approximately 2x to 4x, which is considered balanced compression. This level provides a good balance of speed and compression.&lt;/p&gt;&lt;p&gt;But since we are controlling the compression level on our static blog, let&#39;s crank it all the way up to 11! This is defined as maximum compression, and offers a compression ratio of approximately 4x to 7x smaller than the original file size. The use case for this level of compression is it&#39;s often used for static assets that don&#39;t change regularly, such as font files or infrequently updated files. In my case, once written, I don&#39;t plan to update the minimal amount of JavaScript in use on my blog. Hence, the JavaScript files are the perfect candidate for extreme compression. So let&#39;s get started.&lt;/p&gt;&lt;h2 id=&quot;compression&quot;&gt;Compression&lt;/h2&gt;&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;&lt;p&gt;The simplest way to compress your assets is by using a command-line tool in the terminal. To get started, you&#39;ll first need to install the Brotli Command-line Interface (CLI). On macOS, you can do this by either:&lt;/p&gt;&lt;h4 id=&quot;using-homebrew&quot;&gt;Using Homebrew&lt;/h4&gt;&lt;p&gt;You may or may not have Homebrew already installed. Here I assume you haven&#39;t. To install it, you&#39;d run the following commands in the terminal.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Install&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;/bin/bash &lt;span class=&quot;token variable parameter&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Ensure Homebrew is up-to-date&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brew update&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Install Brotli&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; brotli&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Verify Installation&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brotli &lt;span class=&quot;token variable parameter&quot;&gt;--version&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will return the installed Brotli version. You are now ready to compress your assets using Brotli.&lt;/p&gt;&lt;h4 id=&quot;using-macports&quot;&gt;Using MacPorts&lt;/h4&gt;&lt;p&gt;First, follow the MacPorts &lt;a href=&quot;https://www.macports.org/install.php&quot;&gt;installation here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Then update and install&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; port selfupdate
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; port &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; brotli&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Verify Installation&lt;/strong&gt;:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brotli &lt;span class=&quot;token variable parameter&quot;&gt;--version&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These are just 2 ways of installing Brotli on OSX, others include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Build from source (using &lt;code&gt;Xcode&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;Install via Python&#39;s pip.&lt;/li&gt;&lt;li&gt;Download Precompiled Binaries (via the &lt;a href=&quot;https://github.com/google/brotli/releases&quot;&gt;Brotli releases page&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;Use Docker.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Each method has its benefits depending on your environment and preferences. Once installed, the usage is the same for all methods listed above.&lt;/p&gt;&lt;h3 id=&quot;usage&quot;&gt;Usage&lt;/h3&gt;&lt;p&gt;To compress a file, it&#39;s as simple as:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brotli &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;filename-here&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or if you want to control the compression settings:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brotli &lt;span class=&quot;token variable parameter&quot;&gt;--quality&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;-11&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;filename-here&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will output a Brotli version of the file so &lt;code&gt;file.txt&lt;/code&gt; will be duplicated, and a new file called &lt;code&gt;file.txt.br&lt;/code&gt; will be created. A shortcut for &lt;code&gt;--quality=11&lt;/code&gt; is &lt;code&gt;--best&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;So these commands would produce the same output:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brotli &lt;span class=&quot;token variable parameter&quot;&gt;--quality&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt; file.txt
brotli &lt;span class=&quot;token variable parameter&quot;&gt;--best&lt;/span&gt; file.txt&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For lots more information on the command-line options available, use:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;brotli &lt;span class=&quot;token variable parameter&quot;&gt;-h&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;compression-script-a-single-file&quot;&gt;Compression script — a single file&lt;/h3&gt;&lt;p&gt;Now you can either do this manually, which is long and laborious, or we can semi-automate the task by writing a shell script (Thanks to &lt;a href=&quot;https://hachyderm.io/@real_ozcoder@webperf.social&quot;&gt;ozcoder&lt;/a&gt; for the idea, and initial code!).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;compress.sh&lt;/strong&gt;&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token important shebang&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Check if the user provided a file name&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Usage: &lt;span class=&quot;token variable&quot;&gt;$0&lt;/span&gt; &amp;lt;filename&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Get the file name from the first argument&lt;/span&gt;
&lt;span class=&quot;token variable assign-left&quot;&gt;fname&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Compress the file using Brotli at quality 11&lt;/span&gt;
brotli &lt;span class=&quot;token variable parameter&quot;&gt;--quality&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;--output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${fname}&lt;/span&gt;.br&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${fname}&lt;/span&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Notify the user&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-eq&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;File compressed successfully: &lt;span class=&quot;token variable&quot;&gt;${fname}&lt;/span&gt;.br&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Error occurred during compression.&quot;&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Remember to make the script executable using &lt;code&gt;chmod +x compress.sh&lt;/code&gt;. Then you can use it like so:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;./compress.sh filename.js&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A Brotli compressed file (&lt;code&gt;filename.js.br&lt;/code&gt;) will now be located in the same directory.&lt;/p&gt;&lt;h3 id=&quot;compression-script-whole-directory&quot;&gt;Compression script — whole directory&lt;/h3&gt;&lt;p&gt;If you find manually compressing each file using the &lt;code&gt;compress.sh&lt;/code&gt; script above tedious, then we can modify the shell script to compress all JavaScript assets in a single directory (&lt;code&gt;compress-directory.sh&lt;/code&gt;):&lt;/p&gt;&lt;p&gt;&lt;strong&gt;compress-directory.sh&lt;/strong&gt;&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token important shebang&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Check if the user provided a directory&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Usage: &lt;span class=&quot;token variable&quot;&gt;$0&lt;/span&gt; &amp;lt;directory&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Get the directory from the first argument&lt;/span&gt;
&lt;span class=&quot;token variable assign-left&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Check if the specified directory exists&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$directory&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Error: Directory &#39;&lt;span class=&quot;token variable&quot;&gt;$directory&lt;/span&gt;&#39; does not exist.&quot;&lt;/span&gt;
	&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Compress all .js files in the specified directory (modify for other file types)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable for-or-select&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$directory&lt;/span&gt;&quot;&lt;/span&gt;/*.js&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;# Check if there are .js files in the directory&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$file&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
		&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;No .js files found in &#39;&lt;span class=&quot;token variable&quot;&gt;$directory&lt;/span&gt;&#39;.&quot;&lt;/span&gt;
		&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;# Compress the file using Brotli&lt;/span&gt;
	brotli &lt;span class=&quot;token variable parameter&quot;&gt;--quality&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;--output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${file}&lt;/span&gt;.br&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$file&lt;/span&gt;&quot;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;# Notify the user of success or failure&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-eq&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
		&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Compressed: &lt;span class=&quot;token variable&quot;&gt;$file&lt;/span&gt; -&gt; &lt;span class=&quot;token variable&quot;&gt;${file}&lt;/span&gt;.br&quot;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
		&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Error compressing: &lt;span class=&quot;token variable&quot;&gt;$file&lt;/span&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;

&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Compression complete.&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once either of these scripts have completed you will have one or more Brotli encoded files e.g. &lt;code&gt;filename.js&lt;/code&gt; AND &lt;code&gt;filename.js.br&lt;/code&gt;. You can now delete the original JavaScript file and remove the &lt;code&gt;.br&lt;/code&gt; extension from the newly compressed file. Assuming you are using Git (which, of course, everyone does, right?), you can always retrieve the original JS file from the Git history should you need the uncompressed version again, or you can use the &lt;code&gt;brotli --decompress filename.js.br&lt;/code&gt; command. Remember that Brotli is a Lossless compression algorithm, there&#39;s no data loss during compression and decompression, so you can always decompress to get the original file back.&lt;/p&gt;&lt;h2 id=&quot;my-scripts-are-broken-on-localhost&quot;&gt;My scripts are broken on localhost&lt;/h2&gt;&lt;p&gt;&lt;img alt=&quot;Checking in you browser console will show a number of angre looking errors like &amp;#34;Uncaught SyntaxError: illegal character U+FFFD&amp;#34;.&quot; decoding=&quot;async&quot; height=&quot;67&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/7oSnbXoKAl-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/7oSnbXoKAl-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/7oSnbXoKAl-600.webp 600w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/7oSnbXoKAl-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So now that your JavaScript has been Brotli compressed, it&#39;s no longer in a plaintext format. Brotli compressed files are actually in a binary format, so they aren&#39;t directly editable in your code editor. These errors are in the console because presently, the local browser is expecting to receive the JavaScript in plaintext, so when it doesn&#39;t, it will let you know via the console with an error that says something like &lt;code&gt;Uncaught SyntaxError: illegal character U+FFFD&lt;/code&gt;. Now, this is a scary looking error, but there&#39;s an easy fix. We need to tell our browser that these JavaScript files are no longer plaintext, they are Brotli compressed. To achieve that, we are going to modify our 11ty config file (e.g. &lt;code&gt;eleventy.config.js&lt;/code&gt;) and add in some node middleware that our local 11ty development server will use.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// The rest of your eleventy.config.js code above...&lt;/span&gt;
eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setServerOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token literal-property property&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Access-Control-Allow-Origin&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;*&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Content-Encoding&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;br&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/javascript&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Vary&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Accept-Encoding&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// The rest of your eleventy.config.js code below...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The code above will hopefully be fairly explanatory. It&#39;s basically saying when you serve a JavaScript file from localhost, make sure to include the above response headers. The critical one is the &lt;code&gt;&#39;Content-Encoding&#39;, &#39;br&#39;&lt;/code&gt; response header, which tells the browser that the files are now Brotli compressed. It then recognises that the response body has been compressed using the Brotli algorithm. The browser will then decompress the Brotli-compressed data before interpreting or rendering the content as it would do with a plaintext version.&lt;/p&gt;&lt;h2 id=&quot;cloudflare-compression-rules-setup-pro-account&quot;&gt;Cloudflare Compression rules setup (Pro account)&lt;/h2&gt;&lt;p&gt;Now that the development environment has been fixed, it&#39;s time to dive into the &lt;a href=&quot;https://dash.cloudflare.com/&quot;&gt;Cloudflare dashboard&lt;/a&gt; and make sure we have everything setup in there!&lt;/p&gt;&lt;p&gt;If you have a pro account, you will see a &quot;Compression Rules&quot; menu item under the &quot;Rules&quot; section of the navigation.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Compression Rules option under the Rules section of the Cloudflare navigation dashboard.&quot; decoding=&quot;async&quot; height=&quot;716&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/ojvHLhJH_Q-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/ojvHLhJH_Q-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/ojvHLhJH_Q-508.webp 508w&quot; width=&quot;508&quot;&gt;&lt;/p&gt;&lt;p&gt;Once inside this menu, you will see an option to create a new compression rule:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;There&#39;s a &amp;#34;Create rule&amp;#34; button. I&#39;ve already created a &amp;#34;Brotli Compression&amp;#34; rule in the image.&quot; decoding=&quot;async&quot; height=&quot;201&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/s431O1Zt33-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/s431O1Zt33-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/s431O1Zt33-600.webp 600w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/s431O1Zt33-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You will see I&#39;ve already created a &quot;Brotli Compression&quot; rule in the image, and it is enabled.&lt;/p&gt;&lt;p&gt;Within this section, you will see a long page with 2 main sections:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An image of the compression rules available to you including &amp;#34;If incoming requests match... Default Content Types, Custom filter expression, and All incoming requests.&quot; decoding=&quot;async&quot; height=&quot;458&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/JD5rlzY4cZ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/JD5rlzY4cZ-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/JD5rlzY4cZ-600.webp 600w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/JD5rlzY4cZ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I added a name and just left the option on the &lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/compression/#compression-between-cloudflare-and-website-visitors&quot;&gt;default content types&lt;/a&gt; to be compressed.&lt;/p&gt;&lt;p&gt;Next, we need to set the compression settings we want to apply to these files being served to our users:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An image showing the compression options lower down the page. These include: Enable standard (Zstd) Compression (beta), Enable Brotli and Gzip Compression, Disable Compression, and Custom.&quot; decoding=&quot;async&quot; height=&quot;347&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/ZGT71fx4YF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/ZGT71fx4YF-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/ZGT71fx4YF-600.webp 600w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/ZGT71fx4YF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You will see in my settings I&#39;ve been very explicit in the order of compression I would like: &quot;Brotli, Zstandard, Gzip, Auto&quot;. Zstandard (Zstd) is generally considered better than Gzip in terms of performance and compression ratio for most use cases. Refer to &lt;a href=&quot;https://facebook.github.io/zstd/&quot;&gt;this page here for more information about Zstandard&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;after&quot;&gt;After&lt;/h2&gt;&lt;p&gt;Once all the settings have been updated, and we have &lt;strong&gt;cleared the CDN cache&lt;/strong&gt;! We can see the final result from the network panel below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The result of my brotli modification on my Pro Cloudflare account. All assets that are served are now Brotli compressed.&quot; decoding=&quot;async&quot; height=&quot;183&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/W_5Tl7fZLB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/W_5Tl7fZLB-300.webp 300w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/W_5Tl7fZLB-600.webp 600w, https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/W_5Tl7fZLB-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Looking at the far-right column from the network panel, we can see that all my assets are now Brotli compressed.&lt;/p&gt;&lt;p&gt;Although the panel doesn&#39;t show it, the assets are being compressed using a mixture of dynamic and static compression:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;HTML document — Dynamic (level 4) — by Cloudflare.&lt;/li&gt;&lt;li&gt;CSS file — Dynamic (level 4) — by Cloudflare.&lt;/li&gt;&lt;li&gt;JavaScript files — Static (level 11) — by me.&lt;/li&gt;&lt;li&gt;Favicon — Dynamic (level 4) — by Cloudflare.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If I wanted to, I could statically compress the &lt;code&gt;favicon.ico&lt;/code&gt; file too, since that&#39;s never going to change. But I&#39;ll leave that to you the reader to figure out how to do that, should you wish to!&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;It&#39;s a real shame that you need a Cloudflare Pro account to use Brotli compression for all static assets served by Cloudflare. But their free account does give you a tremendous number of features for web performance and security out of the box, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;HTTP/2, and HTTP/3 + QUIC.&lt;/li&gt;&lt;li&gt;0-RTT.&lt;/li&gt;&lt;li&gt;Cloudflare Workers.&lt;/li&gt;&lt;li&gt;DNSSEC.&lt;/li&gt;&lt;li&gt;Excellent cacheability of static assets.&lt;/li&gt;&lt;li&gt;Email security and forwarding.&lt;/li&gt;&lt;li&gt;Free hosting on Cloudflare Pages.&lt;/li&gt;&lt;li&gt;Content Delivery Network (CDN) with global caching.&lt;/li&gt;&lt;li&gt;Page Rules for fine-grained cache control.&lt;/li&gt;&lt;li&gt;Improved security.&lt;/li&gt;&lt;li&gt;Image optimisation.&lt;/li&gt;&lt;li&gt;Improved web performance.&lt;/li&gt;&lt;li&gt;DDoS protection.&lt;/li&gt;&lt;li&gt;Basic Web Application Firewall (WAF) rules.&lt;/li&gt;&lt;li&gt;SSL/TLS encryption (Free Universal SSL).&lt;/li&gt;&lt;li&gt;Bot mitigation.&lt;/li&gt;&lt;li&gt;IP masking via reverse proxy.&lt;/li&gt;&lt;li&gt;Free DNS hosting with fast resolution.&lt;/li&gt;&lt;li&gt;plus many other features too…&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;m genuinely interested to know if this can all be achieved on the free plan?&lt;/p&gt;&lt;p&gt;For example, by compressing your assets with Brotli (11), then setting your Pages &lt;code&gt;_headers&lt;/code&gt; file to serve the &lt;code&gt;&#39;Content-Encoding&#39;, &#39;br&#39;&lt;/code&gt; response header along with these compressed assets. If so, I probably wouldn&#39;t recommend it as it may be against one of the &lt;a href=&quot;https://www.cloudflare.com/en-gb/website-terms/&quot;&gt;Cloudflare Website and Online Services Terms of Use&lt;/a&gt;. And sounds like a quick way to get your account deleted by Cloudflare! If anyone tries this please do let me know! I really wish Cloudflare had a referral program, I&#39;d probably get my Pro account paid for in no time!&lt;/p&gt;&lt;p&gt;And there we have it, another blog post off the back of my migration to 11ty. As always, thanks for reading, and I hope you found it useful and informative! If you have any feedback or comments, please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;05/01/25: Initial post published.&lt;/li&gt;&lt;li&gt;05/01/25: Thanks to &lt;a href=&quot;https://bsky.app/profile/tunetheweb.com&quot;&gt;Barry Pollard&lt;/a&gt; for pointing out that I&#39;d broken the shell scripts by adding a comment before the shebang! Doh!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Refactoring a Web Performance Snippet for Security and Best Practice</title>
    <link href="https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/" />
    <updated>2025-01-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/</id>
    <content type="html">&lt;p&gt;In this blog post, I&#39;m going to discuss a little Web Performance Snippet that I&#39;ve seen a few WebPerf evangelists use on their websites. As you will have seen in a &lt;a href=&quot;https://nooshu.com/blog/2024/12/22/migrating-to-11ty/&quot;&gt;previous blog post&lt;/a&gt;, I&#39;ve recently overhauled my blog, both in terms of design and also static hosting. In doing so, I&#39;ve completely rewritten almost everything from my old site by either migrating it across and &quot;cleaning it up&quot;, or simply realising the feature was no longer useful and discarding it, thus reducing technical debt in order to streamline maintenance in the future.&lt;/p&gt;&lt;h2 id=&quot;security&quot;&gt;Security&lt;/h2&gt;&lt;p&gt;In my &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/&quot;&gt;last blog post&lt;/a&gt;, I touched on improving the security of this blog. And this is the reason for this post (in a very roundabout way!). For the past few years, since I started strengthening my Web Performance knowledge, I&#39;ve had a little code snippet in my footer. It simply queries the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance&quot;&gt;Performance API&lt;/a&gt; and outputs an elementary Page Load time via the use of &lt;code&gt;loadEventEnd&lt;/code&gt; property. Unfortunately, these are now deprecated features of the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming&quot;&gt;PerformanceTiming API&lt;/a&gt;. But as they still work, I&#39;ve had no reason to change it. I&#39;d love to say I came up with this idea and code myself, but I didn&#39;t. I actually copied this feature from &lt;a href=&quot;https://timkadlec.com/&quot;&gt;Tim Kadlec&lt;/a&gt; several years ago, it can still be seen in the footer of his website today (bottom-right corner of his website):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the bottom-right corner of his website, you will see the &amp;#34;This page loaded in 0.376 seconds.&amp;#34;.&quot; decoding=&quot;async&quot; height=&quot;302&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/HKIWi9T4rI-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/HKIWi9T4rI-300.webp 300w, https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/HKIWi9T4rI-600.webp 600w, https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/HKIWi9T4rI-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Examining the code that outputs this paragraph to the page, I can see it looks like this:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;performance &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
			window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;performance &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
			window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mozPerformance &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
			window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;msPerformance &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
			window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitPerformance &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; performance&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;timing &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; start &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;navigationStart&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			end &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loadEventEnd&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		loadTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;end &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; start&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; copy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.copy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		copy&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;innerHTML &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt;
			&lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;p class=&#39;loaded&#39;&gt;This page loaded in &amp;lt;strong&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
			loadTime &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
			&lt;span class=&quot;token string&quot;&gt;&quot; seconds&amp;lt;/strong&gt;.&amp;lt;/p&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And this is the code I had running for many years on my old website. I liked having this code on my website as it gave me an extremely basic statistic on how well the site loaded on any given device I happen to use (e.g. an older mobile device, or when I&#39;m using an unstable connection in a rural area of the country). It could also be used to verify there&#39;s no obvious performance problems. For example, if I see that the metric is anywhere over 500 ms (on my laptop) then that&#39;s a good indication that something isn&#39;t performing as it should be. Over 500 ms for such a simple static page on an 2020 MacBook Air (M1) would be a real worry!&lt;/p&gt;&lt;h2 id=&quot;updating-the-code&quot;&gt;Updating the code&lt;/h2&gt;&lt;p&gt;Unfortunately, there are a few issues with this code that wouldn&#39;t work with my newly updated website. The biggest of all being the use of the &lt;code&gt;setTimeout()&lt;/code&gt; which is a type of &lt;code&gt;eval()&lt;/code&gt; and is a huge security risk. You only need to look at the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval&quot;&gt;MDN Page for eval&lt;/a&gt;, and it comes with a big red warning at the top of the page:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;&amp;#34;Warning: Executing JavaScript from a string is an enormous security risk. It is far too easy for a bad actor to run arbitrary code when you use eval(). See Never use direct eval()! below.&amp;#34;&quot; decoding=&quot;async&quot; height=&quot;163&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/Y9GZY9gP2R-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/Y9GZY9gP2R-300.webp 300w, https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/Y9GZY9gP2R-600.webp 600w, https://nooshu.com/blog/2025/01/02/refactoring-a-web-performance-snippet-for-security-and-best-practice/Y9GZY9gP2R-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;They even have &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_direct_eval!&quot;&gt;a whole section&lt;/a&gt; on the page as to why you shouldn&#39;t do this.&lt;/p&gt;&lt;p&gt;For my newly built blog, I planned to update my blog to use a fairly strict Content Security Policy (CSP). And as I intended to use the &lt;code&gt;unsafe-inline&lt;/code&gt; CSP directive AND didn&#39;t want to go down the &lt;a href=&quot;https://web.dev/articles/strict-csp#set-prepare&quot;&gt;Nonce-based strict CSP or Hash-based strict CSP route&lt;/a&gt; due to the additional complexity it adds to the site. For these reasons, and also for something to do over the Christmas Holiday period, I thought I&#39;d just refactor the code and use something more modern, like the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver&quot;&gt;PerformanceObserver API&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;In doing so, I&#39;d remove the need for the setTimeout code:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above code uses something called &quot;macro-task scheduling&quot;. This is where the browser adds this function to the end of the event loop queue. In doing so, it ensures:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The load event handler has finished.&lt;/li&gt;&lt;li&gt;The browser has had time to populate all the performance timing values.&lt;/li&gt;&lt;li&gt;The &lt;code&gt;performance.timing.loadEventEnd&lt;/code&gt; property has a valid value.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Without this macro-task scheduling code, the &lt;code&gt;loadEventEnd&lt;/code&gt; value won&#39;t have populated by the time it is required, which would likely result in a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN&quot;&gt;Not-A-Number (NaN)&lt;/a&gt; value being displayed on the page (or alternatively a non-helpful value of 0).&lt;/p&gt;&lt;h2 id=&quot;refactored-code&quot;&gt;Refactored Code&lt;/h2&gt;&lt;p&gt;In the updated code below I refactored it by:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining&quot;&gt;Optional chaining operator&lt;/a&gt; for detecting &lt;code&gt;window.performance&lt;/code&gt; and &lt;code&gt;window.performance.getEntriesByType&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;removing the setTimeout &quot;macro-task scheduling&quot; code for greater security (no more eval!).&lt;/li&gt;&lt;li&gt;using a modern API for optimal code execution (&lt;code&gt;PerformanceObserver&lt;/code&gt; API).&lt;/li&gt;&lt;li&gt;Wrapping the code in an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/IIFE&quot;&gt;Immediately Invoked Function Expression (IIFE)&lt;/a&gt; to ensure the browsers global scope isn&#39;t polluted with random functions and variables.&lt;/li&gt;&lt;li&gt;using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals&quot;&gt;Template literals (Template strings)&lt;/a&gt; for concatenating the final string that is injected into the DOM.&lt;/li&gt;&lt;li&gt;replacing the deprecated &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming&quot;&gt;PerformanceTiming API&lt;/a&gt; and have migrated to the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming&quot;&gt;PerformanceNavigationTiming API&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- HTML in my footer --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;footer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/js/webperf.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;defer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;footer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;4427ed19a89e1e3e897db9902d1002bf&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Web Performance Snippet — Page Load Time Display (webperf.js)
 *
 * Displays a simple &quot;This page loaded in: N milliseconds.&quot; message in the
 * footer using the PerformanceNavigationTiming API. Used for quick,
 * device-level feedback on load performance (e.g. on slow connections or
 * older devices).
 *
 * Requirements: PerformanceNavigationTiming (getEntriesByType(&#39;navigation&#39;)).
 * Runs only in supporting browsers; no polyfills. No eval; safe for strict CSP.
 */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// Ensure the browser supports the Performance API and the method we need&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// to fetch navigation timing (PerformanceNavigationTiming entries).&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;performance&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;getEntriesByType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Performance API not supported&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;/**
	 * Reads navigation timing and injects the load-time message into the DOM.
	 * Uses fallbacks when duration or loadEventEnd are 0 (e.g. iOS Safari, bfcache).
	 */&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;showLoadTime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entries &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; performance&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getEntriesByType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;navigation&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nav &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; entries&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;nav&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// --- iOS Safari / WebKit 0ms fix and fallbacks ---&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// On iOS Safari (and some WebKit builds), reading timing too early can&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// yield duration === 0 or loadEventEnd === 0. For example, if we had&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// used PerformanceObserver with type &quot;navigation&quot;, its callback can fire&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// before the load event has completed, so loadEventEnd is not yet set.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// We avoid that by only calling showLoadTime after the load event (see&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// runAfterLoad). Here we still defensively handle any remaining 0 values&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// (e.g. bfcache restores, or timing quirks):&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 1. Prefer nav.duration (equivalent to loadEventEnd - startTime when set).&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 2. If duration is 0 but loadEventEnd is set, compute loadEventEnd - startTime.&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// 3. If both are 0, fall back to domContentLoadedEventEnd - startTime.&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; ms &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nav&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;duration&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;ms &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; nav&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loadEventEnd &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			ms &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nav&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loadEventEnd &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; nav&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;startTime&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;ms &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; nav&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;domContentLoadedEventEnd &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			ms &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nav&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;domContentLoadedEventEnd &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; nav&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;startTime&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; ms &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ms &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageLoadTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;floor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ms&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; loadTimeElement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pl&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;loadTimeElement&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			loadTimeElement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;p&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			loadTimeElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pl&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; footer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;footer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;footer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
				footer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insertBefore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;loadTimeElement&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; footer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;firstChild&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;loadTimeElement&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			loadTimeElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;This page loaded in: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;pageLoadTime&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; milliseconds.&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;/**
	 * Runs showLoadTime after the load event, using a single setTimeout(0) so
	 * the read happens in a subsequent macro task. This is the main fix for the
	 * iOS issue where the snippet would otherwise show &quot;0 milliseconds&quot;:
	 * - On iOS Safari, if we read navigation timing before the load event has
	 *   fully completed, loadEventEnd (and thus duration) can still be 0.
	 * - By attaching to the window &quot;load&quot; event and then deferring the read
	 *   with setTimeout(showLoadTime, 0), we ensure the browser has set
	 *   loadEventEnd before we read from the Performance entry. We do not use
	 *   PerformanceObserver for the initial read for this reason.
	 */&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;runAfterLoad&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;showLoadTime&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;readyState &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;complete&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;runAfterLoad&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;load&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; runAfterLoad&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href=&quot;https://gist.github.com/Nooshu/4427ed19a89e1e3e897db9902d1002bf&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Here&#39;s a link to GitHub Gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; for the code above (with less comments).&lt;/p&gt;&lt;p&gt;In refactoring Tim&#39;s code, I &lt;strong&gt;deliberately&lt;/strong&gt; chose to exclude certain older browsers and their users from accessing this feature. The refactored code includes a check for the PerformanceNavigationTiming API, ensuring the web performance snippet is &lt;strong&gt;only&lt;/strong&gt; added to the footer if the browser supports this feature. This prevents older browsers from displaying a partially broken footer. Interestingly, this approach aligns with a modern take on a methodology Tim already uses on his website. His website follows a progressive enhancement technique known as &quot;cutting the mustard&quot;, a concept introduced by the &lt;a href=&quot;https://responsivenews.co.uk/post/18948466399/cutting-the-mustard&quot;&gt;BBC News web development team&lt;/a&gt; in April 2013!&lt;/p&gt;&lt;p&gt;Cutting the mustard is a technique that involves categorising browsers into two groups: older &quot;feature browsers&quot; with limited capabilities and modern &quot;smart browsers&quot; that support advanced features. By adopting a two-tiered approach to responsive web design, a core experience is delivered to all users, while additional enhancements are applied for more capable browsers. This ensures broad accessibility while providing a refined and feature-rich experience for modern devices like smartphones and larger screens. I actually used my own version of this in a previous blog post back in November 2019, when I implemented Webmentions purely in a client-side JavaScript in my blog post: &quot;&lt;a href=&quot;https://nooshu.com/blog/2019/11/18/implementing-webmentions/&quot;&gt;Implementing Webmentions on this blog&lt;/a&gt;&quot;. TL;DR: &lt;a href=&quot;https://gist.github.com/Nooshu/121e5bf7f3d5c6528413c57db257894f&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Thankfully, most modern browsers now auto-update every 6-weeks without users even knowing, so these older browsers are becoming less and less of an &quot;issue&quot;.&lt;/p&gt;&lt;p&gt;For example, let&#39;s have a look at the refactored features I mentioned above:&lt;/p&gt;&lt;h3 id=&quot;performanceobserver-api&quot;&gt;PerformanceObserver API&lt;/h3&gt;&lt;p&gt;This API allows developers to asynchronously observe and collect performance-related metrics, such as resource loading times, navigation events, and custom user timing marks. It listens for entries in the Performance Timeline, filtering them by type (e.g., &lt;code&gt;resource&lt;/code&gt;, &lt;code&gt;navigation&lt;/code&gt;, &lt;code&gt;paint&lt;/code&gt;) and executes a callback function whenever new entries are available. This API is useful for real-time performance monitoring (RUM) and analytics, e.g. Google Analytics.&lt;/p&gt;&lt;p&gt;The PerformanceObserver API was introduced into most major browsers between 2016 and 2017, Microsoft Edge was the last to introduce the API in January 2020 when they migrated over to using the Chromium browser engine, rather than EdgeHTML.&lt;/p&gt;&lt;p&gt;According to &lt;a href=&quot;https://caniuse.com/mdn-api_performanceobserver&quot;&gt;Can I Use, 96.64%&lt;/a&gt; of users globally now use a browser that supports the PerformanceObserver API.&lt;/p&gt;&lt;h3 id=&quot;template-literals&quot;&gt;Template Literals&lt;/h3&gt;&lt;p&gt;Template literals in JavaScript are string literals enclosed by backticks (&lt;code&gt;`&lt;/code&gt;) that allow for embedded expressions using the &lt;code&gt;${expression}&lt;/code&gt; syntax. They support multi-line strings, string interpolation, and special constructs like tagged templates for advanced processing of literals.&lt;/p&gt;&lt;p&gt;Template Literals were introduced into the JavaScript Language in ECMAScript 6 (ES6) which was &quot;released&quot; in 2015. Amazingly, most major browsers managed to implement the new specifications in the same year (2015), including iOS Safari 9.2 AND Edge 13 (EdgeHTML).&lt;/p&gt;&lt;p&gt;According to &lt;a href=&quot;https://caniuse.com/template-literals&quot;&gt;Can I Use, 97.97%&lt;/a&gt; of users globally now use a browser that supports the Template Literals language feature.&lt;/p&gt;&lt;h4 id=&quot;performancetiming-api&quot;&gt;PerformanceTiming API&lt;/h4&gt;&lt;p&gt;This API is part of the deprecated Navigation Timing API, that provides detailed timestamps for key events in the navigation and page load process, such as DNS lookup, server response, DOM processing, and resource loading. Accessible via &lt;code&gt;performance.timing&lt;/code&gt;, it allows developers to measure metrics like page load time, Time to First Byte (TTFB), and DOM readiness. While useful for diagnosing performance bottlenecks, it has been largely replaced by the more precise and structured PerformanceNavigationTiming API, available through the &lt;code&gt;performance.getEntriesByType(&#39;navigation&#39;)&lt;/code&gt; method.&lt;/p&gt;&lt;p&gt;The PerformanceTiming API is now deprecated, but it was first supported by all major browsers back in 2015. Incredibly, IE9 even supported it way back in 2011!&lt;/p&gt;&lt;p&gt;It&#39;s not recommended that the API be used today because at some point browser vendors may consider removing it. According to &lt;a href=&quot;https://caniuse.com/mdn-api_performancetiming&quot;&gt;Can I Use, 97.38%&lt;/a&gt; of users globally now use a browser that supports the PerformanceTiming API.&lt;/p&gt;&lt;h3 id=&quot;performancenavigationtiming-api&quot;&gt;PerformanceNavigationTiming API&lt;/h3&gt;&lt;p&gt;The API is a modern, high-precision replacement for the deprecated PerformanceTiming API, offering detailed metrics on navigation and page load performance. Accessible via &lt;code&gt;performance.getEntriesByType(&#39;navigation&#39;)&lt;/code&gt;, it provides timestamps for events like DNS lookup, server response, DOM processing, and resource loading, along with new attributes like &lt;code&gt;transferSize&lt;/code&gt; and &lt;code&gt;decodedBodySize&lt;/code&gt; for enhanced analysis. It simplifies performance monitoring with a single object and is ideal for modern web applications, including Single Page Apps (SPA&#39;s), making it the preferred choice for navigation performance insights.&lt;/p&gt;&lt;p&gt;If you know you are still using the PerformanceTiming API, it is recommended that you update to use the PerformanceNavigationTiming API. Although it&#39;s unlikely that support for the older API will be fully removed, the new API offers so much more functionality and is a much easier API to use. You may as well take advantage of the new features and functionality that is built into all modern browsers.&lt;/p&gt;&lt;p&gt;According to &lt;a href=&quot;https://caniuse.com/mdn-api_performancenavigationtiming&quot;&gt;Can I Use, 96.06%&lt;/a&gt; of users globally now use a browser that supports the PerformanceNavigationTiming API. It was supported all the way back in Edge 12 (EdgeHTML), and was finally supported in iOS Safari in version 15.3 back in December 2021.&lt;/p&gt;&lt;h2 id=&quot;data-driven-decision-making&quot;&gt;Data Driven Decision-Making&lt;/h2&gt;&lt;p&gt;So when it comes to browser support for modern features like this, how is it best to approach the old browser issue? In my opinion, you should approach it as a data collection exercise. If you have data to show that your site has browsers visiting that are approaching a decade old (at the time of writing), you should absolutely continue to &quot;cut the mustard&quot; to support those users.&lt;/p&gt;&lt;p&gt;Although I believe there&#39;s a very valid counterargument too. That by supporting such old and outdated browsers you&#39;re actually harming a users&#39; experience of the web as a whole, by allowing them to use such an outdated browser. Also, can you imagine how vulnerable a user who is still using a browser like IE9 would be on the modern web? Finally point, the web performance barrier to entry of modern &lt;a href=&quot;https://httparchive.org/reports/state-of-javascript&quot;&gt;JavaScript-driven websites&lt;/a&gt; would make the internet completely unusable for these browsers! With a &lt;strong&gt;Median&lt;/strong&gt; JavaScript size of 650 KB(!) as of December 2024, even a modern desktop browser would struggle!&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;So there we go, another blog post related to updating nooshu.com to 11ty and the steps I took to shed the years of technical debt I&#39;d accumulated and move to a more maintainable and forward facing blog. Big thanks to &lt;a href=&quot;https://timkadlec.com/&quot;&gt;Tim Kadlec&lt;/a&gt; for writing the original code snippet and inspiring me to add it to my blog! As always, I hope you enjoyed reading my random ramblings, and please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt; if you have any comments or feedback.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;02/01/25: Initial post published.&lt;/li&gt;&lt;li&gt;04/03/26: Updated the code snippet and Gist with a fix for the iOS mobile browser (Webkit), as &quot;0 milliseconds&quot; was often the result on page load.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Securing your static website with HTTP response headers</title>
    <link href="https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/" />
    <updated>2024-12-28T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/</id>
    <content type="html">&lt;p&gt;In this post, I&#39;m going to go into how I secured my 11ty blog (this site), using Cloudflare pages and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Response_header&quot;&gt;HTTP Response headers&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you aren&#39;t bothered about all the information below, you can just jump &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#just-show-me-the-code&quot;&gt;straight to the code&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;what-is-a-response-header&quot;&gt;What is a response header?&lt;/h2&gt;&lt;p&gt;An HTTP response header is a part of the response sent from a web server to a client (like your browser) when the client requests a resource, such as a webpage, image, or data.&lt;/p&gt;&lt;p&gt;The response header will contain additional information (metadata) about the request being made. Response headers come in three main categories:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Information about the response, for example the status code of the resource being requested. e.g. &lt;code&gt;200&lt;/code&gt; for an OK response or a status of &lt;code&gt;404&lt;/code&gt; for not found.&lt;/li&gt;&lt;li&gt;Details about the resource, for example a resource&#39;s Content Type e.g. &lt;code&gt;text/html&lt;/code&gt;, &lt;code&gt;text/css&lt;/code&gt;, &lt;code&gt;application/javascript&lt;/code&gt;&lt;sup&gt;†&lt;/sup&gt;.&lt;/li&gt;&lt;li&gt;Instructions for the client, for example how long to store a resource in a browser&#39;s cache (caching rules).&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;†: I realise that technically &lt;code&gt;Content-Type&lt;/code&gt; isn&#39;t actually a response header, it&#39;s actually a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Representation_header&quot;&gt;representation header&lt;/a&gt;, which provides metadata about the message body, such as its length, type, and encoding, helping the recipient understand and process the data correctly. But for simplicity&#39;s sake we&#39;ll consider it to be a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Response_header&quot;&gt;response header&lt;/a&gt; in this post.&lt;/p&gt;&lt;p&gt;For the rest of the blog post, we are going to focus on number 3: &quot;Instructions for the client&quot;. As with the security HTTP response headers, we will be instructing a user&#39;s browser as to what it can, and can&#39;t do with resources on a webpage.&lt;/p&gt;&lt;h2 id=&quot;why-its-a-static-website&quot;&gt;Why? It&#39;s a static website!&lt;/h2&gt;&lt;p&gt;Now I know what some of you will be thinking, since it&#39;s just static HTML and only hosted on a basic web server (with nothing dynamic), surely, there&#39;s nothing to secure? I mean, it&#39;s not like a hacker can hack into the Content Management System (CMS), and steal a username and password because in most cases there isn&#39;t one! This is true, but it&#39;s important to remember, static websites can be used for anything. I&#39;m sure plenty of developers use static websites to build transactional websites that accept sensitive user data, like their address, Social Security Number (US), National Insurance Number (UK) or Credit Card details. So really, the security you are implementing isn&#39;t to protect you, it&#39;s to protect &lt;strong&gt;your users&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Here is a list of reasons below:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Preventing Cross-Site Scripting (XSS) Attacks&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;See the &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#content-security-policy-csp&quot;&gt;Content-Security-Policy (CSP)&lt;/a&gt; section for details.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Securing Sensitive User Data&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;See the &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#strict-transport-security&quot;&gt;Strict-Transport-Security (HSTS)&lt;/a&gt; section for details.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Reducing Information Leakage&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;See the &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#referrer-policy&quot;&gt;Referrer-Policy&lt;/a&gt; section for details.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Isolating Contexts&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;See the &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#cross-origin-opener-policy-coop&quot;&gt;Cross-Origin-Opener-Policy (COOP)&lt;/a&gt; and &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#cross-origin-embedder-policy-coep&quot;&gt;Cross-Origin-Embedder-Policy (COEP)&lt;/a&gt; sections for details.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Blocking Unintended Features&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;See the &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#permissions-policy-formerly-feature-policy&quot;&gt;Permissions-Policy&lt;/a&gt; section for details.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Preventing Clickjacking&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;See the &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#x-frame-options&quot;&gt;X-Frame-Options&lt;/a&gt; section for details.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Improving Browser Behaviour&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;See &lt;a href=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/#x-content-type-options&quot;&gt;X-Content-Type-Options&lt;/a&gt; section for details.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Enhancing Trust and SEO&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;A secure website builds trust with users and browsers. Search engines also rank secure websites, potentially improving your site&#39;s SEO rankings.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;It&#39;s important to remember that even static websites can serve as entry points for attackers, host phishing pages, or be used in more complex attack chains.&lt;/p&gt;&lt;h2 id=&quot;just-show-me-the-code&quot;&gt;Just show me the code&lt;/h2&gt;&lt;p&gt;It&#39;s worth mentioning that the examples I&#39;m giving aren&#39;t specific to Cloudflare Pages or 11ty. They can be used with any static website, or indeed any website or web server (e.g. Apache, Nginx, IIS, Tomcat, Node.js).&lt;/p&gt;&lt;p&gt;The code below sits in my &lt;code&gt;_headers&lt;/code&gt; file in the &lt;code&gt;public&lt;/code&gt; assets directory of my 11ty blog on Cloudflare Pages. There&#39;s a tonne of information in the &lt;a href=&quot;https://developers.cloudflare.com/pages/configuration/headers/&quot;&gt;Cloudflare documentation on headers here&lt;/a&gt; if you&#39;re interested.&lt;/p&gt;&lt;pre class=&quot;language-text&quot; data-gist=&quot;61eff0dc1921c0beea352f9776e68d35&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/*
  Access-Control-Allow-Origin: https://nooshu.com
  Cache-Control: public, s-maxage=31536000, max-age=31536000
  Content-Security-Policy: base-uri &#39;self&#39;;child-src &#39;self&#39;;connect-src &#39;self&#39;;default-src &#39;none&#39;;img-src &#39;self&#39; https://v1.indieweb-avatar.11ty.dev/;font-src &#39;self&#39;;form-action &#39;self&#39; https://webmention.io https://submit-form.com/DmOc8anHq;frame-ancestors;frame-src &#39;self&#39; https://player.vimeo.com/ https://www.slideshare.net/ https://www.youtube.com/ https://giscus.app/ https://www.google.com/;manifest-src &#39;self&#39;;media-src &#39;self&#39;;object-src &#39;none&#39;;script-src &#39;self&#39; https://giscus.app/ https://www.google.com/ https://www.gstatic.com/;style-src &#39;self&#39; &#39;unsafe-inline&#39; https://giscus.app/;worker-src &#39;self&#39;;upgrade-insecure-requests;
  Cross-Origin-Opener-Policy: same-origin
  Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
  Referrer-Policy: strict-origin-when-cross-origin
  Cross-Origin-Resource-Policy: cross-origin
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  X-Content-Type-Options: nosniff
  X-DNS-Prefetch-Control: off
  X-Frame-Options: DENY
  X-Permitted-Cross-Domain-Policies: none
  Origin-Agent-Cluster: ?1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or I&#39;ve created a GitHub &lt;a href=&quot;https://gist.github.com/Nooshu/61eff0dc1921c0beea352f9776e68d35&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; if that&#39;s easier to copy, paste, and modify.&lt;/p&gt;&lt;p&gt;Just remember to change the &lt;code&gt;Content-Security-Policy&lt;/code&gt; (CSP) and ensure it allows any 3rd-party assets you load on your website. For this, you are specifically looking at the following CSP directives:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;form-action&lt;/code&gt; for form actions to a third party, e.g. a contact form.&lt;/li&gt;&lt;li&gt;&lt;code&gt;script-src&lt;/code&gt; to allow the execution of scripts from a third party, e.g. Google Analytics.&lt;/li&gt;&lt;li&gt;&lt;code&gt;img-src&lt;/code&gt; to allow images to load from a third party, e.g. Webmention images.&lt;/li&gt;&lt;li&gt;&lt;code&gt;frame-src&lt;/code&gt; to allow &lt;code&gt;&amp;lt;iframe&gt;&lt;/code&gt; embeds to load on your website e.g. YouTube, Vimeo, &lt;span class=&quot;strike&quot;&gt;Twitter&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;other-static-website-hosting&quot;&gt;Other Static website hosting&lt;/h2&gt;&lt;p&gt;I&#39;ve also created Gists for other static website hosts below, if you don&#39;t use Cloudflare pages.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: These versions haven&#39;t been tested, so use them at your own risk! They are a direct port from my Cloudflare Pages &lt;code&gt;_headers&lt;/code&gt; file to whatever format the specific host uses. Please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;contact me&lt;/a&gt; if you find any errors in any of these versions and I will update the Gist and attribute the change to you in the post changelog.&lt;/p&gt;&lt;h3 id=&quot;netlify&quot;&gt;Netlify&lt;/h3&gt;&lt;p&gt;As a &lt;a href=&quot;https://gist.github.com/Nooshu/f662f949bbbe5ca3f19c69af24f5c0f6&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;_headers file&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; or a &lt;a href=&quot;https://gist.github.com/Nooshu/77a2380bb30febe46bca79f0b1c8f215&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;netlify.toml file&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;vercel&quot;&gt;Vercel&lt;/h3&gt;&lt;p&gt;As a &lt;a href=&quot;https://gist.github.com/Nooshu/cbf16bc079da73a0ce476f96ae7b8d31&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;vercel.json file&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;github-pages&quot;&gt;GitHub Pages&lt;/h3&gt;&lt;p&gt;GitHub Pages itself does not support custom headers directly. Use a custom domain with a proxy service like Cloudflare to inject headers.&lt;/p&gt;&lt;h3 id=&quot;cloudflare-pages&quot;&gt;Cloudflare Pages&lt;/h3&gt;&lt;p&gt;Repeating myself I know, but for anyone who just quick scans looking for a &lt;a href=&quot;https://gist.github.com/Nooshu/61eff0dc1921c0beea352f9776e68d35&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Cloudflare Pages version it is here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;surge&quot;&gt;Surge&lt;/h3&gt;&lt;p&gt;A Surge version can be &lt;a href=&quot;https://gist.github.com/Nooshu/5862d39194228a4a18ac08b85c561c37&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;render&quot;&gt;Render&lt;/h3&gt;&lt;p&gt;A Render version can be &lt;a href=&quot;https://gist.github.com/Nooshu/a3bb16896f20db5ebbaa137365b6e4f6&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;firebase-hosting&quot;&gt;Firebase Hosting&lt;/h3&gt;&lt;p&gt;A Firebase version can be &lt;a href=&quot;https://gist.github.com/Nooshu/8870f4b7fd0db598807b89547a0d2a23&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;heroku&quot;&gt;Heroku&lt;/h3&gt;&lt;p&gt;A Heroku version can be &lt;a href=&quot;https://gist.github.com/Nooshu/13e72a89b7aa6ad6c69d1152dbcc67a3&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;infinityfree&quot;&gt;InfinityFree&lt;/h3&gt;&lt;p&gt;An InfinityFree version can be &lt;a href=&quot;https://gist.github.com/Nooshu/b3693f68cca7bc4869512b5ed82998ef&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;digitalocean&quot;&gt;DigitalOcean&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;A complete &lt;code&gt;nginx.conf&lt;/code&gt; config for DigitalOcean version can be &lt;a href=&quot;https://gist.github.com/Nooshu/c42671296ee58413147a0299b1eab604&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Or if you only require the &lt;code&gt;location&lt;/code&gt; block for Nginx that can be &lt;a href=&quot;https://gist.github.com/Nooshu/ccf899c71f23f1ca2e18daeb60bec214&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;1-and-1-ionos&quot;&gt;1&amp;1 IONOS&lt;/h3&gt;&lt;p&gt;1&amp;1 IONOS itself does not support custom headers directly. Use a custom domain with a proxy service like Cloudflare to inject headers.&lt;/p&gt;&lt;h2 id=&quot;header-specifics&quot;&gt;Header Specifics&lt;/h2&gt;&lt;p&gt;To learn what each of these response headers does, continue reading below:&lt;/p&gt;&lt;h3 id=&quot;access-control-allow-origin&quot;&gt;Access-Control-Allow-Origin&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This header specifies which origins (domains) are permitted to access the resources on the server via cross-origin requests. It is primarily used in handling Cross-Origin Resource Sharing (CORS).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Instead of using *, which allows any origin to access the resource (less secure), specify the trusted origin explicitly: &lt;code&gt;Access-Control-Allow-Origin: https://yourdomain.com&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://portswigger.net/web-security/cors/access-control-allow-origin&quot;&gt;portswigger.net&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://thecodebuzz.com/top-10-best-practices-cors-cross-origin-resource-sharing/&quot;&gt;thecodebuzz.com&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing#cors-misconfiguration&quot;&gt;owasp.org&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://www.w3.org/TR/cors/&quot;&gt;W3C Specification&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;cache-control&quot;&gt;Cache-Control&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The &lt;code&gt;Cache-Control&lt;/code&gt; header doesn&#39;t have anything to do with security, but it&#39;s an essential header for web performance as it gives the browser instructions on how an asset is stored or cached in the browser.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;There are too many options for this particular header. If you are interested in learning more about efficient browser caching, I recommend checking out the following sources.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://csswizardry.com/2019/03/cache-control-for-civilians/&quot;&gt;Cache-Control for Civilians&lt;/a&gt; by &lt;a href=&quot;https://webperf.social/@csswizardry&quot;&gt;Harry Roberts&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://jakearchibald.com/2016/caching-best-practices/&quot;&gt;Caching best practices &amp; max-age gotchas&lt;/a&gt; by &lt;a href=&quot;https://mastodon.social/@jaffathecake&quot;&gt;Jake Archibald&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;[https://alokai.com/blog/cache-control-http-header] by &lt;a href=&quot;https://peplinski.pro/&quot;&gt;Jakub Pepliński&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://simonhearne.com/2022/caching-header-best-practices/&quot;&gt;Caching Header Best Practices&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/SimonHearne&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Simon Hearne&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://httpwg.org/specs/rfc9111.html#field.cache-control&quot;&gt;W3C Specification&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;content-security-policy-csp&quot;&gt;Content-Security-Policy (CSP)&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The Content Security Policy (CSP) header is a security feature that protects web applications from attacks like Cross-Site Scripting (XSS) and code injection by controlling the sources from which browsers can load and execute content. If there&#39;s only a single response header you implement today, then make it the CSP Response header.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Here&#39;s a basic example of a CSP, I recommend checking out the resources below to ensure your website is secured with a comprehensive Content Security Policy. &lt;code&gt;Content-Security-Policy: default-src &#39;self&#39;; script-src &#39;self&#39; &#39;unsafe-inline&#39;; style-src &#39;self&#39; &#39;unsafe-inline&#39;; img-src &#39;self&#39; data:;&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;There are so many directives and settings available for a CSP, it&#39;s impossible to define a one-size fits all value. The best approach is to ensure you have one for your website. Adding a CSP to an existing website can be complicated, luckily there are tools out there to help developers, a number of which are listed below.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP&quot;&gt;CSP information on MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html&quot;&gt;Content Security Policy Cheat Sheet on owasp&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://csp-evaluator.withgoogle.com/&quot;&gt;Google CSP Evaluator&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://report-uri.com/home/generate&quot;&gt;CSP Generator from ReportURI&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://report-uri.com/home/analyse&quot;&gt;CSP Analyser from ReportURI&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.validbot.com/tools/csp-wizard.php&quot;&gt;Content Security Policy (CSP) Generator by ValidBot&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.quantcdn.io/tools/automatic-csp-generator&quot;&gt;CSP Generator from QuantCDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.feroot.com/blog/how-to-create-and-deploy-a-content-security-policy/&quot;&gt;How to Create and Deploy a Content Security Policy by feroot.com&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.w3.org/TR/CSP2/&quot;&gt;W3C Recommendation — CSP2&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;cross-origin-opener-policy-coop&quot;&gt;Cross-Origin-Opener-Policy (COOP)&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The COOP header controls whether a document can share its browsing context group with other documents from different origins. It helps isolate your web page from potentially malicious cross-origin interactions by preventing the sharing of resources like global objects and browsing contexts.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Cross-Origin-Opener-Policy: unsafe-none&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The recommended value isolates the page entirely from other origins. &lt;code&gt;Cross-Origin-Opener-Policy: same-origin&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheethtml#recommendation_10&quot;&gt;OWASP Cheat sheet&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy&quot;&gt;Cross-Origin-Opener-Policy on MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://andrewlock.net/understanding-security-headers-part-1-cross-origin-opener-policy-preventing-attacks-from-popups/&quot;&gt;Cross-Origin-Opener-Policy: preventing attacks from popups by Andrew Lock&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policies&quot;&gt;HTML Living Standard&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;permissions-policy-formerly-feature-policy&quot;&gt;Permissions-Policy (formerly Feature-Policy)&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Enables developers to specify which browser features can be accessed by different origins, both on the main page and within embedded frames.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Permissions-Policy: geolocation=(), microphone=()&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This isn&#39;t a one size fits all header, you basically disable any browsers features that you know your site will never use. A list of valid permission directives can be &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#directives&quot;&gt;found here&lt;/a&gt;. In the above example we are disabling the geolocation and microphone browser functionality on all origins.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#recommendation_13&quot;&gt;OWASP cheat sheet&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.okta.com/blog/2021/10/18/security-headers-best-practices#permissions-policy&quot;&gt;An Overview of Best Practices for Security Headers by Octa Developer&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://protocolguard.com/resources/what-is-permissions-policy/&quot;&gt;Permissions Policy HTTP Header: Configuration and Examples by ProtocolGuard&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://robotecture.com/http-topics/http-headers/permissions-policy/&quot;&gt;HTTP Header Permissions-Policy: Your Comprehensive Guide by Robotecture&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/h5bp/server-configs-apache/blob/main/h5bp/security/permissions-policy.conf&quot;&gt;HTML5 Boilerplate Apache Permissions Policy example&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://w3c.github.io/webappsec-permissions-policy/#permissions-policy-http-header-field&quot;&gt;W3C Editor&#39;s Draft&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;referrer-policy&quot;&gt;Referrer-Policy&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The Referrer-Policy HTTP response header determines the amount of referrer data to include in the Referrer header for outgoing requests. This security policy can either be set as a response header or in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; of the HTML document.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Referrer-Policy: origin&lt;/code&gt; OR &lt;code&gt;&amp;lt;meta name=&quot;referrer&quot; content=&quot;origin&quot; /&gt;&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Referrer-Policy: strict-origin-when-cross-origin&lt;/code&gt; OR &lt;code&gt;&amp;lt;meta name=&quot;referrer&quot; content=&quot;strict-origin-when-cross-origin&quot; /&gt;&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#recommendation_3&quot;&gt;OWASP Cheat sheet&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/referrer-best-practices&quot;&gt;Referrer and Referrer-Policy best practices by Maud Nalpas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://scotthelme.co.uk/a-new-security-header-referrer-policy/&quot;&gt;A new security header: Referrer Policy by Scott Helme&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/http-headers-referrer-policy/&quot;&gt;HTTP headers | Referrer-Policy by GeeksforGeeks&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header&quot;&gt;W3C&#39;s Editors Draft&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;cross-origin-resource-policy-corp&quot;&gt;Cross-Origin-Resource-Policy (CORP)&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The Cross-Origin-Resource-Policy (CORP) header is a security feature that controls whether resources like images, scripts, or fonts can be accessed by other websites to prevent unauthorized cross-origin access.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Cross-Origin-Resource-Policy: same-origin&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Cross-Origin-Resource-Policy: same-site&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#recommendation_12&quot;&gt;OWASP Cheat sheet&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://resourcepolicy.fyi/&quot;&gt;Consider deploying Cross-Origin Resource Policy&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy&quot;&gt;Cross-Origin Resource Policy (CORP) on MDN&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header&quot;&gt;Fetch Living Standard&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;cross-origin-embedder-policy-coep&quot;&gt;Cross-Origin-Embedder-Policy (COEP)&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The Cross-Origin-Embedder-Policy (COEP) header is a security feature that ensures your webpage only embeds safe, explicitly authorized resources, protecting against vulnerabilities like data leaks or &lt;a href=&quot;https://www.cloudflare.com/en-gb/learning/security/threats/meltdown-spectre/&quot;&gt;Spectre attacks&lt;/a&gt;. It is essential for sites using advanced web features like SharedArrayBuffer or WebAssembly to enhance security and performance.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: I&#39;ve included this header for completeness, but I currently don&#39;t have this header enabled on this site. This is because the comment technology I use &lt;a href=&quot;https://giscus.app/&quot;&gt;Giscus&lt;/a&gt;, doesn&#39;t support the COEP header. I have raised an issue on their &lt;a href=&quot;https://github.com/giscus/giscus/issues/1560&quot;&gt;GitHub Repository here&lt;/a&gt;, but unfortunately, there&#39;s yet to be any comments as to when it will be supported. So be careful with this header if you embed &lt;code&gt;&amp;lt;iframe&gt;&lt;/code&gt;&#39;s into your site as this header may break the functionality.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Cross-Origin-Embedder-Policy: unsafe-none&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Cross-Origin-Embedder-Policy: require-corp&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#recommendation_11&quot;&gt;OWASP Cheat sheet&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy&quot;&gt;Cross-Origin-Embedder-Policy on MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://really-simple-ssl.com/instructions/configuring-the-cross-origin-policies/&quot;&gt;Configuring the Cross-Origin Policies on Really Simple Security&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://andrewlock.net/understanding-security-headers-part-3-cross-origin-embedder-policy/&quot;&gt;Cross-Origin-Embedder-Policy: securing embedded resources by Andrew Lock&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://protocolguard.com/resources/cross-origin-embedder-policy/&quot;&gt;Cross-Origin Embedder Policy: An Essential Tool for Ensuring Web Security by ProtocolGuard&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/browsers.html#coep&quot;&gt;HTML Living Standard&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;strict-transport-security&quot;&gt;Strict-Transport-Security&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The Strict-Transport-Security (HSTS) header is a security feature that forces web browsers to communicate only over HTTPS with a website, ensuring that all subsequent requests are secure. It prevents attackers from exploiting protocols like HTTP or performing downgrade attacks. When enabled, the header includes parameters such as max-age (the duration for which the browser should enforce HTTPS), includeSubDomains (to apply to all subdomains), and preload (to include the site in browsers&#39; HSTS preload lists). This header significantly enhances security by protecting users from man-in-the-middle (MITM) attacks and ensuring encrypted communication.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Strict-Transport-Security: max-age=86400&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The max-age recommendation of 2-years seems to be quite a contentious topic as Google&#39;s preload list recommends 1-year (31536000), whereas OWASP recommends 2-years. There were varied opinions when I asked about it earlier in the year on the &lt;a href=&quot;https://community.cloudflare.com/t/owasp-recommends-hsts-max-age-63072000-recommendation/683734?u=m.r.hobbs&quot;&gt;Cloudflare forums&lt;/a&gt;. I currently have it set to 2-years, so it can be added to Google&#39;s Preload list and conforms with OWASP&#39;s recommendation (links in the sources below). &lt;code&gt;Strict-Transport-Security: max-age=63072000; includeSubDomains; preload&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security&quot;&gt;Strict-Transport-Security from MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html&quot;&gt;HTTP Strict Transport Security Cheat Sheet from OWASP&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://hstspreload.org/&quot;&gt;HSTS Preload Submission from Google&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.invicti.com/learn/http-strict-transport-security-hsts/&quot;&gt;HTTP Strict Transport Security (HSTS) from Invicti Security Corp&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://https.cio.gov/hsts/&quot;&gt;HTTP Strict Transport Security from CIO.GOV&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6797#section-6.1&quot;&gt;HTTP Strict Transport Security (HSTS)&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;x-content-type-options&quot;&gt;X-Content-Type-Options&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The X-Content-Type-Options header is a security feature that prevents browsers from interpreting files as a different MIME type than what is specified by the server. By setting it to nosniff, it ensures that browsers strictly adhere to the declared Content-Type, reducing the risk of MIME type confusion attacks, such as executing malicious scripts disguised as other file types. This header is particularly effective in preventing cross-site scripting (XSS) and drive-by download attacks.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;X-Content-Type-Options: none&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;There&#39;s only a single directive with this header, but I&#39;ve linked to additional resources below for further reference. &lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#recommendation_2&quot;&gt;HTTP Headers from OWASP&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options&quot;&gt;X-Content-Type-Options from MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.cytruslogic.com/website-security/understanding-x-content-type-options/&quot;&gt;Understanding X-Content-Type-Options from Cytrus Logic&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://fetch.spec.whatwg.org/#x-content-type-options-header&quot;&gt;Fetch Living Standard&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;x-dns-prefetch-control&quot;&gt;X-DNS-Prefetch-Control&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The X-DNS-Prefetch-Control header controls whether browsers should perform DNS prefetching, a performance feature that resolves domain names of links on a page before they are clicked. By setting it to on, DNS prefetching is enabled, potentially improving page load times. Conversely, setting it to off disables this behaviour, which can enhance privacy and security by preventing unnecessary DNS queries that could expose browsing behaviour or enable certain tracking methods. This header is useful for balancing performance and privacy needs.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: This header is currently a non-standard header, so check browser support before using.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;X-DNS-Prefetch-Control: on&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;X-DNS-Prefetch-Control: off&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#recommendation_19&quot;&gt;HTTP Headers from OWASP&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/http-headers-x-dns-prefetch-control/&quot;&gt;HTTP headers | X-DNS-Prefetch-Control from GeeksforGeeks&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.holisticseo.digital/technical-seo/web-accessibility/http-header/x-dns-prefetch-control&quot;&gt;X-DNS-Prefetch-Control HTTP Header: Syntax, Directive, Examples from Holistic SEO&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Not part of any current specification at the time of writing.&lt;/p&gt;&lt;h3 id=&quot;x-frame-options&quot;&gt;X-Frame-Options&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The X-Frame-Options HTTP header prevents clickjacking attacks by controlling whether a web page can be embedded in a frame or iframe. It supports three values: DENY (disallows framing completely), SAMEORIGIN (allows framing only on the same origin), and ALLOW-FROM URL (deprecated, allows framing from a specific origin). This header is being replaced by the more flexible frame-ancestors directive in Content Security Policy (CSP).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: This header is gradually being replaced with the (CSP) &lt;code&gt;frame-ancestors&lt;/code&gt; directive, but implementing both while this crossover happens should cause no negative impact to your website.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The ALLOW-FROM option is not widely supported and is largely replaced by the frame-ancestors directive in CSP. &lt;code&gt;X-Frame-Options: ALLOW-FROM https://example.com&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;X-Frame-Options: DENY&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#recommendation&quot;&gt;OWASP Cheat Sheet&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options&quot;&gt;X-Frame-Options on MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.indusface.com/learning/x-frame-options/&quot;&gt;Understanding X-Frame-Options: Examples and Benefits on Indusface&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.okta.com/blog/2021/10/18/security-headers-best-practices?utm_source=chatgpt.com#x-frame-options&quot;&gt;Security Header Best Practice from Okta Developer&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://pragmaticwebsecurity.com/articles/securitypolicies/preventing-framing-with-policies.html&quot;&gt;Current best practices to restrict framing in the browser&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/document-lifecycle.html#the-x-frame-options-header&quot;&gt;HTML Living Standard&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;x-permitted-cross-domain-policies&quot;&gt;X-Permitted-Cross-Domain-Policies&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The X-Permitted-Cross-Domain-Policies header specifies whether a browser or client can load cross-domain content, such as Flash or PDF files, from your domain. By setting it to values like none, master-only, or by-content-type, it controls the loading of potentially unsafe resources, mitigating risks such as data theft or unauthorized content execution. This header is particularly relevant for applications using plugins like Adobe Flash, providing a safeguard against cross-origin attacks and unauthorized data access.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: This header is currently a non-standard header, so check browser support before using.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;X-Permitted-Cross-Domain-Policies: by-content-type&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;X-Permitted-Cross-Domain-Policies: none&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Permitted-Cross-Domain-Policies&quot;&gt;X-Permitted-Cross-Domain-Policies from MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://geekflare.com/cybersecurity/http-header-implementation/#x-permitted-cross-domain-policies&quot;&gt;How to Implement Security HTTP Headers to Prevent Vulnerabilities? By Geekflare&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/CrossDomain_PolicyFile_Specification.pdf&quot;&gt;Adobe Cross Domain Policy File Specification&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;origin-agent-cluster&quot;&gt;Origin-Agent-Cluster&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The Origin-Agent-Cluster header controls whether a webpage should have its own separate &quot;&lt;a href=&quot;https://tc39.es/ecma262/#sec-agent-clusters&quot;&gt;agent cluster&lt;/a&gt;&quot;, isolating it from other origins in terms of memory and execution contexts. By setting it to &lt;code&gt;?1&lt;/code&gt;, the header ensures that the page&#39;s scripts, documents, and workers operate in isolation, preventing data leakage and improving security by mitigating cross-origin side-channel attacks like Spectre. This isolation enhances security for web applications, especially in environments with sensitive data or shared hosting scenarios.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: This header is currently a non-standard header, so check browser support before using.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Origin-Agent-Cluster: ?1&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The value of &lt;code&gt;?1&lt;/code&gt; is the &lt;a href=&quot;https://www.fastly.com/blog/improve-http-structured-headers&quot;&gt;structured header&lt;/a&gt; syntax for a boolean true value. &lt;code&gt;Origin-Agent-Cluster: ?1&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended value sources&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster&quot;&gt;Origin-Agent-Cluster on MDN&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://caniuse.com/?search=Origin-Agent-Cluster&quot;&gt;Origin-Agent-Cluster on CanIUse&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/origin-agent-cluster&quot;&gt;Requesting performance isolation with the Origin-Agent-Cluster header on Web.dev&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/browsers.html#origin-agent-cluster&quot;&gt;HTML Living Standard&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;final-results&quot;&gt;Final results&lt;/h2&gt;&lt;p&gt;So let&#39;s have a quick look at the final results of improving the HTTP Security headers on this blog by running it through the security scanner &lt;a href=&quot;https://securityheaders.com&quot;&gt;Security Headers by Snyk&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;before&quot;&gt;Before&lt;/h3&gt;&lt;p&gt;The score before came out at a pretty poor &quot;D&quot;: &lt;img alt=&quot;Nooshu.com with a security score of D on the Security Headers scanner.&quot; decoding=&quot;async&quot; height=&quot;214&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/fdqKDtUmU7-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/fdqKDtUmU7-300.webp 300w, https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/fdqKDtUmU7-600.webp 600w, https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/fdqKDtUmU7-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;after&quot;&gt;After&lt;/h3&gt;&lt;p&gt;The score after implementing the above changes comes out at a much better &quot;A+&quot;!: &lt;img alt=&quot;Nooshu.com with a security score of A+ on the Security Headers scanner.&quot; decoding=&quot;async&quot; height=&quot;214&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/-DR38yi7rf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/-DR38yi7rf-300.webp 300w, https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/-DR38yi7rf-600.webp 600w, https://nooshu.com/blog/2024/12/28/securing-your-static-website-with-http-response-headers/-DR38yi7rf-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;in-summary&quot;&gt;In summary&lt;/h2&gt;&lt;p&gt;In this post, we&#39;ve looked at what a response header is, why security response headers are important (even for static websites!), what the different implementations look like on different static hosting platforms, and also details on each of the response headers currently in use on this blog. Thanks for reading! I hope you found it interesting! And as always, if you spot any errors or have any feedback, please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;28/12/24: Initial post published.&lt;/li&gt;&lt;li&gt;29/12/24: Added the HTML5 Boilerplate Apache Permissions Policy example (Thanks to &lt;a href=&quot;https://hachyderm.io/@dmarti@federate.social&quot;&gt;Don Marti&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Migrating to 11ty</title>
    <link href="https://nooshu.com/blog/2024/12/22/migrating-to-11ty/" />
    <updated>2024-12-22T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2024/12/22/migrating-to-11ty/</id>
    <content type="html">&lt;p&gt;So, the first thing to mention about this post is: It&#39;s been years in the making! As I mentioned in my &lt;a href=&quot;https://nooshu.com/blog/2024/11/02/the-history-of-nooshu-com/&quot;&gt;previous blog post&lt;/a&gt;, I&#39;ve hated running my site on GitHub Pages and Jekyll for years now! So when I first heard that the wonderful &lt;a href=&quot;https://www.zachleat.com/&quot;&gt;Zach Leatherman&lt;/a&gt; had built an awesome new blogging framework that was built on Node.js back in December 2017, I thought, you know I need to migrate over to that ASAP! Then you find ASAP becomes weeks, weeks become months, and before you know it, the whole world goes through a global pandemic, and you get diagnosed with grade 3 brain cancer… Or maybe that&#39;s just me! It&#39;s just over 7-years since 11ty&#39;s first release, and I&#39;ve finally got around to updating this blog! Finally moving away from GitHub Pages and Ruby, and embracing the future. I&#39;ve decided to go all in with Node.js, Nunjucks, and Cloudflare. And as a bonus, this includes all the lovely web performance goodies that Cloudflare and Cloudflare Pages provide.&lt;/p&gt;&lt;h2 id=&quot;the-catalyst&quot;&gt;The Catalyst&lt;/h2&gt;&lt;p&gt;So what was the catalyst, you may ask? Well, it all starts with a man in a red hat and who has a deep, booming voice. He is very well-known in the web community as he runs 2 fantastic conferences, &lt;a href=&quot;https://2024.stateofthebrowser.com/&quot;&gt;State of the Browser&lt;/a&gt; (SotB), and also &lt;a href=&quot;https://londonwebstandards.org/&quot;&gt;London Web Standards&lt;/a&gt;, among other things! Of course, I&#39;m talking about the amazing &lt;a href=&quot;https://mastodon.social/@dletorey&quot;&gt;Dave Letorey&lt;/a&gt;! Let me tell you a short story about how I first met Dave. I was in Amsterdam to speak at &lt;a href=&quot;https://youtu.be/VdBxrZB9V_c?si=yKxXVM00-Iu8BKTz&quot;&gt;Performance Now 2023&lt;/a&gt;. Now, I&#39;d love to give the somewhat predictable story of how he walked up to me after my talk and asked me to speak at State of the browser 2024, but as I&#39;ve since learned, Dave likes to party! So I was sitting in front of Dave on the first day of Performance Now when he introduced himself. He said he was eagerly awaiting my talk tomorrow, and he&#39;s looking for speakers for SotB 2024, and would I be interested? I immediately said yes, thanked him, and started to watch the rest of the speakers from day one, while getting increasingly nervous all day, as the quality of the speakers at Performance Now are always incredible!&lt;/p&gt;&lt;p&gt;I was scheduled to be the &lt;a href=&quot;https://youtu.be/VdBxrZB9V_c?si=DwgUKtTLKCOVzZ89&quot;&gt;first talk on the second day&lt;/a&gt;. After the quality of the speakers on day 1, I knew I was going to have a tough time keeping folks interested and engaged with my talk, especially after they&#39;ve only just woken up and had their breakfast!&lt;/p&gt;&lt;p&gt;The next morning came and as I nervously scanned the audience, but I couldn&#39;t see Dave in the audience anywhere. I just assumed he was just a late riser, and he had decided to miss the morning talks. But as I mentioned, Dave likes to party, and it turns out, Dave had got very drunk the night before and ended up god knows where at 4am in Amsterdam! He admitted this to me later in the day once he&#39;d arrived. He was very apologetic for missing my talk, as it had been one he&#39;d been excited to hear! Thankfully (for Dave), the &lt;a href=&quot;https://www.youtube.com/playlist?list=PLjnstNlepBvNb93j0MICxh9ymBlq1It1D&quot;&gt;talks are recorded&lt;/a&gt;, so he&#39;d get to watch it at a later date!&lt;/p&gt;&lt;p&gt;Fast-forward a couple of months, I was just about to start a new job, and just my luck, my health took another turn for the worse, so I reluctantly had to pull out of speaking at State of the Browser 2024. It turns out Dave is an incredibly kind and giving person, as he contacted me closer to the conference date and asked if I&#39;d like to attend the conference as his guest. I agreed immediately, as it&#39;d been several years since my last commute into London (thanks to COVID-19 and cancer!). So that&#39;s what I did, and boy I&#39;m glad I took him up on the offer!&lt;/p&gt;&lt;h2 id=&quot;state-of-the-browser-2024&quot;&gt;State of the Browser 2024&lt;/h2&gt;&lt;p&gt;SotB always has some incredible speakers, you only need to look at their &lt;a href=&quot;https://2024.stateofthebrowser.com/history/#previous-years&quot;&gt;previous speaker list&lt;/a&gt; to see that! I enjoyed all the outstanding talks that day, but the one that resonated with me the most was &lt;a href=&quot;https://clagnut.com/&quot;&gt;Richard Rutter&lt;/a&gt;&#39;s talk on &quot;Fluid typography (and its role in design systems)&quot;. Now, I&#39;ve been a big fan of typography for many years, but I&#39;ve never been particularly good at design. Even so, I like to think I can recognise good design when I see it. His talk goes over various typography topics like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=18s&quot;&gt;Blue Note Jazz Album Covers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=185s&quot;&gt;Typographic Hierarchy&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=245s&quot;&gt;Type Scales&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=355s&quot;&gt;Responsive Design&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=798s&quot;&gt;Fluid Typography&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=1105s&quot;&gt;Utopia.fy&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=1450s&quot;&gt;Design Systems&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=X9uEszeeSOk&amp;t=1670s&quot;&gt;Real-World Examples&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;315&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; src=&quot;https://www.youtube.com/embed/X9uEszeeSOk?si=PembTkTmY-YHdJez&quot; title=&quot;YouTube video player&quot; width=&quot;560&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;It was when Richard started to discuss the tool that his company &lt;a href=&quot;https://clearleft.com/&quot;&gt;Clearleft&lt;/a&gt; had built called &lt;a href=&quot;https://utopia.fyi&quot;&gt;utopia.fyi&lt;/a&gt;, it really got me thinking and genuinely excited.&lt;/p&gt;&lt;p&gt;If you get the chance I&#39;d recommend you &lt;a href=&quot;https://www.youtube.com/embed/X9uEszeeSOk?si=PembTkTmY-YHdJez&quot;&gt;watch Richards talk&lt;/a&gt;, But In short, Utopia.fyi is a conceptual approach to fluid responsive design that encourages designers and developers to create systems where elements scale proportionally and fluidly across different screen sizes, without relying on arbitrary breakpoints (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries&quot;&gt;media queries&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;This methodology promotes minimalistic and elegant design, enhances collaboration between design and development teams, and ensures visual harmony and consistency. To support this approach, Utopia offers free tools such as type, space, and grid calculators.&lt;/p&gt;&lt;p&gt;It was after he demoed the tools in action that I realised it was time to throw away my old website design and be brave and attempt to create something a lot more modern and simplistic. And with the help of &lt;a href=&quot;https://utopia.fyi&quot;&gt;utopia.fyi&lt;/a&gt;, I may actually be able to &quot;design&quot; it myself!&lt;/p&gt;&lt;h2 id=&quot;throwing-everything-away&quot;&gt;Throwing everything away&lt;/h2&gt;&lt;p&gt;Once I&#39;d decided that I wanted to throw away the old design, it actually made moving forwards a lot easier. My problem with simply migrating an existing design to any new framework is that I&#39;d most likely migrate my &quot;bad habits&quot; and &quot;technical debt&quot; along with it. It was admitting this technical debt existed that gave me the freedom and the perfect opportunity to start from scratch and focus on building something I&#39;d be happy to maintain into the future.&lt;/p&gt;&lt;p&gt;This was something I never had with Jekyll. I used to dread writing and trying to publish a new blog post after not doing it for a while. There was always the feeling that GitHub may have changed &quot;something&quot;, and I was just about to run full-force into dependency hell just to get it published once written! It&#39;s safe to say that without Richard&#39;s inspirational talk, I&#39;d still have &quot;Migrate nooshu.com to 11ty&quot; very much welded to my to-do list!&lt;/p&gt;&lt;h2 id=&quot;learning-11ty&quot;&gt;Learning 11ty&lt;/h2&gt;&lt;h3 id=&quot;11ty-2-x-x&quot;&gt;11ty 2.x.x&lt;/h3&gt;&lt;p&gt;I swear I must be one of the unluckiest people I know when it comes to timings. 11ty version 2.0.x was released in February 2023, and the beta of version 3.0.0 had been in both alpha and beta release phases since June 2024. And of course, I picked the exact week 3.0.0 final was released to start my migration over to 11ty! At the start of the week, I began building from &lt;a href=&quot;https://github.com/11ty/eleventy-base-blog/tree/v8&quot;&gt;eleventy-base-blog v8&lt;/a&gt;, only to realise the scale of the differences between v2 and v3 were considerable for a beginner in 11ty like me. So I ended up scrapping my v2.0.0 version in favour of starting again using the latest &lt;a href=&quot;https://github.com/11ty/eleventy-base-blog&quot;&gt;eleventy-base-blog v9&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;11ty-3-0-0&quot;&gt;11ty 3.0.0&lt;/h3&gt;&lt;p&gt;Having never used 11ty before, I wanted to stick as close to current &quot;best practice&quot; in the 11ty ecosystem as I could, so figured using the &quot;eleventy-base-blog&quot; would be the best place to start since it&#39;s maintained by 11ty creator Zach Leatherman. You can&#39;t really get much closer to the original source than that! Luckily, most of my v2 work like the CSS and partials could be easily migrated across to v3 without any issues. It was mainly the core of 11ty that had changed. Especially, it&#39;s adoption of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules&quot;&gt;ECMAScript Modules (ESM)&lt;/a&gt; for its configuration.&lt;/p&gt;&lt;h2 id=&quot;minimal-viable-website&quot;&gt;Minimal Viable Website&lt;/h2&gt;&lt;p&gt;One of the problems with starting to migrate a live website from one technology to another is: at what point do you decide that the new site is &quot;ready&quot; to be published? It sounds simple, but it really isn&#39;t. It&#39;s far too easy to get greedy with functionality.&lt;/p&gt;&lt;p&gt;I found myself looking at other people&#39;s websites and saying: &quot;Ooh! That&#39;s cool! I need to make sure I build that functionality &lt;strong&gt;before&lt;/strong&gt; I put the site live!&quot;. And that&#39;s the slippery slope to a site that will never be published because it&#39;s never perfect. So I came up with a list for a Minimum Viable Website (MVW), off the back of a conversation with &lt;a href=&quot;https://sia.codes/&quot;&gt;Sia Karamalegos&lt;/a&gt;, who&#39;d I&#39;d had the absolute pleasure of meeting at Performance Now too.&lt;/p&gt;&lt;p&gt;My MVW list looked like this:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Built using &lt;a href=&quot;https://www.11ty.dev/blog/eleventy-v3/&quot;&gt;11ty v3.0.0&lt;/a&gt; (duh!).&lt;/li&gt;&lt;li&gt;New website design using &lt;a href=&quot;https://utopia.fyi/&quot;&gt;Utopia.fyi&lt;/a&gt; for both typography and spacing.&lt;/li&gt;&lt;li&gt;Hosted on &lt;a href=&quot;https://pages.cloudflare.com/&quot;&gt;Cloudflare Pages&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Optimised for &lt;a href=&quot;https://web.dev/accessibility&quot;&gt;accessibility&lt;/a&gt; and &lt;a href=&quot;https://web.dev/learn/performance&quot;&gt;web performance&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Content migrated to only use standard &lt;a href=&quot;https://en.wikipedia.org/wiki/Markdown&quot;&gt;Markdown&lt;/a&gt; for maximum portability in the future.&lt;/li&gt;&lt;li&gt;A simple contact form (ideally using &lt;a href=&quot;https://developers.cloudflare.com/workers/&quot;&gt;Cloudflare Workers&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;A &lt;a href=&quot;https://www.arimetrics.com/en/digital-glossary/blogroll&quot;&gt;blogroll page&lt;/a&gt; for the syndication of amazing content from all the wonderful content creator folks I follow.&lt;/li&gt;&lt;li&gt;An &lt;a href=&quot;https://en.wikipedia.org/wiki/RSS&quot;&gt;RSS feed&lt;/a&gt; in multiple formats (e.g. Atom, RSS, and JSON).&lt;/li&gt;&lt;li&gt;Static build automation using &lt;a href=&quot;https://github.com/features/actions&quot;&gt;GitHub Actions&lt;/a&gt; to rebuild the blog every 12 or so hours.&lt;/li&gt;&lt;li&gt;Implement &lt;a href=&quot;https://indieweb.org/Webmention&quot;&gt;Webmentions&lt;/a&gt; for indieweb syndication.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://json-ld.org/&quot;&gt;JSON+LD&lt;/a&gt; for SEO and machine-readable content.&lt;/li&gt;&lt;li&gt;Microformats using &lt;a href=&quot;https://microformats.org/wiki/h-card&quot;&gt;h-card&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://giscus.app/&quot;&gt;Giscus comments&lt;/a&gt; to allow readers to comment on posts should they wish to.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;As you can see, the list is already getting quite long! But just to make it a little more interesting, I decided to challenge myself to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Use a single flexible, responsive viewport that works on all devices (i.e. no &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries&quot;&gt;media queries&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;Only use plain old CSS, no &lt;a href=&quot;https://sass-lang.com/&quot;&gt;Sass&lt;/a&gt;, &lt;a href=&quot;https://lesscss.org/&quot;&gt;Less&lt;/a&gt; or &lt;a href=&quot;https://stylus-lang.com/&quot;&gt;Stylus&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Leverage modern &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties&quot;&gt;CSS variables&lt;/a&gt; wherever possible.&lt;/li&gt;&lt;li&gt;Utilise modern layouts like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout&quot;&gt;CSS Grid&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Flexbox&quot;&gt;Flexbox&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://dev.to/starbist/you-don-t-need-react-for-building-websites-455f&quot;&gt;Minimal JavaScript&lt;/a&gt;, no need for any JavaScript frameworks here&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Hand_coding&quot;&gt;Hand-coded&lt;/a&gt; HTML, CSS, and JavaScript all the way!&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme&quot;&gt;Light and dark mode&lt;/a&gt; functionality.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/SVG&quot;&gt;SVG&#39;s&lt;/a&gt; for icons and the main logo.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/browser-level-image-lazy-loading&quot;&gt;Lazy load&lt;/a&gt; as much as possible.&lt;/li&gt;&lt;li&gt;Use a &lt;a href=&quot;https://alistapart.com/article/understandingprogressiveenhancement/&quot;&gt;Progressive Enhancement&lt;/a&gt; methodology.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now, many readers may be wondering why I&#39;ve added some of these requirements into the build, isn&#39;t it just making life harder for myself? In all honesty, it&#39;s primarily for my interest and curiosity. The web is constantly evolving and moving forwards, and there&#39;s no better place to learn something new than your own personal website! Take CSS Variables for example, I&#39;ve known about them for quite a while as I&#39;d used to use their predecessor in Sass which had a similar variable implementation, but I&#39;d never used native, web platform &quot;vanilla&quot; CSS Variables. And after using them I&#39;m so glad I did, as it&#39;s going to make maintenance in the future much simpler!&lt;/p&gt;&lt;h2 id=&quot;migration-challenges&quot;&gt;Migration challenges&lt;/h2&gt;&lt;p&gt;Here I&#39;ll go over a few of the pain points I found during the migration:&lt;/p&gt;&lt;h3 id=&quot;cleaning-up-my-content-part-1&quot;&gt;Cleaning up my content — Part 1&lt;/h3&gt;&lt;p&gt;I&#39;ve written quite a few blog posts since 2009, when I &lt;a href=&quot;https://nooshu.com/blog/2024/11/02/the-history-of-nooshu-com/#the-beginning&quot;&gt;started blogging&lt;/a&gt;, and in that time I&#39;ve migrated the content across multiple CMS&#39;s and platforms. I actually wrote all about it in a &lt;a href=&quot;https://nooshu.com/blog/2024/11/02/the-history-of-nooshu-com/&quot;&gt;previous blog post&lt;/a&gt;. It probably took me around 1.5 weeks to complete the content migration task, doing the odd hour or so in the evening. What made matters worse is that during the migration from WordPress to Jekyll and GitHub pages, literally all the HTML character encoding across all blog posts broke! Thankfully, it wasn&#39;t noticeable at all to readers, but it&#39;s annoyed the hell out of me for years as it made my text editor light up like a Christmas tree. It also made spotting actual spelling errors much more difficult. This task in itself was very much a search and replace job, so it took a while!&lt;/p&gt;&lt;h3 id=&quot;cleaning-up-my-content-part-2&quot;&gt;Cleaning up my content — Part 2&lt;/h3&gt;&lt;p&gt;Over the lifetime of my Jekyll build, I&#39;d implemented some unusual non-standard ways to output elements like images. Basically, a simple abstraction layer include, that I added to make my life easier (in theory). As It turns out, it really doesn&#39;t when migrating to another blogging platform!&lt;/p&gt;&lt;p&gt;All of my images in every blog post looked like this:&lt;/p&gt;&lt;pre class=&quot;language-njk&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-njk&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;year/month/image-name.gif&quot;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Alt Text here&quot;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;caption&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;An example Caption here&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But in 11ty it all had to be changed. For 11ty, I&#39;ve decided to use the more standard Markdown way of adding images. So the above code needed to be transformed into this:&lt;/p&gt;&lt;pre class=&quot;language-md&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;Alt Text here.&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;./year/month/image-name.gif&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The new markup is certainly a lot cleaner and easier to remember, but the only downside I&#39;ve found is there&#39;s no support for image captions (&lt;code&gt;&amp;lt;figure&gt;&lt;/code&gt; &amp; &lt;code&gt;&amp;lt;figcaption&gt;&lt;/code&gt;). FYI: This is still an issue I&#39;ve yet to solve, so if anyone has added captions to their 11ty images output using Markdown, please &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;do let me know&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;Thankfully, there&#39;s a whole range of new tools on the web currently that can ease the pain of a migration process like this. It&#39;s a buzzword that is being crowbarred into pretty much every product and company presentation in 2024: &quot;&lt;strong&gt;AI&lt;/strong&gt;&quot;!&lt;/p&gt;&lt;p&gt;I&#39;ve used both the free and paid for versions of &lt;a href=&quot;https://chatgpt.com/&quot;&gt;ChatGPT&lt;/a&gt; and &lt;a href=&quot;https://gemini.google.com/app&quot;&gt;Google Gemini&lt;/a&gt;, and both of these basic models are perfect for this type of repetitive, mundane work.&lt;/p&gt;&lt;p&gt;My methodology, was to get the basic prompt setup at first, e.g. &quot;I want you to convert the &lt;code&gt;njk&lt;/code&gt; image template into the &lt;code&gt;md&lt;/code&gt; template&quot;, then see how well it did on its own. If there were issues, prompt it again to address the issues. Then keep repeating this process. And finally, once the output was what I needed it to be, I&#39;d give it a prompt like: &quot;That output format is perfect, do the same for each input I give you from now on&quot;.&lt;/p&gt;&lt;p&gt;From then on, simply paste in the old format and let AI generate the new format for you! Even that small workflow change literally saved me hours of manual copy / pasting into the new image template format.&lt;/p&gt;&lt;p&gt;I did wonder if I could prompt it to do this for a whole Markdown file from a post, but the prompt became so long and unwieldy, that I eventually gave up on that idea. But I&#39;ll take what I can from AI, every minute saved with the content migration task means more time to spend on &lt;a href=&quot;https://nooshu.com/blog/2024/12/22/migrating-to-11ty/#minimal-viable-website&quot;&gt;more interesting tasks&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;observations&quot;&gt;Observations&lt;/h2&gt;&lt;p&gt;Here are a few observations I&#39;ve made during my migration to 11ty:&lt;/p&gt;&lt;h3 id=&quot;flexibility&quot;&gt;Flexibility&lt;/h3&gt;&lt;p&gt;11ty is unbelievably flexible in terms of how you set it up and the templating languages it can accept. I have to admit, in some ways it&#39;s almost too flexible for a beginner. A lot of the time I found myself asking: &quot;Okay, I can do it that way… but is that the &quot;correct&quot; way to do it?&quot;. To give you a prime example of where I struggled, when I looked at the template languages documentation on &lt;a href=&quot;https://www.11ty.dev/docs/languages/&quot;&gt;this page&lt;/a&gt;, as a beginner I was overwhelmed by the number of options available to me.&lt;/p&gt;&lt;p&gt;I ended up using YAML frontmatter, Markdown for content, and Nunjucks for templating. A fairly standard setup coming from &quot;Jekyll land&quot;. But I&#39;m sure there are hundreds if not thousands of ways to achieve the same thing with all the templates 11ty supports. And as a beginner, it&#39;s challenging to know what the advantages and disadvantages are when choosing a particular templating language path. It&#39;s almost like a single paragraph is required on complex pages like this saying: &quot;If you are a 11ty beginner, we&#39;d recommend using a, b, and c because of reasons x, y, and z&quot;. Then, once a beginner is on a &quot;safe&quot; path to success, they can build up their confidence and start to evaluate other options and iterate accordingly.&lt;/p&gt;&lt;p&gt;Just so you know, this is in no way a dig at 11ty, I&#39;m just feeding this back to (hopefully) help others who also find too many options a little overwhelming at first!&lt;/p&gt;&lt;h3 id=&quot;unforgiving&quot;&gt;Unforgiving&lt;/h3&gt;&lt;p&gt;I&#39;ve found 11ty an unforgiving beast when it comes to error handling. If there&#39;s something wrong in the code, it will let you know about it! Even down to missing images that are referenced in blog posts! I believe that this is actually a good thing, and I want to applaud Zach for building it in this way. If I make such an obvious error like omitting an image file referenced in a blog post, I would like to be informed about it so I can fix it!&lt;/p&gt;&lt;p&gt;Although, at times, I did feel like Samuel L. Jackson&#39;s character in the original Jurassic Park (1993) when he repeatedly enters the &lt;a href=&quot;https://youtu.be/RfiQYRn7fBg?si=BU8j5mYlAq3dfiTH&quot;&gt;wrong password into Nedry&#39;s computer&lt;/a&gt;! But all in all, it&#39;s a good to catch these really obvious mistakes early.&lt;/p&gt;&lt;p&gt;If there&#39;s one feature I&#39;d like, it&#39;s to have a little &quot;offline prompt&quot; that is visible in the browser once the 11ty server goes offline (e.g. errors). This is just to make it blatantly obvious that something has gone wrong in the terminal. But it really is a minor issue.&lt;/p&gt;&lt;h3 id=&quot;community&quot;&gt;Community&lt;/h3&gt;&lt;p&gt;I have to commend Zach for successfully building such a strong and diverse community around 11ty! It really is a &quot;poster child&quot; of how an open-source project should be managed and, more importantly, how it should &lt;a href=&quot;https://opencollective.com/11ty&quot;&gt;be funded&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;There are so many resources available for beginners, like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Very comprehensive &lt;a href=&quot;https://www.11ty.dev/docs/deployment/&quot;&gt;documentation&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Eleventy Discussions on &lt;a href=&quot;https://github.com/11ty/eleventy/discussions&quot;&gt;GitHub&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;A dedicated &lt;a href=&quot;https://discord.gg/GBkBy9u&quot;&gt;Discord server&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;A dedicated &lt;a href=&quot;https://www.youtube.com/@EleventyVideo&quot;&gt;YouTube Channel&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Multiple YouTube playlists from the community &lt;a href=&quot;https://www.youtube.com/watch?v=yXcxvBJuULU&amp;list=PLwhCq3ZFGOGjQRYPqIjsiNwNYPVhvHW_r&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://www.youtube.com/watch?v=4wD00RT6d-g&amp;list=PLwhCq3ZFGOGgZDg_pnVh15gUfYqWnlWRs&quot;&gt;2&lt;/a&gt;, &lt;a href=&quot;https://www.youtube.com/c/THEEleventyMeetup/playlists?app=desktop&quot;&gt;3&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://learn-eleventy.pages.dev/&quot;&gt;Andy Bell&#39;s Learn 11ty course&lt;/a&gt;, now open-sourced and maintained by &lt;a href=&quot;https://github.com/uncenter/&quot;&gt;uncenter&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Numerous plugins, both &lt;a href=&quot;https://www.11ty.dev/docs/plugins/official/&quot;&gt;official&lt;/a&gt; and &lt;a href=&quot;https://www.11ty.dev/docs/plugins/community/&quot;&gt;community contributed&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.11ty.dev/docs/&quot;&gt;Getting started documentation&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.11ty.dev/#why-should-you-use-eleventy&quot;&gt;Why you should use Eleventy&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://11ty.rocks/&quot;&gt;11ty Rocks!&lt;/a&gt; By &lt;a href=&quot;https://thinkdobecreate.com/&quot;&gt;Stephanie Eckles&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://11tybundle.dev/&quot;&gt;11tybundle.dev&lt;/a&gt; by &lt;a href=&quot;https://bobmonsour.com/&quot;&gt;Bob Monsour&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.11ty.dev/docs/tutorials/&quot;&gt;Copious tutorials&lt;/a&gt; to keep you busy on the official site.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Honestly, I could go on, but I think you get my point!&lt;/p&gt;&lt;h2 id=&quot;future-plans&quot;&gt;Future Plans&lt;/h2&gt;&lt;p&gt;So, what plans do I have for this blog in the future? I still have a list of functionality that I&#39;d like to roll out at some point in the future, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Adding a plain text version of the &lt;a href=&quot;https://nooshu.com/feed/feed-atom.xml&quot;&gt;RSS feed&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Make the source code to this blog available on GitHub (thanks for the inspiration, &lt;a href=&quot;https://sia.codes/posts/migrating-netlify-to-cloudflare/#hiding-content-in-github&quot;&gt;Sia Karamalegos&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;Re-examining the use of pagination for the &lt;a href=&quot;https://nooshu.com/blog/&quot;&gt;blog post homepage&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Built in search functionality (inspired by &lt;a href=&quot;https://timkadlec.com/&quot;&gt;Tim Kadlec&lt;/a&gt; and &lt;a href=&quot;https://wpostats.com/&quot;&gt;WPOStats&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;Add additional themes just because I can! (inspired by &lt;a href=&quot;https://11tytheme.sjoy.lol/&quot;&gt;11tytheme.sjoy.lol&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;And finally, I have a list of around 5 or 6 smaller blog posts I want to write.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So, yes! Plenty still to be getting on with!&lt;/p&gt;&lt;h2 id=&quot;thanks&quot;&gt;Thanks&lt;/h2&gt;&lt;p&gt;Well, another blog post that turned out to be a lot longer than I first planned! Congratulations, you made it to the end! I hope you found it useful, as always, any feedback, please do &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;Finally, a heartfelt thank you to everyone who patiently answered my many &#39;n00b&#39; questions during this migration process, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://shivjm.blog/&quot;&gt;Aankhen&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://bobmonsour.com/&quot;&gt;Bob Monsour&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.brycewray.com/&quot;&gt;Bryce Wray&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://cassey.dev/&quot;&gt;Cassey Lottman&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://mastodon.social/@dletorey&quot;&gt;Dave Letorey&lt;/a&gt; for the SotB invite.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nicolas-hoizey.com/&quot;&gt;Nicolas Hoizey&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://clagnut.com/&quot;&gt;Richard Rutter&lt;/a&gt; for inspiring me to rebuild, this site.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://sia.codes/&quot;&gt;Sia Karamalegos&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://shkspr.mobi/blog/&quot;&gt;Terence Eden&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://tfedder.de/&quot;&gt;Tobias Fedder&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/uncenter&quot;&gt;Uncenter&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;22/12/24: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>The History of Nooshu.com</title>
    <link href="https://nooshu.com/blog/2024/11/02/the-history-of-nooshu-com/" />
    <updated>2024-11-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2024/11/02/the-history-of-nooshu-com/</id>
    <content type="html">&lt;h2 id=&quot;context&quot;&gt;Context&lt;/h2&gt;&lt;p&gt;To be clear, most (if not everyone but me) may find this post completely pointless and uninteresting. As I&#39;m trying to get back into blogging a lot more, and also consciously understand how this blog evolved. As it certainly wasn&#39;t planned at all, as I describe my &quot;decision-making&quot; process (or lack of, to be honest). You will see that a lot of the time it was just me wanting to learn something new and experiment with the latest shiny new technology in the Frontend world. If you think you&#39;d find that interesting, then please do read on! I&#39;m hoping that future blog posts will be more useful to readers as they will likely be more technical in nature, rather than the ramblings about the past 16 years!&lt;/p&gt;&lt;h2 id=&quot;the-beginning&quot;&gt;The beginning&lt;/h2&gt;&lt;p&gt;This website first popped into existence on the internet on the 30th March 2009 as a WordPress 2.7.1 website. I wish I could tell you where the &quot;Nooshu&quot; name actually came from, but it&#39;s been years, so I have no idea! I &lt;em&gt;think&lt;/em&gt; I may have used a random domain name generator, and it was one of the options that it suggested. The actual domain name was registered on Saturday 17th of January 2009, that&#39;s almost 16 years ago! God, I&#39;m old!&lt;/p&gt;&lt;h3 id=&quot;the-web-in-2009&quot;&gt;The web in 2009&lt;/h3&gt;&lt;p&gt;To put those 16-years into perspective in Web terms, let&#39;s examine what the web was like in 2009:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;IE6 was still the most popular browser on the internet. Although IE7 was becoming more popular since it supported tabs, RSS feeds and better support for the &lt;a href=&quot;https://en.wikipedia.org/wiki/Acid2&quot;&gt;ACID2 test&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Firefox was only on version 3.0. &lt;a href=&quot;https://hacks.mozilla.org/2017/10/saying-goodbye-to-firebug/&quot;&gt;Firebug&lt;/a&gt; was king, (the precursor to all browser DevTools).&lt;/li&gt;&lt;li&gt;Chrome was on Version 3.0 and only available on Windows. Chrome is considered the first silently auto-updating browser with stable, beta, and dev &quot;channels&quot;, once installed, the user doesn&#39;t have to prompt it to update in the future. Genius! Thus setting the course for the future of browser updates (apart from Safari). Nothing has changed there then, thanks Apple!&lt;/li&gt;&lt;li&gt;Mobile started to really take hold, Apple launched the App Store in 2008, and the first Android phones were released.&lt;/li&gt;&lt;li&gt;Most people were still on dial-up internet connections. Imagine having to wait minutes for a single YouTube video to load!&lt;/li&gt;&lt;li&gt;Twitter started to become popular, as celebrities joined the platform and the &quot;trending topics&quot; functionality was added.&lt;/li&gt;&lt;li&gt;Facebook overtook MySpace in global popularity.&lt;/li&gt;&lt;li&gt;jQuery was the saviour for every Frontend developer on the web, abstracting away most of the pain of cross-browser CSS &amp; JavaScript issues! Before it, you had frameworks like &lt;a href=&quot;https://en.wikipedia.org/wiki/MooTools#History&quot;&gt;Moo.fx&lt;/a&gt;, &lt;a href=&quot;https://code.google.com/archive/p/base2/&quot;&gt;base2&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/Prototype_JavaScript_Framework&quot;&gt;Prototype and Script.aculo.us&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Responsive Web Design didn&#39;t exist. It wasn&#39;t until May 2010 that Ethan Marcotte published his truly monumental &lt;a href=&quot;https://alistapart.com/article/responsive-web-design/&quot;&gt;Responsive Web Design article on A List Apart&lt;/a&gt;. That article changed the web platform forever!&lt;/li&gt;&lt;li&gt;Frontend frameworks like &lt;a href=&quot;https://getbootstrap.com/&quot;&gt;Bootstrap&lt;/a&gt; and &lt;a href=&quot;https://get.foundation/&quot;&gt;Zurb Foundation&lt;/a&gt; were yet to be released publicly, that only happened in 2011.&lt;/li&gt;&lt;li&gt;Everything was &lt;a href=&quot;https://en.wikipedia.org/wiki/Web_2.0&quot;&gt;&quot;Web 2.0&quot;&lt;/a&gt;. This term referred to the increasing interactivity and user-generated content on the web, with social media and blogs playing a central role.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I could go on, but I think you get the point I&#39;m trying to make, a lot has changed in 16-years! So what about this website, how has it changed? Here&#39;s a brief timeline of key events in its history.&lt;/p&gt;&lt;h3 id=&quot;2010-wordpress-dreamhost&quot;&gt;2010: WordPress + DreamHost&lt;/h3&gt;&lt;p&gt;The last point in the list above, &quot;Web 2.0&quot; was most likely the catalyst for me buying the domain and setting up my own self-hosted WordPress blog on DreamHost.&lt;/p&gt;&lt;p&gt;In fact, looking back through my post history, the domain was purchased in 2009, but I didn&#39;t post anything at all in the first year. It wasn&#39;t until January 2010 that I posted &lt;a href=&quot;https://nooshu.com/blog/2010/01/18/my-first-blog-post/&quot;&gt;my first blog post&lt;/a&gt;. And what a post it was, huh! You certainly set the web on fire with that one past-Matt, well done me!&lt;/p&gt;&lt;p&gt;2010 was an important time for HTML, as the &lt;a href=&quot;https://html.spec.whatwg.org/dev/&quot;&gt;HTML5 Specification&lt;/a&gt; was becoming a lot more mainstream. With the expiration of the &lt;a href=&quot;https://www.w3.org/2007/03/XHTML2-WG-charter&quot;&gt;XHTML 2.0 Working Group&#39;s charter&lt;/a&gt;, it was clear that HTML5 was the future of HTML.&lt;/p&gt;&lt;p&gt;And It certainly promised a lot, but the key features were: Cleaner code for developers with new semantic elements (&lt;code&gt;&amp;lt;header&gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;footer&gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;nav&gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;article&gt;&lt;/code&gt; etc.)&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Better cross-browser compatibility.&lt;/li&gt;&lt;li&gt;Improved performance&lt;/li&gt;&lt;li&gt;Easier development&lt;/li&gt;&lt;li&gt;Enhanced Search Engine Optimisation (SEO)&lt;/li&gt;&lt;li&gt;Of course, with any major addition to the web platform, older browsers that don&#39;t support new features need consideration too!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The primary rule on the web platform above all else is, remember:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;don&#39;t break the web!&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;There are an &lt;a href=&quot;https://worldwidewebsize.com/&quot;&gt;estimated 50 billion pages on the web&lt;/a&gt;, most of which are very likely never going to be touched or updated ever again! So whatever you do, make sure they work with both past &lt;strong&gt;and&lt;/strong&gt; future browsers.&lt;/p&gt;&lt;p&gt;As a side note: In my humble opinion, the web browser is up there as one of the greatest pieces of software ever created. Just think of all the errors and edge-cases it has to deal with, and it still manages to render readable pages, literally decades after they were first written and published! And in terms of human years of development effort, cooperation across continents, countries, and organisations, I&#39;d be interested to see if anything else comes close (apart from the invention of the World Wide Web itself!).&lt;/p&gt;&lt;p&gt;Now, since HTML5 was a big change that introduced new elements to the platform, older browsers wouldn&#39;t recognise them. So that&#39;s when &lt;a href=&quot;https://remysharp.com/&quot;&gt;Remy Sharp&lt;/a&gt; created the initial JavaScript solution to this problem. He used the &lt;code&gt;document.createElement(elementName)&lt;/code&gt; to let the CSS engine know that an element with that name exists. This advancement has a rich history that I won&#39;t go into here, but if you are interested, &lt;a href=&quot;https://toot.cafe/@paul_irish&quot;&gt;Paul Irish&lt;/a&gt; wrote an &lt;a href=&quot;https://www.paulirish.com/2011/the-history-of-the-html5-shiv/&quot;&gt;excellent blog post&lt;/a&gt; back in 2011 all about it!&lt;/p&gt;&lt;p&gt;Long story short, &lt;a href=&quot;https://nooshu.com/blog/2010/07/17/nooshu-com-now-with-added-html5-goodness/&quot;&gt;I also jumped on the HTML5 bandwagon&lt;/a&gt; and implemented the new semantic elements into my WordPress theme and also included the incredibly short DOCTYPE that I could finally remember! No more copy / pasting from a previous page templates, just look how short and simple it is!&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&gt;&lt;/code&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;2013-zurb-foundation-responsive-design&quot;&gt;2013: Zurb Foundation + Responsive Design&lt;/h3&gt;&lt;p&gt;Now, I fully admit I&#39;ve always been terrible at design! I like to think that I can recognise good design when I see it. But as for creating a design from scratch, not a chance! I clearly remember looking at the &lt;a href=&quot;https://csszengarden.com/&quot;&gt;CSS Zen Garden&lt;/a&gt; by &lt;a href=&quot;https://daveshea.com/&quot;&gt;Dave Shea&lt;/a&gt; in awe as to what people could create using an identical HTML structure and a single modified CSS file! &lt;a href=&quot;https://www.csszengarden.com/pages/alldesigns/&quot;&gt;Looking through the designs&lt;/a&gt; today, it still blows my mind at the creativity and imagination of others. There really were some groundbreaking designs in there.&lt;/p&gt;&lt;p&gt;And remember, CSS 2.0 was still new and far from being feature-complete. Furthermore, due to the first browser wars, browser vendors were racing to implement new features without any agreement on cross-browser specifications!&lt;/p&gt;&lt;p&gt;So when frameworks came out that promised to abstract away the actual design process for me, I jumped at the chance to use one! Simply modify your template to match the HTML specified in the framework, include the CSS and JavaScript, and you are done! A fully responsive website that was well tested and didn&#39;t look like the poster child for &lt;a href=&quot;https://en.wikipedia.org/wiki/GeoCities&quot;&gt;GeoCities&lt;/a&gt;. I&#39;m not sure &lt;a href=&quot;https://nooshu.com/blog/2013/08/28/site-update-using-zurb-foundation-4/&quot;&gt;why I chose Zurb Foundation&lt;/a&gt; over Bootstrap. I think it was because Zurb was less popular and considered the &quot;underdog&quot; against the vast resources of Twitter (makers of Bootstrap).&lt;/p&gt;&lt;h3 id=&quot;2016-pwas-and-amp&quot;&gt;2016: PWAs &amp; AMP&lt;/h3&gt;&lt;p&gt;It turned out that 2016 would be the year to experiment with new technologies. After all, that&#39;s what a personal website is for, isn&#39;t it??&lt;/p&gt;&lt;h4 id=&quot;service-workers&quot;&gt;Service Workers&lt;/h4&gt;&lt;p&gt;In July, I decided to experiment with the shiny new &lt;a href=&quot;https://www.w3.org/TR/service-workers/&quot;&gt;Service Worker API&lt;/a&gt;. This allowed me to turn my humble little blog into a &lt;a href=&quot;https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/&quot;&gt;Progressive Web Application (PWA)&lt;/a&gt;, complete with features like: Push Notifications. Native device access (Camera, GPS, etc). Offline Access. Installable to a user&#39;s device &quot;home screen&quot;. Greater reliability over unreliable network connections. Improved performance via fine control over asset caching. Essentially, PWAs provide a native app-like experience with features like home screen installation and access to more powerful device features.&lt;/p&gt;&lt;p&gt;In hindsight, the functionality is all pretty overkill for a small blog with barely any readers in my own little corner of the internet. But any excuse to dabble with new technology, I guess!&lt;/p&gt;&lt;h4 id=&quot;accelerated-mobile-pages-amp&quot;&gt;Accelerated Mobile Pages (AMP)&lt;/h4&gt;&lt;p&gt;Another technology I implemented was &lt;a href=&quot;https://nooshu.com/blog/2016/08/07/accelerated-mobile-pages/&quot;&gt;Accelerated Mobile Pages&lt;/a&gt;, or AMP for short.&lt;/p&gt;&lt;p&gt;For readers who&#39;ve not heard of them, Accelerated Mobile Pages are stripped-down versions of web pages that load rapidly on mobile devices.&lt;/p&gt;&lt;p&gt;As with the Service worker API, there was no real reason to implement AMP on this site apart from for general experimentation with the technology. In the end, AMP turned into (yet another) walled garden on the internet, this time it helped keep users (and their content) within the Google domain. In fact, the web community were so concerned with AMP that hundreds of developers &lt;a href=&quot;https://web.archive.org/web/20231018125109/http://ampletter.org/&quot;&gt;signed an online petition&lt;/a&gt; to persuade Google to make the technology more viable for the whole of the web platform (not just Google!)&lt;/p&gt;&lt;h3 id=&quot;2017-bye-bye-wordpress-hello-jekyll&quot;&gt;2017: Bye, bye WordPress, Hello Jekyll&lt;/h3&gt;&lt;p&gt;Having been with DreamHost and WordPress for 7-years, the web platform had gone through a huge amount of change. With the invention of the &lt;a href=&quot;https://jamstack.org/&quot;&gt;Jamstack&lt;/a&gt; web development architecture, it was now possible to create simple static websites using only a few build tools and host them on free services. Rather than having to use a CMS, a Database, and a Server-side language (like PHP, Ruby, .Net etc). And since I was already paying for GitHub for repository storage, why not use that for hosting my static website too? That ultimately lead me down the &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; route, since it&#39;s what is used by GitHub to render static HTML via their &lt;a href=&quot;https://pages.github.com/&quot;&gt;Pages functionality&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Annoyingly, I didn&#39;t write a blog post at the time about this dramatic architecture change to the site, so I&#39;m struggling to recall all the details. But I do remember that, not being a ruby developer, I found it quite a steep learning curve at first. Especially when it came to the asset pipeline. Furthermore, I don&#39;t think I ever got used to all the dependency hell you needed to jump through if you wanted to make a small change. When GitHub had updated the version of Jekyll, they used to render the pages e.g. etc.&lt;/p&gt;&lt;p&gt;But once the site was set up and all working, it was a breeze to update and deploy new blog posts! And, bonus! No monthly costs for separate hosting or updating WordPress to the latest version because of zero-day security issues!&lt;/p&gt;&lt;p&gt;The main downside I found with Jekyll and GitHub Pages was the limitation that you could only use &lt;a href=&quot;https://pages.github.com/versions/&quot;&gt;&quot;GitHub approved&quot; plugins&lt;/a&gt;. If you wanted to use another, less common plugin that GitHub didn&#39;t support, you&#39;re out of luck! There was probably a solution to that, but I never took the time to investigate further.&lt;/p&gt;&lt;p&gt;All in all, I was pleased with the setup for many years, but the dependency on Ruby in the background never sat well with me. It&#39;s an incredibly powerful language, but always seemed very overkill for generating simple static HTML files!&lt;/p&gt;&lt;h3 id=&quot;2018-bye-bye-amp&quot;&gt;2018: Bye, bye AMP&lt;/h3&gt;&lt;p&gt;Considering the uproar about AMP in 2016, I&#39;m not sure why it took me so long for to rip it out of the site!&lt;/p&gt;&lt;p&gt;It was most likely that having 2 young children, a mortgage, and a job that required 4-hours commute each day! Being a fully-fledged adult basically left me with no time to do it! But, thankfully, I did manage to write a post all about my &lt;a href=&quot;https://nooshu.com/blog/2018/02/06/removing-amp/&quot;&gt;reasons for removing AMP here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;2021-bye-bye-github-pages-hello-netlify-and-cloudflare&quot;&gt;2021: Bye, bye GitHub Pages, Hello Netlify, and Cloudflare&lt;/h3&gt;&lt;p&gt;So, 2021 turned out to be an incredibly &lt;a href=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/&quot;&gt;character building year&lt;/a&gt; for me personally and a truly horrible time for my immediate family and friends. As our late Queen Liz said in &lt;a href=&quot;https://en.wikipedia.org/wiki/Annus_horribilis#1992&quot;&gt;1992&lt;/a&gt; &amp; &lt;a href=&quot;https://en.wikipedia.org/wiki/Annus_horribilis#2019&quot;&gt;2019&lt;/a&gt;, it was our Annus horribilis (disastrous year).&lt;/p&gt;&lt;p&gt;But if there &lt;em&gt;is&lt;/em&gt; a silver lining to being diagnosed with Stage 3 brain cancer, it&#39;s that it &lt;em&gt;really&lt;/em&gt; motivates you to do things you&#39;ve been putting off for years! Looking back now, I was using work as a kind of &quot;safety net&quot;. Since it was something I had at least a little control over in my life. I&#39;d thrown myself well and truly into my work at Government Digital Service (GDS) and was getting more involved in the wonderful &lt;a href=&quot;https://ldnwebperf.org/&quot;&gt;Web Performance community&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;In doing so, I decided I needed much more control over the blog than what GitHub Pages could offer. The natural progression at that time was to &lt;a href=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/&quot;&gt;migrate to Netlify&lt;/a&gt;. The company had a free tier that came with all the features I required and also supported Jekyll. So migration would be simple. Once the migration was completed, my &quot;digital legacy&quot; would be running on 100% free services. I even placed the site behind Cloudflare&#39;s free tier services, so I could take advantage of all the lovely web performance goodies that they have to offer! Once completed, I could then focus on my immediate future (family, operation, radiotherapy, chemotherapy, recovery etc).&lt;/p&gt;&lt;h3 id=&quot;2024-bye-bye-netlify-and-jekyll-hello-11ty-and-cloudflare-pages&quot;&gt;2024: Bye, bye Netlify and Jekyll, Hello 11ty, and Cloudflare Pages&lt;/h3&gt;&lt;p&gt;I&#39;ve now had &quot;stable&quot; MRI scans for the past 3-years, which is remarkable! So it&#39;s time to look to the future and do something new with the site. Which brings us up to the present day. And this is the very blog post I&#39;ve written, and you (presumably) just read, that has been published using &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;11ty&lt;/a&gt; and &lt;a href=&quot;https://pages.cloudflare.com/&quot;&gt;Cloudflare Pages&lt;/a&gt;. And I must admit, it feels fantastic to finally clean up years and years of technical debt and give the site a new lick of paint, I even managed to complete most of the design myself!&lt;/p&gt;&lt;p&gt;But I&#39;ll leave the details about the full migration from Netlify and Jekyll, to 11ty and Cloudflare Pages for another blog post.&lt;/p&gt;&lt;h2 id=&quot;the-future&quot;&gt;The future&lt;/h2&gt;&lt;p&gt;In summary, a lot can change in 16-years, both on the web and in life. Or if you prefer a cheesy one-liner: &quot;Embrace the past, and use it to shape the future.&quot;&lt;/p&gt;&lt;p&gt;On that cheesy note, I&#39;ll leave you with one of my favourite quotes from &lt;a href=&quot;https://en.wikipedia.org/wiki/C._S._Lewis&quot;&gt;C.S. Lewis&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Isn&#39;t it funny how day by day nothing changes, but when you look back everything is different?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;As always, thanks for reading. And if you have any comments or feedback, either use the comment section below, or use one of the links on the &lt;a href=&quot;https://nooshu.com/contact&quot;&gt;contact page&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;02/11/24: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>GOV.UK Cookie banner and why it won&#39;t go away</title>
    <link href="https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/" />
    <updated>2023-12-12T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/</id>
    <content type="html">&lt;p&gt;This is a blog post I&#39;ve been meaning to write for many years. While working at Government Digital Service (GDS) for 6 years as Head of Frontend, I had so many people ask me about the GOV.UK Cookie banner. The main complaint being &quot;it would never go away&quot;. I was asked this question on so many platforms like email, Twitter, and even Reddit! The fact is, it has a surprisingly complex answer!&lt;/p&gt;&lt;p&gt;The aim of this blog post is to answer this question, so I can easily point someone in the direction of this post, should they be interested in the many technical details involved.&lt;/p&gt;&lt;p&gt;First and for-most, what is a cookie banner?&lt;/p&gt;&lt;h2 id=&quot;what-is-a-cookie-banner&quot;&gt;What is a cookie banner?&lt;/h2&gt;&lt;p&gt;In simple terms a cookie banner is that annoying pop-up that you get on every website on the modern web. The one that asks you to &quot;accept&quot; or &quot;decline&quot; the storage of cookies in your browser.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the GOV.UK Cookie banner on a laptop computer.&quot; decoding=&quot;async&quot; height=&quot;400&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/G1h2Rg6dJq-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/G1h2Rg6dJq-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/G1h2Rg6dJq-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/G1h2Rg6dJq-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The BBC Home cookie banner&quot; decoding=&quot;async&quot; height=&quot;140&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/jWxc8S_R1F-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/jWxc8S_R1F-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/jWxc8S_R1F-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/jWxc8S_R1F-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Cookie banner for VUE Cinema UK.&quot; decoding=&quot;async&quot; height=&quot;227&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/drqttXaHn4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/drqttXaHn4-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/drqttXaHn4-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/drqttXaHn4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Huge CNN cookie banner.&quot; decoding=&quot;async&quot; height=&quot;407&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/Soc8NqVmos-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/Soc8NqVmos-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/Soc8NqVmos-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/Soc8NqVmos-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;why-do-cookie-banner-exist&quot;&gt;Why do cookie banner exist?&lt;/h3&gt;&lt;p&gt;Cookie banners exist primarily due to the implementation of privacy regulations such as the &lt;a href=&quot;https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/guide-to-pecr/what-are-pecr/&quot;&gt;Privacy and Electronic Communications Regulations (PECR)&lt;/a&gt; in the European Union. There are similar regulations in other parts of the world like the &lt;a href=&quot;https://www.oag.ca.gov/privacy/ccpa&quot;&gt;California Consumer Privacy Act (CCPA)&lt;/a&gt; in the United States.&lt;/p&gt;&lt;h3 id=&quot;what-are-cookies&quot;&gt;What are cookies?&lt;/h3&gt;&lt;p&gt;Cookies are small text files that store information on a user&#39;s computer or device when they visit a website. They are used for various purposes. Some example usages are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;remembering a user&#39;s preferences&lt;/li&gt;&lt;li&gt;tracking user behaviour across websites&lt;/li&gt;&lt;li&gt;delivering targeted advertising&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;why-are-they-called-cookies&quot;&gt;Why are they called &quot;cookies&quot;?&lt;/h3&gt;&lt;p&gt;The term &quot;cookie&quot; was coined by web-browser programmer &lt;a href=&quot;https://en.wikipedia.org/wiki/Lou_Montulli&quot;&gt;Lou Montulli&lt;/a&gt; in 1994. Montulli was inspired by the concept of &quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/Magic_cookie&quot;&gt;magic cookies&lt;/a&gt;&quot; in early computer networking. These are small pieces of data that are sent between computers to help them communicate more efficiently. The term &quot;magic cookie&quot; was itself inspired by fortune cookies, which are small treats that contain a surprise message.&lt;/p&gt;&lt;p&gt;Montulli decided to use the term &quot;cookie&quot; for this new technology as he thought it would be easier for people to remember than a complex technical term.&lt;/p&gt;&lt;p&gt;Over time, the term &quot;cookie&quot; has become synonymous with any small piece of data that is stored on a user&#39;s computer by a website.&lt;/p&gt;&lt;h2 id=&quot;the-gov-uk-cookie-banner&quot;&gt;The GOV.UK Cookie banner&lt;/h2&gt;&lt;p&gt;In this post, my focus is specifically GOV.UK (&lt;a href=&quot;https://www.gov.uk&quot;&gt;www.gov.uk&lt;/a&gt;) and related UK Government services e.g.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.universal-credit.service.gov.uk/&quot;&gt;Universal Credit&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.check-mot.service.gov.uk/&quot;&gt;Check MOT&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.access.service.gov.uk/login/signin/&quot;&gt;HMRC Login&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sign-in.service.gov.uk/getting-started&quot;&gt;One Login&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.payments.service.gov.uk/&quot;&gt;GOV.UK Pay&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.notifications.service.gov.uk/&quot;&gt;GOV.UK Notify&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;What you may notice about the GOV.UK homepage (&lt;a href=&quot;https://www.gov.uk&quot;&gt;www.gov.uk&lt;/a&gt;) and the services listed above, is that they all have the same design.&lt;/p&gt;&lt;p&gt;There are a few minor differences between pages, but the overall aim is to use the same design language across all services. This is why the &lt;a href=&quot;https://design-system.service.gov.uk/&quot;&gt;GOV.UK Design System&lt;/a&gt; exists. To help service teams across the UK government achieve the same design quickly and easily.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the Design System Homepage&quot; decoding=&quot;async&quot; height=&quot;409&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/o8i2PJRaVi-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/o8i2PJRaVi-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/o8i2PJRaVi-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/o8i2PJRaVi-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It&#39;s also worth mentioning that the GOV.UK Design System also has a &lt;a href=&quot;https://design-system.service.gov.uk/components/cookie-banner/&quot;&gt;cookie component&lt;/a&gt; with lots of guidance for other service teams to use.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The design system has a dedicated cookie banner component.&quot; decoding=&quot;async&quot; height=&quot;630&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/vD9Mt2AFe1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/vD9Mt2AFe1-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/vD9Mt2AFe1-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/vD9Mt2AFe1-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This consistency was a conscious decision by the design team &lt;a href=&quot;https://gds.blog.gov.uk/2012/01/19/designing-govuk/&quot;&gt;back in 2011–2012&lt;/a&gt;. Its design is clean and simple and has always had accessibility &amp; inclusivity as top priorities from the very beginning.&lt;/p&gt;&lt;p&gt;The design actually won the &lt;a href=&quot;https://www.gov.uk/government/news/govuk-wins-design-of-the-year-2013&quot;&gt;Design Museum Design of the Year Award 2013&lt;/a&gt; and the &lt;a href=&quot;https://www.dandad.org/awards/professional/2013/writing-for-design/20081/govuk/&quot;&gt;D&amp;AD 2013 Black Pencil award&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;But as with every bold decision in life there are people who disagree. Like &lt;a href=&quot;https://gds.blog.gov.uk/2012/10/16/gov-uk-the-start/#comment-2454&quot;&gt;this comment from Howard Andrews&lt;/a&gt; who says:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Where is the government web site for grown-ups? The whole of &quot;gov.uk&quot; is obviously designed for primary school children and those with special educational needs.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I, personally, disagree with Howard, but as it&#39;s taxpayer money being spent everyone is entitled to express their opinion.&lt;/p&gt;&lt;p&gt;The point I&#39;m getting at is that consistency across the whole user journey was chosen to simplify the site for users. Do users need to know that they are moving between services run by entirely different departments? Not really! As long as they can complete their task quickly and easily and get on with their lives, I&#39;m sure they&#39;ll be happy.&lt;/p&gt;&lt;p&gt;But it&#39;s worth noting that this design consistency across services is a double-edged sword:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Gives users a stable design across the complete user journey thus reducing the cognitive load users experience.&lt;/li&gt;&lt;li&gt;Allows users to recognise a government website instantly.&lt;/li&gt;&lt;li&gt;Reduces development and design time in the public sector.&lt;/li&gt;&lt;li&gt;Reduces costs associated with building government services.&lt;/li&gt;&lt;li&gt;Builds brand loyalty and a site&#39;s trustworthiness&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Changing a design across multiple departments requires more coordination.&lt;/li&gt;&lt;li&gt;Technical edge-cases like the cookie banner reappearing across services (a lot more on this later!)&lt;/li&gt;&lt;li&gt;Users may be unaware they have navigated to an entirely different site.&lt;/li&gt;&lt;li&gt;Design stagnation due to a rigid set of rules to follow, the design may eventually look dated.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I strongly believe the benefits of consistent branding far outweigh the drawbacks. Especially when it comes to government services that millions of people use every day.&lt;/p&gt;&lt;h2 id=&quot;go-away-cookie-banner&quot;&gt;Go away cookie banner!&lt;/h2&gt;&lt;p&gt;Of all the &quot;cons&quot; listed above, this is the one that users notice the most!&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Users may be unaware they have navigated to a completely different site&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;If the site design barely changes between user navigations it&#39;s no wonder a user still thinks they are on the same site. As mentioned before, this was an intentional decision. It abstracts away the complexity of government departments into a clear and consistent user journey.&lt;/p&gt;&lt;p&gt;There&#39;s no single source of truth when it comes to back-end technologies in UK government services. For example, &lt;code&gt;www.gov.uk&lt;/code&gt; is built using Ruby on Rails. Whereas Universal Credit uses Java. The DVSA&#39;s Check MOT service is built using PHP. Unfortunately, the reason the cookie banner &quot;won&#39;t go away&quot; is exactly &lt;em&gt;because&lt;/em&gt; the user is on an entirely different site. There are technical reasons for why this happens that I will cover now.&lt;/p&gt;&lt;h2 id=&quot;browser-privacy&quot;&gt;Browser Privacy&lt;/h2&gt;&lt;p&gt;We start our technical explanation with how browsers have changed over recent years to protect users&#39; privacy online. Back when web browsers were first developed, the internet was nothing like it is today. User-data capture was primitive and so was internet advertising. Unfortunately, the same can&#39;t be said about the &quot;modern web&quot; of 2023. Companies across the globe have realised how valuable user-data is. And a modern-day gold-rush has emerged from the access to billions of peoples private browser data. Companies like Facebook and Google were until very recently able to easily track users across the internet using specific information like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Websites visited&lt;/li&gt;&lt;li&gt;Search queries&lt;/li&gt;&lt;li&gt;Social media interactions&lt;/li&gt;&lt;li&gt;Location data&lt;/li&gt;&lt;li&gt;Device information&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;From this data, companies can build an extremely accurate digital profile of users. Especially when these companies analyse this data all together (e.g. for all users). Very recently Google toyed with an idea called &lt;a href=&quot;https://privacysandbox.com/proposals/floc/&quot;&gt;Federated Learning of Cohorts (FloC)&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;FloC was a fairly simple idea. Examine all the captured user data and look for similarities in the data. Then create a bucket (or cohort) for all these similar profiles to live in. This turned out to be a great way to track a large group of people while keeping an individual&#39;s identity private. For example, if you recently browsed websites related to motorsport (e.g. Formula 1), then you would be placed into a cohort of other people who are interested in motorsport.&lt;/p&gt;&lt;p&gt;This all sounds very innocent, but it&#39;s incredible how accurate these cohorts can become, when there are 10s of thousands of people in them and millions of data points to examine. These cohorts would become more and more accurate over time as they gather more data and refine the algorithms. Eventually, these companies would have such an accurate idea of who you are and what you like. They&#39;d be able to predict what you enjoy and target you with targetted adverts that they would be fairly certain you were likely to engage with. Thankfully, the privacy community pushed back on the idea of FloC and the development was stopped in favour of a new technology called &lt;a href=&quot;https://privacysandbox.com/proposals/topics/&quot;&gt;Topics&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The key differences between FloC and Topics are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;A user will be assigned to a maximum of 5 topics from a predefined list of 350.&lt;/li&gt;&lt;li&gt;Cohorts aren&#39;t used, instead a user is assigned to specific topics based on their browsing history.&lt;/li&gt;&lt;li&gt;A user can edit the topics they have been assigned to and even turn the API off completely if they don&#39;t want targetted adverts.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The Topics API was first enabled in Chrome 115, released on July 17, 2023 as an &lt;a href=&quot;https://developer.chrome.com/origintrials/&quot;&gt;Origin Trial&lt;/a&gt;. On September 20, 2023 the Origin trial ended and Topics is now available for the vast majority of Chrome users.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Topics settings for Google Chrome.&quot; decoding=&quot;async&quot; height=&quot;356&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/uItb8-T5m8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/uItb8-T5m8-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/uItb8-T5m8-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/uItb8-T5m8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The current future of interest-based advertising on the web is uncertain. It is possible that these technologies will be replaced by new approaches in the future.&lt;/p&gt;&lt;h2 id=&quot;network-isolation-and-http-cache-partitioning&quot;&gt;Network isolation and HTTP Cache partitioning&lt;/h2&gt;&lt;p&gt;The next topic to discuss is another fairly modern development for browsers. Back in the early days of browsers there was a single browser cache that was used for every site you visited. This left site visitors open to a number of security and privacy attacks such as:&lt;/p&gt;&lt;h3 id=&quot;detect-if-a-user-has-visited-a-specific-site&quot;&gt;Detect if a user has visited a specific site&lt;/h3&gt;&lt;p&gt;An attacker can examine what is in the cache and infer what sites a user has visited from the cache contents. As certain resources may only be available on a specific set of sites.&lt;/p&gt;&lt;h3 id=&quot;cross-site-search-attack-xs-search&quot;&gt;Cross-site search attack (&lt;a href=&quot;https://xsleaks.dev/docs/attacks/xs-search/&quot;&gt;XS-Search&lt;/a&gt;)&lt;/h3&gt;&lt;p&gt;An attacker can identify whether a particular search query yielded no results by examining the presence of a &quot;no search results&quot; image stored in the user&#39;s browser cache.&lt;/p&gt;&lt;h3 id=&quot;cross-site-tracking&quot;&gt;Cross-site tracking&lt;/h3&gt;&lt;p&gt;The cache can be employed as a repository for persistent identifiers, enabling malicious actors to monitor user behaviour across multiple websites.&lt;/p&gt;&lt;p&gt;Thankfully, browser vendors have added security and privacy features to stop these attacks. The status of each major browser is listed below:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Chrome&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Cache partitioning was first introduced in Chrome 86, which was released on October 5, 2020. Additional storage partitioning beyond the HTTP Cache was enabled from Chrome 115 which was released on October 4, 2022. See the Privacy Sandbox article &lt;a href=&quot;https://developers.google.com/privacy-sandbox/3pcd/storage-partitioning&quot;&gt;here&lt;/a&gt; for more information.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Firefox&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Cache partitioning, also known as network partitioning, was enabled by default for all users in Firefox 85, which was released on January 13, 2021. This feature was introduced as part of Firefox&#39;s efforts to improve user privacy by making it more difficult for websites to track users across different websites. The browser also supports &lt;a href=&quot;https://support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-desktop&quot;&gt;Enhanced Tracking Protection&lt;/a&gt; which automatically protects a user&#39;s privacy while they browse.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Edge (v79+)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Edge usually follows Chrome closely, and at the time of writing Chrome and Edge are aligned and stable in terms of Cache partitioning. More information on Microsoft&#39;s plans for Edge and the Privacy-Preserving Ads API in 2024 can be found in &lt;a href=&quot;https://blogs.windows.com/msedgedev/2024/03/05/new-privacy-preserving-ads-api/&quot;&gt;this blog post published on the 5th March 2024&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Safari&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Apple has been way ahead of the curve with these features. The team actually talked about these features way back in &lt;a href=&quot;https://bugs.webkit.org/show_bug.cgi?id=110269&quot;&gt;February 2013&lt;/a&gt;! For more information check out this post &quot;&lt;a href=&quot;https://andydavies.me/blog/2018/09/06/safari-caching-and-3rd-party-resources/&quot;&gt;Safari, Caching and Third-Party Resources&lt;/a&gt;&quot; by good friend &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;ios-intelligent-tracking-prevention-itp&quot;&gt;iOS Intelligent Tracking Prevention (ITP)&lt;/h2&gt;&lt;p&gt;While we are on the subject of Apple and Safari, it&#39;s probably a good time to mention their default tracking protection. Apple enabled &lt;a href=&quot;https://webkit.org/blog/7675/intelligent-tracking-prevention/&quot;&gt;Intelligent Tracking Prevention (ITP)&lt;/a&gt; in Safari with the release of iOS 11 and macOS High Sierra on September 19, 2017.&lt;/p&gt;&lt;p&gt;ITP is used to stop sites from tracking a user across the internet. Safari automatically examines resources downloaded from a users&#39; journey around the internet.&lt;/p&gt;&lt;p&gt;Machine Learning is then used on the device to classify these resources to see if they could be used for tracking purposes. For more privacy, if a user hasn&#39;t interacted with a website in the past 30 days the website data and cookies are immediately purged.&lt;/p&gt;&lt;h3 id=&quot;itp-2-1&quot;&gt;ITP 2.1&lt;/h3&gt;&lt;p&gt;In February 2019 Apple released &lt;a href=&quot;https://webkit.org/blog/8613/intelligent-tracking-prevention-2-1/&quot;&gt;ITP version 2.1&lt;/a&gt; for iOS 12.2 and Safari 12.1 on macOS High Sierra and Mojave. This version brought major changes for sites wishing to track users. Third-party (tracking) cookies were blocked completely. Furthermore, any existing cookies that have had no user interaction for 30 days are then purged. More importantly for the GOV.UK cookie banner, any cookie set using the &lt;code&gt;document.cookie&lt;/code&gt; API automatically expires after only &lt;strong&gt;7 days&lt;/strong&gt;!&lt;/p&gt;&lt;h2 id=&quot;implications-of-itp-2-1-for-gov-uk&quot;&gt;Implications of ITP 2.1 for GOV.UK&lt;/h2&gt;&lt;p&gt;Although most users of iOS 12.2 and Safari 12.1 on macOS High Sierra and Mojave wouldn&#39;t have realised ITP 2.1 had been installed and enabled on their devices. This was, in fact, a considerable change for websites that used this data for decision-making purposes.&lt;/p&gt;&lt;p&gt;GDS and the GOV.UK team in particular, were one of those impacted by ITP 2.1. Although I&#39;ve heard many complaints over the years about this, GOV.UK uses Google Analytics (GA) for its analytics. The data captured is pretty limited. For example, whole sections of GA aren&#39;t enabled internally, like &quot;user demographics&quot;, &quot;behaviours&quot;, and &quot;interests&quot; weren&#39;t available for performance analysts to see. That&#39;s not to say Google can&#39;t see that data themselves!&lt;/p&gt;&lt;p&gt;But this is the hand we were dealt and a legacy of the age of GOV.UK. Back in 2012 when GOV.UK was first launched an Analytics product was needed to evaluate and prove that users were using GOV.UK and which parts were being used. User privacy wasn&#39;t as important as it is today, and GA was likely the most feature rich (and easiest) to implement. In hindsight a self-hosted analytics solution should have been used like &lt;a href=&quot;https://piwik.pro/&quot;&gt;Piwik&lt;/a&gt; (now &lt;a href=&quot;https://matomo.org/&quot;&gt;Motomo&lt;/a&gt;). Although I can&#39;t be sure if the versions at the time had all the features required by the GDS teams at the time (2007). See the &lt;a href=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/#additional-context&quot;&gt;additional context&lt;/a&gt; from &lt;a href=&quot;https://twitter.com/tomskitomski&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tom Loosemore&lt;/a&gt; below for more information on the early decisions made by GDS about tracking.&lt;/p&gt;&lt;h3 id=&quot;a-cookie-banner-every-7-days&quot;&gt;A cookie banner every 7 days!&lt;/h3&gt;&lt;p&gt;With the release and rollout of ITP 2.1 on iOS and OSX this meant the annoying cookie banner at the top of GOV.UK would appear every 7 days. This is because the cookie used to store the information that the user had already seen it would be purged every week. So a user browsing GOV.UK first thing Monday morning on an iOS device, accepts the banner then exactly 1 week later they will need to do the same. This would be pretty frustrating for the user, but unfortunately, it&#39;s completely outside any website&#39;s control. So if you&#39;re an iOS user and have ever wondered why you keep seeing the same cookie banner over and over again, this is the reason!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;A workaround to the 7 day problem&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for this info on a workaround. I quote:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;When you drop the cookie using client side JS, you make a JS API call a back-end which reads the cookie, and then sends it back as an HTTP set-cookie header. Boom - it&#39;s no longer a JS cookie.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;This is allowed AFAIK because Apple wants to block JS that isn&#39;t on the same domain as the document (i.e. most likely tracking cookies). Their intention was never to block pure 1st party cookies as I understand it.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;ios-mobile-bias&quot;&gt;iOS Mobile Bias&lt;/h3&gt;&lt;p&gt;According to the monthly GOV.UK statistics I posted &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1620739180119879680&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;back in February 2023&lt;/a&gt;, Safari made up 31.48% of traffic in the month. And &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1620739192270749696&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;56.21% of users were on a mobile&lt;/a&gt;, with the &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1620739195957575683&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Apple iPhone being the most popular device&lt;/a&gt; by an absolutely huge margin of 46.98%!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Overall stats for January 2023 available here: https://twitter.com/TheRealNooshu/status/1620739180119879680&quot; decoding=&quot;async&quot; height=&quot;1016&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ldnzziDmkn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ldnzziDmkn-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ldnzziDmkn-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ldnzziDmkn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Device type stats for January 2023 available here: https://twitter.com/TheRealNooshu/status/1620739192270749696&quot; decoding=&quot;async&quot; height=&quot;1405&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/aPfB-cVAsz-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/aPfB-cVAsz-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/aPfB-cVAsz-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/aPfB-cVAsz-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Popular device type stats for January 2023: https://twitter.com/TheRealNooshu/status/1620739195957575683&quot; decoding=&quot;async&quot; height=&quot;923&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/CbH6-Xvbel-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/CbH6-Xvbel-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/CbH6-Xvbel-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/CbH6-Xvbel-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Unfortunately, with the ITP change in version 2.1 there&#39;s no way of knowing how accurate these figures are. This is why I always posted a notice in the tweet thread saying:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;‡ Note: since December 2019 GOV.UK requires explicit opt-in for tracking which introduces bias. This is especially true for mobile devices, since the cookie banner takes up more screen estate and is more likely to be accepted.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Which brings us back to the cookie banner design. This is what GOV.UK homepage looks like on Desktop:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The cookie banner at the top of the GOV.UK homepage&quot; decoding=&quot;async&quot; height=&quot;409&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ou0IbtY1GU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ou0IbtY1GU-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ou0IbtY1GU-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/ou0IbtY1GU-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This is what it looks like on Mobile:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The GOV.UK homepage with cookie banner on an iPhone 6&quot; decoding=&quot;async&quot; height=&quot;1656&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/7DSI_BmbzR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/7DSI_BmbzR-300.webp 300w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/7DSI_BmbzR-600.webp 600w, https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/7DSI_BmbzR-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see, the cookie banner is huge on any mobile device taking up around 65% of the screen. It&#39;s also worth noting that a user is &lt;strong&gt;only&lt;/strong&gt; seen by GA if they accept the cookie banner. As the cookie banner on a mobile is huge, a mobile user is more likely to &quot;Accept&quot; it so they can actually view the page content! This where the mobile bias comes into play with the GOV.UK analytics data.&lt;/p&gt;&lt;p&gt;Note: This only became an issue when the &lt;a href=&quot;https://ico.org.uk/&quot;&gt;Information Commissioner&#39;s Office (ICO)&lt;/a&gt; ruled that tracking all users by default wasn&#39;t adhering to their &lt;a href=&quot;https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/lawful-basis/consent/&quot;&gt;explicit consent policy&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;additional-context&quot;&gt;Additional Context&lt;/h3&gt;&lt;p&gt;This is one of the reasons why I love blogging so much, it&#39;s astonishing how much you can learn from the feedback other people give you.&lt;/p&gt;&lt;p&gt;And it&#39;s also a great example of why you should keep some form of decision record in an organisation for when people leave and take all that precious knowledge with them! A huge thank you to &lt;a href=&quot;https://twitter.com/tomskitomski&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tom Loosemore&lt;/a&gt; for this additional context. Tom was Deputy Director, Government Digital Service, Cabinet Office, between January 2011 and October 2015.&lt;/p&gt;&lt;p&gt;I&#39;ve directly quoted his comment he left me on LinkedIn related to this post:&lt;/p&gt;&lt;blockquote&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Behind the scenes GDS tried very hard circa 2012 to convince the ICO that forcing cookie opt-in pop-ups would be counter productive to their laudable aims. Had they done proper user research as part of their response to &lt;a href=&quot;https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/guide-to-pecr/what-are-pecr/&quot;&gt;PECR legislation&lt;/a&gt; we might not be in this mess. Policy is delivery, redux.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;The GOV.UK team blogged several times about balancing the need for user analytics with privacy, with a nicely intense debate in the comments. Sadly piwik was totally unsuitable as an alternative to GA. Enough well-meaning organisations had adopted it and then regretted it for us to know it was a non-starter. See this &lt;a href=&quot;https://gds.blog.gov.uk/2012/01/12/cookies-on-the-beta/&quot;&gt;blog post&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Google would break its contract with HMG if it looked at or used the GA analytics data collected across *.gov.uk for anything other than protecting the GA service itself from attack. The options to stop Google employees (even GA support staff) accessing the *.gov.uk analytics data still seem strong enough to me. See this &lt;a href=&quot;https://support.google.com/analytics/answer/1011397?hl=en&amp;ref_topic=2919631#details-and-benefits&amp;zippy=%2Cin-this-article&quot;&gt;blog post&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/blockquote&gt;&lt;h2 id=&quot;the-public-suffix-list&quot;&gt;The Public Suffix List&lt;/h2&gt;&lt;p&gt;When it comes to setting cookies across domains &lt;code&gt;gov.uk&lt;/code&gt; and &lt;code&gt;service.gov.uk&lt;/code&gt;, there&#39;s an added complication. Both of these domains are on the &lt;a href=&quot;https://publicsuffix.org/list/public_suffix_list.dat&quot;&gt;Public Suffix List (PSL).&lt;/a&gt;&lt;/p&gt;&lt;p&gt;In simple terms, the PSL is a list of domain names that are considered to be effective top-level domains (eTLD). This has ramifications for setting cookies, that I will go into next.&lt;/p&gt;&lt;p&gt;If we wanted to stop the cookie banner appearing multiple times across a GOV.UK users journey, a cookie needs to be set to store the information that the banner has been seen and accepted. If we wanted to achieve this only using cookies, the PSL stops us from being able to do this.&lt;/p&gt;&lt;p&gt;As mentioned above &lt;code&gt;gov.uk&lt;/code&gt; and &lt;code&gt;service.gov.uk&lt;/code&gt; are in the PSL. So they are known as an effective top-level domain (eTLD). Browsers handle eTLD&#39;s differently to other domains. It so happens that Network isolation and HTTP Cache partitioning are partitioned using the following URL structure &lt;code&gt;scheme://eTLD+1&lt;/code&gt;. There&#39;s an example below that, I hope, will explain it clearer:&lt;/p&gt;&lt;p&gt;For a GOV.UK user browsing from the &lt;a href=&quot;https://www.gov.uk/&quot;&gt;homepage&lt;/a&gt; to the &lt;a href=&quot;https://www.universal-credit.service.gov.uk/sign-in&quot;&gt;Universal Credit Sign In&lt;/a&gt; page the browser sees the following:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;// ‘gov.uk&#39;
eTLD = gov.uk
+1 = www
eTLD + 1 = https://www.gov.uk

// any other government service
eTLD = service.gov.uk
+1 = www.universal-credit
eTLD + 1 = https://www.universal-credit.service.gov.uk
OR
eTLD + 1 = https://www.check-mot.service.gov.uk/
OR
eTLD + 1 = https://www.payments.service.gov.uk/
OR
// I think you get the idea&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The eTLD + 1&#39;s are the keys used for cache and network partitioning. Due to this partitioning, any cookie set on &lt;code&gt;www.gov.uk&lt;/code&gt; can&#39;t be seen or shared on &lt;code&gt;www.universal-credit.service.gov.uk&lt;/code&gt; or any other service domain. Thus, a user that accepted the cookie banner on the homepage will see it again in their user journey because this cookie can&#39;t persist through to the service domain.&lt;/p&gt;&lt;p&gt;This is the reason the cookie banner &quot;won&#39;t go away&quot;. Each service on a user&#39;s journey, sees them as a brand-new user that needs to accept that service&#39;s own cookie banner.&lt;/p&gt;&lt;p&gt;Unfortunately, as we mentioned earlier service domains follow the same GOV.UK branding, so many users won&#39;t even realise they are browsing an entirely different website.&lt;/p&gt;&lt;h2 id=&quot;related-website-sets-formerly-known-as-first-party-sets&quot;&gt;Related Website Sets (formerly known as: First-Party Sets)&lt;/h2&gt;&lt;p&gt;Another browser technology that looked promising and was considered to solve the shared cookie problem is called &lt;a href=&quot;https://wicg.github.io/first-party-sets/&quot;&gt;Related Website Sets (formerly known as: First-Party Sets)&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;First-Party Sets (now renamed Related Website Sets) is a Privacy Sandbox proposal that allows a company to declare relationships between websites they own. This enables browsers to allow limited cross-site tracking between these related sites for specific purposes, even after third-party cookies are phased out. This aims to minimise disruption to certain website functionalities while still prioritising user privacy.&lt;/p&gt;&lt;p&gt;At the time it was deemed unsuitable for &lt;code&gt;*.gov.uk&lt;/code&gt; and &lt;code&gt;*.service.gov.uk&lt;/code&gt; purposes because of the following limitations:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;There was a limitation of ~150 sites included in the &quot;set&quot;. This limit is way to small for the GOV.UK estate of sites and services.&lt;/li&gt;&lt;li&gt;For security and verification purposes each site in the set was required to have the same SSL certificate. This would simply be impossible to roll out and co-ordinate across all sites and services that encapsulate GOV.UK, hundreds of departments and teams would be involved in this process alone!&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Because of these 2 reasons the use of First-Party Sets was quickly dismissed as a viable option to solve a the shared cookie banner across all GOV.UK sites and services. These limitations may now have completely changed as the specification has evolved. There&#39;s a great explanation video on Related Website Sets (RWS) on the &lt;a href=&quot;https://developers.google.com/privacy-sandbox/3pcd/related-website-sets&quot;&gt;Privacy Sandbox website here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;&lt;p&gt;So there we have it. A whole brain dump on the GOV.UK cookie banner, GOV.UK service design and surrounding browser technologies related to privacy and security. Hopefully, you found it interesting. Maybe it answered a few questions you didn&#39;t even want to know 😂 As always if you have any questions or feedback, please do &lt;a href=&quot;https://twitter.com/TheRealNooshu/&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;get in touch&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post change-log:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;12/12/23: Initial blog post published. Happy Birthday to me!&lt;/li&gt;&lt;li&gt;12/12/23: Thanks (again) to &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for the numerous fixes to the &lt;a href=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/#cross-site-tracking&quot;&gt;Cross Site Tracking section of the post&lt;/a&gt; and a workaround for the 7 day time limit on Safari!&lt;/li&gt;&lt;li&gt;13/12/23: A huge thanks to &lt;a href=&quot;https://twitter.com/tomskitomski&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tom Loosemore&lt;/a&gt; for his incredibly informative LinkedIn post on the ICO and GA in the early years of GOV.UK. His update can be &lt;a href=&quot;https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security/#additional-context&quot;&gt;seen here&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;13/12/23: Thanks to &lt;a href=&quot;https://twitter.com/joelanman&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Joe Lanman&lt;/a&gt; for pointing out it&#39;s Privacy and Electronic Communications Regulations (PECR) not General Data Protection Regulation (GDPR) that is important for cookie banners.&lt;/li&gt;&lt;li&gt;13/12/23: Thanks to &lt;a href=&quot;https://toot.cafe/@slightlyoff&quot;&gt;Alex Russell&lt;/a&gt;, for getting back to me about the status of Cache partitioning in Edge.&lt;/li&gt;&lt;li&gt;05/03/24: Thanks to &lt;a href=&quot;https://twitter.com/rowan_m&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rowan Merewood&lt;/a&gt;, for the new information on what the Edge team has planned for 2024 in their &lt;a href=&quot;https://blogs.windows.com/msedgedev/2024/03/05/new-privacy-preserving-ads-api/&quot;&gt;latest blog post&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;02/07/24: Added information about Related Website Sets (formerly known as: First-Party Sets).&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Remove Subresource Integrity (SRI) from the GOV.UK assets domain</title>
    <link href="https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/" />
    <updated>2023-10-23T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to remove Subresource Integrity (SRI) from the assets served from the &lt;code&gt;assets&lt;/code&gt; domain of GOV.UK, thus enabling better performance for the HTTP/2 protocol. It was originally posted on the &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs&quot;&gt;GOV.UK RFCs GitHub repo&lt;/a&gt;. It can be &lt;a href=&quot;https://raw.githubusercontent.com/alphagov/govuk-rfcs/7e8909e095af59003179e055629522b1d38a48e1/rfc-112-remove-sri-on-assets.md&quot;&gt;viewed here&lt;/a&gt;. I&#39;m posting it to my blog so as to own my own work and hopefully give it more visibility. I wrote a blog post about the changes but the content was deemed too technical for the &quot;Technology in Government blog&quot;, so I pushed for it to be published as a case study instead, which can be &lt;a href=&quot;https://www.gov.uk/government/case-studies/how-gds-improved-govuks-frontend-performance-with-http2&quot;&gt;seen here&lt;/a&gt;. I also wrote about this issue on my personal blog the post is called &lt;a href=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/&quot;&gt;HTTP/2 and Subresource Integrity don&#39;t always get on&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Due to the security requirements of SRI, &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; is required for these assets. This forces browsers to open a separate TCP connection to download this resource. Since TCP connections are expensive to open and maintain, this practice has a negative effect on frontend performance.&lt;/p&gt;&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;/h2&gt;&lt;p&gt;Running &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;Web Page Test&lt;/a&gt; (WPT) against a number of pages across GOV.UK reveals that the browser is having to open a greater number of TCP connections due to the use or SRI, and more specifically the &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; attribute attached to the CSS and JavaScript references. This attribute is mandatory for &lt;a href=&quot;https://shubhamjain.co/til/subresource-integrity-crossorigin/&quot;&gt;security against cross origin policy violations&lt;/a&gt;. The use of the &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; forces the browser to open a brand new TCP connection to ensure that no user credentials are shared across the connection. This can be seen in the images below:&lt;/p&gt;&lt;h3 id=&quot;homepage&quot;&gt;Homepage&lt;/h3&gt;&lt;p&gt;In this example 11 TCP connections are being opened when the browser default for &lt;a href=&quot;https://docs.pushtechnology.com/cloud/latest/manual/html/designguide/solution/support/connection_limitations.html&quot;&gt;HTTP/1.1 in many browsers is 6&lt;/a&gt;. Since opening TCP &lt;a href=&quot;https://hpbn.co/building-blocks-of-tcp/#three-way-handshake&quot;&gt;connections is expensive&lt;/a&gt;, this will be having a negative effect on performance.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The WebPageTest connection view before SRI was removed.&quot; decoding=&quot;async&quot; height=&quot;362&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/O9P0DX8W3r-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/O9P0DX8W3r-300.webp 300w, https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/O9P0DX8W3r-600.webp 600w, https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/O9P0DX8W3r-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Full test can no longer be seen due to WebPageTest being sold to CatchPoint.&lt;/p&gt;&lt;h3 id=&quot;past-prime-ministers-page&quot;&gt;Past Prime Ministers page&lt;/h3&gt;&lt;p&gt;The same issue can be seen on the Past Prime ministers page, where 13 TCP connections are opened instead of the default 6 a browser usually does. As can be seen in the bandwidth graph the connection isn&#39;t being fully utilised, this is probably due to the TCP slow start is an algorithm that each TCP connection uses.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The WebPageTest connection view on the past Prime Ministers page is even more complex with a total of 14 TCP connections.&quot; decoding=&quot;async&quot; height=&quot;423&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/xzkFy2zxJV-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/xzkFy2zxJV-300.webp 300w, https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/xzkFy2zxJV-600.webp 600w, https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain/xzkFy2zxJV-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Full test can no longer be seen due to WebPageTest being sold to CatchPoint.&lt;/p&gt;&lt;h3 id=&quot;html-size&quot;&gt;HTML size&lt;/h3&gt;&lt;p&gt;A very minor point, but adding the &lt;code&gt;crossorigin=&quot;anonymous&quot; integrity=&quot;sha256-wLi6ixZSqsrSmNdPJHUiYBh/U4tQxAwkhPfzM8vDzys=&quot;&lt;/code&gt; to the 12 resource requests across a page could be adding an extra 1KB to the HTML size (although GZipping will minimise this).&lt;/p&gt;&lt;h3 id=&quot;http-2&quot;&gt;HTTP/2&lt;/h3&gt;&lt;p&gt;Will HTTP/2 solve this issue? Since HTTP/2 multiplexes assets over different streams on a single TCP connection. Unfortunately not. Requests without credentials use a separate connection as mentioned in the article &lt;a href=&quot;https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/#requests-without-credentials-use-a-separate-connection&quot;&gt;here&lt;/a&gt;. So even when we eventually enable HTTP/2 we will have the same issue where an anonymous connection will be opened.&lt;/p&gt;&lt;h2 id=&quot;proposal&quot;&gt;Proposal&lt;/h2&gt;&lt;p&gt;I&#39;m proposing we disable SRI for all assets from the &lt;code&gt;https://assets.publishing.service.gov.uk&lt;/code&gt; domain. Since this is a domain under our control it is highly unlikely that a MITM attack will occur. Assuming an attacker gets that far they would most likely be able bring the whole site down anyway due to the level of access they would need. Removing SRI will then allow us to remove the mandatory &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt;, which will then allow the browser to open and reuse connections as they are intended (and hit the maximum of 6 connections).&lt;/p&gt;&lt;p&gt;For assets that do come from a third-party domain, they should still have SRI enabled for security reasons.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Enabling HTTP/2 on GOV.UK</title>
    <link href="https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/" />
    <updated>2023-09-28T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable HTTP/2 on GOV.UK. It was originally posted on the &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs&quot;&gt;GOV.UK RFCs GitHub repository&lt;/a&gt;. It can be &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/blob/4d28064a477ab225864651f7c912c5aeae6c2fc1/rfc-115-enabling-http2-on-govuk.md&quot;&gt;viewed here&lt;/a&gt;. I&#39;m posting it to my blog so as to own my own work and hopefully give it more visibility. The Technology in Government blog post I wrote on the work can be &lt;a href=&quot;https://technology.blog.gov.uk/2020/07/02/speeding-up-gov-uk-with-http-2/&quot;&gt;seen here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Back in November 2018 we trialed the use of HTTP/2 on GOV.UK. According to quite a few sources, enabling HTTP/2 should improve web performance for users by introducing technology like multiplexed streams, HPACK header compression and stream prioritisation. Unfortunately it turned out that from our synthetic web performance testing it actually slowed the site down in many instances.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Results from testing HTTP/1.1 vs HTTP/2.&quot; decoding=&quot;async&quot; height=&quot;241&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/z8Hgwq87_8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/z8Hgwq87_8-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/z8Hgwq87_8-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/z8Hgwq87_8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;We tested 5 different page types, on multiple devices and connection speeds and examined the following performance metrics to come up with a result:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;First visual change&lt;/li&gt;&lt;li&gt;Visually complete 95%&lt;/li&gt;&lt;li&gt;Last visual change&lt;/li&gt;&lt;li&gt;Speed index&lt;/li&gt;&lt;li&gt;Load time (fully loaded)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And for Lighthouse reports these metrics were examined:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;First Contentful Paint&lt;/li&gt;&lt;li&gt;First Meaningful Paint&lt;/li&gt;&lt;li&gt;Speed Index&lt;/li&gt;&lt;li&gt;First CPU Idle&lt;/li&gt;&lt;li&gt;Time to Interactive&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The RFC below discusses the problems with our current setup and suggests possible solutions.&lt;/p&gt;&lt;h2 id=&quot;problems&quot;&gt;Problems&lt;/h2&gt;&lt;h3 id=&quot;1-sub-resource-integrity-sri&quot;&gt;1 - Sub Resource Integrity (SRI)&lt;/h3&gt;&lt;p&gt;On GOV.UK we are using Subresource Integrity for all our CSS and JavaScript assets coming from the assets domain. The &lt;a href=&quot;https://www.w3.org/TR/SRI/#cross-origin-data-leakage&quot;&gt;SRI specification&lt;/a&gt; requires that the &lt;code&gt;crossorigin&lt;/code&gt; attribute is set to &lt;code&gt;anonymous&lt;/code&gt; to be used with SRI resources for security reasons. This is because of data leakage from credentialed TCP connections. This requirement is forcing the browser to open a second TCP connection in &#39;anonymous mode&#39; so it can download the CSS / JS from the assets domain. In doing so this is adding 100&#39;s of milliseconds of delay to the page rendering timeline. This occurs even when using the &lt;a href=&quot;https://www.w3.org/TR/resource-hints/#fetching-the-resource-hint-link&quot;&gt;&lt;code&gt;preconnect&lt;/code&gt;&lt;/a&gt; resource hint, a browser feature intended to help fix this issue.&lt;/p&gt;&lt;p&gt;This performance issue is occurring in both HTTP/1.1 and HTTP/2 as seen in the WebPageTest connection view waterfalls below:&lt;/p&gt;&lt;h4 id=&quot;http-1-1&quot;&gt;HTTP/1.1&lt;/h4&gt;&lt;p&gt;&lt;img alt=&quot;The connection view can tell you a lot about how your connections are being utilised. Focus on each one in turn and see how much of the row is empty. This will show you the wasted time on each connection.&quot; decoding=&quot;async&quot; height=&quot;298&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/cPkmttAWw3-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/cPkmttAWw3-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/cPkmttAWw3-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/cPkmttAWw3-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we can see 13 TCP connections being opened (6 &#39;credentialed&#39;, 6 &#39;anonymous&#39;, 1 third-party to Google Analytics). If we weren&#39;t using SRI, we could reduce this requirement down to 8 (6 &#39;credentialed&#39;, 1 &#39;anonymous&#39; for fonts, 1 third-party to Google Analytics). This is the first step in improving web performance for GOV.UK users on HTTP/1.1.&lt;/p&gt;&lt;h4 id=&quot;http-2&quot;&gt;HTTP/2&lt;/h4&gt;&lt;p&gt;Below you can see the delay in the waterfall while the 2nd &lt;code&gt;anonymous&lt;/code&gt; TCP connection is established:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The delay seen in the HTTP/2 waterfall chart&quot; decoding=&quot;async&quot; height=&quot;261&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/4t8B8LqZBB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/4t8B8LqZBB-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/4t8B8LqZBB-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/4t8B8LqZBB-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;And this is what it could look like if we were to remove SRI:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The impact removing SRI has on the waterfall chart&quot; decoding=&quot;async&quot; height=&quot;243&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/J3bgFMrU8G-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/J3bgFMrU8G-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/J3bgFMrU8G-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/J3bgFMrU8G-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example test above on a Nexus 5 device under 3G connection speeds, we could bring the request of the CSS &amp; JS files forwards by ~750 ms. This should speed up the whole waterfall and turn the results from the summary list above from red to green in favour of HTTP/2.&lt;/p&gt;&lt;p&gt;This is achieved through the use of HTTP/2 connection coalescing, which can be seen in action on GOV.UK from our trial below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of an optimised connection view waterfall.&quot; decoding=&quot;async&quot; height=&quot;160&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/Jslsljb85u-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/Jslsljb85u-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/Jslsljb85u-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/Jslsljb85u-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The coalesced connection is under-utilised if &#39;anonymous mode&#39; is used on our static assets. There&#39;s also an impact from the fact that &lt;a href=&quot;https://en.wikipedia.org/wiki/TCP_congestion_control#Slow_start&quot;&gt;TCP Slow Start&lt;/a&gt; is in action on the delayed &lt;code&gt;anonymous&lt;/code&gt; connection, meaning assets will download slower than they could via the single coalesced connection. So we are in the following situation: the connection used to download the HTML isn&#39;t utilised fully (by this point it will already be up to speed), and the &lt;code&gt;anonymous&lt;/code&gt; connection downloading the critical CSS has been delayed, so it isn&#39;t up to speed yet. For best performance we should be utilising the connection that has already been established via the HTML download, and use it for other critical page assets (CSS/JS).&lt;/p&gt;&lt;h3 id=&quot;2-assets-served-with-access-control-allow-origin&quot;&gt;2 - Assets served with &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;My initial thinking was that we could quickly switch the &lt;code&gt;crossorigin&lt;/code&gt; attribute from &lt;code&gt;anonymous&lt;/code&gt; to &lt;code&gt;use-credentials&lt;/code&gt; for our static assets (CSS/JS). Unfortunately on examining the &lt;a href=&quot;https://fetch.spec.whatwg.org/&quot;&gt;Fetch specification&lt;/a&gt; there&#39;s &lt;a href=&quot;https://fetch.spec.whatwg.org/#cors-protocol-and-credentials&quot;&gt;information in the table (5th row down)&lt;/a&gt; that states:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;, &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;, and &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; response headers can only use &lt;code&gt;*&lt;/code&gt; as value when request&#39;s credentials mode is not &quot;include&quot;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Basically, the use of the wildcard (&lt;code&gt;*&lt;/code&gt;) isn&#39;t allowed on a credentialed connection (&lt;code&gt;use-credentials&lt;/code&gt;). And if it is used, the browser will block any requests and raise an error message the that looks like this:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘https://assets.example.com/script.js&#39;. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin&#39; is ‘*&#39;).&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;We are currently &lt;a href=&quot;https://github.com/alphagov/govuk-puppet/blob/962ea899e9c6778fe91e80074346912bd4314b10/modules/router/templates/assets_origin.conf.erb#L36-L38&quot;&gt;serving all our assets&lt;/a&gt; from the &lt;code&gt;assets&lt;/code&gt; domain with the &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; header, which is blocking us from making this change. The reason why we are serving assets like this is because we are using SRI and they need the &lt;code&gt;crossorigin&lt;/code&gt; attribute for it to work.&lt;/p&gt;&lt;p&gt;The Heroku applications (e.g. https://government-frontend.herokuapp.com) that are created are a legitimate reason for using this header when &lt;code&gt;crossorigin&lt;/code&gt; is present. If we were to remove the &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; header then the JS and fonts (due to their &lt;a href=&quot;https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements&quot;&gt;unique CORS requirements&lt;/a&gt;) on the Heroku previews would break. According to the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin&quot;&gt;documentation&lt;/a&gt;, there are only 3 valid values for this header:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;*&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;&amp;lt;origin&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;null&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The &lt;code&gt;&amp;lt;origin&gt;&lt;/code&gt; value is very important. As this is where we would ideally like to specify: &lt;code&gt;*.gov.uk; *.herokuapp.com&lt;/code&gt;. Unfortunately this header only accepts a &lt;strong&gt;single&lt;/strong&gt; origin, and it &lt;strong&gt;doesn&#39;t&lt;/strong&gt; recognise wildcard values. So we are in a situation where we need to use &lt;code&gt;*&lt;/code&gt; (for SRI) and a specific origin value (e.g &lt;code&gt;https://government-frontend.herokuapp.com&lt;/code&gt; for every unique Heroku app URL, including individual PR&#39;s) at the same time. There&#39;s the possibility that this can be done by adjusting the &lt;a href=&quot;https://www.fastly.com/blog/caching-cors&quot;&gt;VCL config on the CDN&lt;/a&gt;, but that then opens us out to more potential complications as listed in @kevindew&#39;s comment &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/pull/115#discussion_r366510706&quot;&gt;here&lt;/a&gt;. So this could complicate both the CDN configuration and local development if not properly investigated.&lt;/p&gt;&lt;h3 id=&quot;3-the-assets-domain&quot;&gt;3 - The assets domain&lt;/h3&gt;&lt;p&gt;Domain sharding for static assets is an anti-pattern under HTTP/2, and our current HTTP/1.1 setup isn&#39;t optimal for performance either. If our static assets weren&#39;t being served including SRI we would be able to remove the &lt;code&gt;crossorigin&lt;/code&gt; and &lt;code&gt;integrity&lt;/code&gt; attributes from the &lt;code&gt;&amp;lt;script&gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;link&gt;&lt;/code&gt; tags. This in turn would allow the browser to correctly use &lt;a href=&quot;https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/&quot;&gt;HTTP/2 connection coalescing&lt;/a&gt;, which would minimise the impact the assets domain is having on HTTP/2 performance.&lt;/p&gt;&lt;p&gt;Some browser implementations of HTTP/2 connection coalescing are &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=1011685&quot;&gt;notoriously flaky&lt;/a&gt;, so this performance benefit will only be available in some browsers. Users on older versions of Safari (before 12), IE, and Edge 18 and below, all don&#39;t support coalescing, so won&#39;t benefit from this optimisation. But as we&#39;ve seen above, the domain sharding isn&#39;t offering any benefits anyway. So why not remove the need for connection coalescing completely for our static assets and serve them from the origin. All browsers that support HTTP/2 will then receive the same benefits and experience of the browser using a single multiplexed TCP connection. &lt;a href=&quot;https://caniuse.com/#search=http2&quot;&gt;95.7% of the UK population&lt;/a&gt; use a browser that supports HTTP/2, so most of our users will benefit from this change.&lt;/p&gt;&lt;p&gt;Once completed our connection graph will look similar to this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of an optimised connection graph&quot; decoding=&quot;async&quot; height=&quot;186&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/XJIj_d3Dqh-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/XJIj_d3Dqh-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/XJIj_d3Dqh-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk/XJIj_d3Dqh-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The bulk of the page assets will be downloaded on the initial connection to the origin (www.gov.uk), with a secondary &lt;code&gt;anonymous&lt;/code&gt; connection only being opened for the fonts. That&#39;s twelve connections under our current HTTP/1.1 implementation, down to just two for HTTP/2 in many cases. So in order to fix our HTTP/1.1 and HTTP/2 performance issues we should serve static assets (CSS, JavaScript, fonts, images) all from the origin (www.gov.uk).&lt;/p&gt;&lt;h3 id=&quot;summary-2&quot;&gt;Summary&lt;/h3&gt;&lt;p&gt;In order to enable HTTP/2 on the GOV.UK domain (and for it to perform well) we need to all problems listed above:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;SRI requires the &lt;code&gt;crossorigin&lt;/code&gt; attribute to be set for it to work.&lt;/li&gt;&lt;li&gt;The use of &lt;code&gt;crossorigin=&#39;anonymous&#39;&lt;/code&gt; breaks HTTP/1.1 connection reuse and HTTP/2 connection coalescing.&lt;/li&gt;&lt;li&gt;Switching to use &lt;code&gt;crossorigin=&#39;use-credentials&#39;&lt;/code&gt; isn&#39;t possible because we serve our assets with the &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; header so that the assets leveraging SRI can be used on other domains (e.g. Heroku).&lt;/li&gt;&lt;li&gt;Even if we serve the static assets from the origin, we will still need the &lt;code&gt;crossorigin&lt;/code&gt; attribute because we will still be loading them with SRI enabled.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Therefore my recommendation is to remove the &lt;code&gt;integrity&lt;/code&gt; and &lt;code&gt;crossorigin&lt;/code&gt; attributes from our static resources (CSS, JS) and work towards serving these assets from the origin domain so we are no longer reliant on an individual browsers implementation of HTTP/2 connection coalescing. NOTE: it is possible to do these streams of work separately.&lt;/p&gt;&lt;p&gt;We should only serve our fonts with the &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; header (this is a requirement when using web fonts). All other assets can have this header removed (as we will have removed the &lt;code&gt;crossorigin&lt;/code&gt; attribute). We only need to be serving the following font files:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;EOT - For Internet Explorer only (6-11)&lt;/li&gt;&lt;li&gt;WOFF - For all modern versions of &quot;evergreen&quot; browsers and IE9-11.&lt;/li&gt;&lt;li&gt;WOFF2 - All modern browsers since ~2016. it&#39;s usage trumps the use of WOFF.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;EOT files are no longer going to be served to Internet Explorer once GOV.UK has migrated fully to the Design System. So EOT can be removed in the future. There are no plans that I am aware of to create another webfont format, so we won&#39;t be needing to add any more for the foreseeable future.&lt;/p&gt;&lt;p&gt;SRI Note: In serving the static assets from the origin domain, SRI no longer serves any purpose, as it&#39;s a security measure intended to be used on third party resources. As all of our assets will be first party, it really isn&#39;t required and serves only to complicate our setup and impact on performance.&lt;/p&gt;&lt;p&gt;Serving static assets from the origin also clears a path for future web performance enhancements like &lt;a href=&quot;https://www.fastly.com/blog/why-fastly-loves-quic-http3&quot;&gt;HTTP/3, QUIC (Quick UDP Internet Connection)&lt;/a&gt; and 0-RTT. &lt;a href=&quot;https://caniuse.com/#feat=http3&quot;&gt;HTTP/3&lt;/a&gt; has no support at the moment as it currently sits behind browser flags. But at some point in the future it will be supported and the asset domain will need to be removed for static assets anyway, to keep up with the latest protocol developments.&lt;/p&gt;&lt;p&gt;NOTE: As mentioned by @david-ncsc in &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/pull/115#issuecomment-567907397&quot;&gt;his comment&lt;/a&gt;, there&#39;s probably a security benefit to keeping the uploaded files on a separate origin as defence against uploaded malicious files. We should therefore only serve the static assets (CSS, JavaScript, fonts) from the origin. All other assets can stay the same.&lt;/p&gt;&lt;h2 id=&quot;proposal&quot;&gt;Proposal&lt;/h2&gt;&lt;h3 id=&quot;must&quot;&gt;MUST&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Only serve our font assets with &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; header (WOFF2, WOFF, EOT). - Medium (work required to modify NGINX config &lt;a href=&quot;https://github.com/alphagov/govuk-puppet/blob/962ea899e9c6778fe91e80074346912bd4314b10/modules/router/templates/assets_origin.conf.erb#L36-L38&quot;&gt;here&lt;/a&gt; and &lt;a href=&quot;https://github.com/alphagov/govuk-puppet/blob/master/modules/govuk/templates/asset_pipeline_extra_nginx_conf.erb&quot;&gt;here&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Remove the &lt;code&gt;integrity&lt;/code&gt; and &lt;code&gt;crossorigin&lt;/code&gt; attributes from our static resources (CSS, JS) - Easy (PR&#39;s quickly raised for all apps CSS/JS).&lt;/li&gt;&lt;li&gt;Serve static assets from the origin rather than the assets domain - Medium/Hard (spike already investigated by @kevindew and commented on &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/pull/115#issuecomment-573008991&quot;&gt;here&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;should&quot;&gt;SHOULD&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Review the use of &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; and &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; headers as from looking at the CORS documentation they aren&#39;t actually needed. - Easy (Code resides &lt;a href=&quot;https://github.com/alphagov/govuk-puppet/blob/962ea899e9c6778fe91e80074346912bd4314b10/modules/router/templates/assets_origin.conf.erb#L36-L38&quot;&gt;here&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;consequences&quot;&gt;Consequences&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Browsers that support HTTP/2 will be allowed to use it to its maximum potential. Browsers that don&#39;t will still use HTTP/1.1, but will now only require 6 TCP connections rather than 12.&lt;/li&gt;&lt;li&gt;Both H1 and H2 users should receive improved performance due to improved TCP connection efficiencies.&lt;/li&gt;&lt;li&gt;Under HTTP/2 we should see only 2 TCP connections on the WebPageTest connection graph, with the vast majority of assets loading via the same connection as the HTML.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;how-to-enable-http-2&quot;&gt;How to enable HTTP/2&lt;/h3&gt;&lt;p&gt;Once we have made the changes how do we enable HTTP/2, and what changes need to be made to the applications?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;I simply contact Fastly support and ask them to enable it on GOV.UK. This will enable it on production, integration, and the assets domains.&lt;/li&gt;&lt;li&gt;No changes to any infrastructure or apps need to be made, as this is a transport layer change on the CDN.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Enabling Brotli compression on GOV.UK</title>
    <link href="https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/" />
    <updated>2023-09-28T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable Brotli compression on GOV.UK. It was originally posted on the &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs&quot;&gt;GOV.UK RFCs GitHub repo&lt;/a&gt;. It can be &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/blob/4d28064a477ab225864651f7c912c5aeae6c2fc1/rfc-138-enable-brotli-compression.md&quot;&gt;viewed here&lt;/a&gt;. I&#39;m posting it to my blog so as to own my own work and hopefully give it more visibility. A blog post was drafted about this change but unfortunatly never published, as it was considered too technical for a GDS blog post.&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Brotli compression is a new compression algorithm that is available in modern browsers that can offer 10-20% better compression over gzip compression. By enabling this option on the CDN users with browsers that support it will receive smaller static files over the wire (HTML, CSS, JavaScript), and those that don&#39;t will continue to receive assets that are gzipped.&lt;/p&gt;&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;/h2&gt;&lt;p&gt;On GOV.UK we should always be looking to reduce the number of bytes a user has to download to view our pages. This is both good for web performance and data consumption on user devices. Any browser that supports WOFF2 fonts will also support Brotli compression (since this is the compression used in the fonts). According to the latest stats from &lt;a href=&quot;https://caniuse.com/brotli&quot;&gt;caniuse.com&lt;/a&gt;, the Brotli &lt;code&gt;Accept-Encoding&lt;/code&gt;/&lt;code&gt;Content-Encoding&lt;/code&gt; functionality is supported by 95% of user browsers globally, and I expect this figure will actually be bigger if we only consider UK based users:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of browser support for the brotli compression header.&quot; decoding=&quot;async&quot; height=&quot;278&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/5Ssh1-u627-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/5Ssh1-u627-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/5Ssh1-u627-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/5Ssh1-u627-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This compression has been enabled on both integration and staging. We saw the following savings in terms of total page bytes for HTML, CSS, and JavaScript across the frontend applications of GOV.UK:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Page&lt;/th&gt;&lt;th&gt;Application&lt;/th&gt;&lt;th&gt;Before (b)&lt;/th&gt;&lt;th&gt;After (b)&lt;/th&gt;&lt;th&gt;Diff (%)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;/&lt;/td&gt;&lt;td&gt;frontend&lt;/td&gt;&lt;td&gt;259,371&lt;/td&gt;&lt;td&gt;248,653&lt;/td&gt;&lt;td&gt;-4.13&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/coronavirus&lt;/td&gt;&lt;td&gt;info-frontend&lt;/td&gt;&lt;td&gt;288,650&lt;/td&gt;&lt;td&gt;272,687&lt;/td&gt;&lt;td&gt;-5.53&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/browse/driving/driving-licences&lt;/td&gt;&lt;td&gt;collections&lt;/td&gt;&lt;td&gt;278,385&lt;/td&gt;&lt;td&gt;265,832&lt;/td&gt;&lt;td&gt;-4.51&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/contact&lt;/td&gt;&lt;td&gt;feedback&lt;/td&gt;&lt;td&gt;242,047&lt;/td&gt;&lt;td&gt;232,124&lt;/td&gt;&lt;td&gt;-4.10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/search/all&lt;/td&gt;&lt;td&gt;finder-frontend&lt;/td&gt;&lt;td&gt;313,400&lt;/td&gt;&lt;td&gt;295,979&lt;/td&gt;&lt;td&gt;-5.56&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/bank-holidays&lt;/td&gt;&lt;td&gt;frontend&lt;/td&gt;&lt;td&gt;263,686&lt;/td&gt;&lt;td&gt;251,555&lt;/td&gt;&lt;td&gt;-4.60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/national-minimum-wage-rates&lt;/td&gt;&lt;td&gt;government-frontend&lt;/td&gt;&lt;td&gt;266,699&lt;/td&gt;&lt;td&gt;254,073&lt;/td&gt;&lt;td&gt;-4.73&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/info/coronavirus/business-support&lt;/td&gt;&lt;td&gt;info-frontend&lt;/td&gt;&lt;td&gt;233,133&lt;/td&gt;&lt;td&gt;226,382&lt;/td&gt;&lt;td&gt;-2.90&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/licence-finder/sectors&lt;/td&gt;&lt;td&gt;licence-finder&lt;/td&gt;&lt;td&gt;242,555&lt;/td&gt;&lt;td&gt;234,969&lt;/td&gt;&lt;td&gt;-3.13&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/guidance/immigration-rules&lt;/td&gt;&lt;td&gt;manuals-frontend&lt;/td&gt;&lt;td&gt;250,431&lt;/td&gt;&lt;td&gt;240,763&lt;/td&gt;&lt;td&gt;-3.86&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/service-manual/service-standard&lt;/td&gt;&lt;td&gt;service-manual-frontend&lt;/td&gt;&lt;td&gt;254,529&lt;/td&gt;&lt;td&gt;245,881&lt;/td&gt;&lt;td&gt;-3.40&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/additional-commodity-code/y&lt;/td&gt;&lt;td&gt;frontend&lt;/td&gt;&lt;td&gt;250,664&lt;/td&gt;&lt;td&gt;240,974&lt;/td&gt;&lt;td&gt;-3.87&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;/government/people/theresa-may&lt;/td&gt;&lt;td&gt;whitehall&lt;/td&gt;&lt;td&gt;277,888&lt;/td&gt;&lt;td&gt;265,642&lt;/td&gt;&lt;td&gt;-4.41&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;These savings for the pages tested converted into the following performance improvements. All tested on a simulated Moto G4 mobile on a 3G connection.&lt;/p&gt;&lt;h3 id=&quot;homepage&quot;&gt;Homepage&lt;/h3&gt;&lt;p&gt;The visual progress of the homepage has improved by approximately 110 ms as can be seen in the visual progress graph:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Visual progress of the homepage using both compression methods.&quot; decoding=&quot;async&quot; height=&quot;361&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/L6xR2g4aPR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/L6xR2g4aPR-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/L6xR2g4aPR-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/L6xR2g4aPR-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;And we see the a reduction in bytes for all the expected assets:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;bytes for assets compared on the GOV.UK homepage&quot; decoding=&quot;async&quot; height=&quot;540&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/j64OzEPEK--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/j64OzEPEK--300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/j64OzEPEK--600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/j64OzEPEK--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;coronavirus-page&quot;&gt;Coronavirus page&lt;/h3&gt;&lt;p&gt;The visual progress graph for this page starts off worse by 400ms, but quickly catches up and the viewport for Brotli completes rendering 200ms before gzip:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;visual progress graph for the covid page.&quot; decoding=&quot;async&quot; height=&quot;361&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/IQfZWtk_Rl-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/IQfZWtk_Rl-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/IQfZWtk_Rl-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/IQfZWtk_Rl-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;And we see the a reduction in bytes for all the expected assets:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;bytes for assets compared on the coronavirus page&quot; decoding=&quot;async&quot; height=&quot;541&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/B2cefoylrh-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/B2cefoylrh-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/B2cefoylrh-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/B2cefoylrh-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Showing a 5.53% reduction.&lt;/p&gt;&lt;h3 id=&quot;bank-holidays&quot;&gt;Bank holidays&lt;/h3&gt;&lt;p&gt;The visual progress of the bank holidays has improved by approximately 200ms as can be seen in the visual progress graph:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Visual progress of the bank holidays using both compression methods.&quot; decoding=&quot;async&quot; height=&quot;360&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/vyD1PRGA_u-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/vyD1PRGA_u-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/vyD1PRGA_u-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/vyD1PRGA_u-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;And we see the a reduction in bytes for all the expected assets:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;bytes for assets compared on the bank holidays page.&quot; decoding=&quot;async&quot; height=&quot;541&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/jsqsM4irKT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/jsqsM4irKT-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/jsqsM4irKT-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/jsqsM4irKT-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Showing a 4.60% reduction.&lt;/p&gt;&lt;h3 id=&quot;search-page&quot;&gt;Search page&lt;/h3&gt;&lt;p&gt;The visual progress of the search page has improved by approximately 400ms initially, 200ms at the end. As can be seen in the visual progress graph:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Visual progress of the search page using both compression methods.&quot; decoding=&quot;async&quot; height=&quot;361&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/2YygVPcfM1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/2YygVPcfM1-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/2YygVPcfM1-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/2YygVPcfM1-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;And we see the a reduction in bytes for all the expected assets:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;bytes for assets compared for the search page&quot; decoding=&quot;async&quot; height=&quot;542&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/fiQqPPirgm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/fiQqPPirgm-300.webp 300w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/fiQqPPirgm-600.webp 600w, https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk/fiQqPPirgm-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Showing a 5.56% reduction.&lt;/p&gt;&lt;h3 id=&quot;summary-2&quot;&gt;Summary&lt;/h3&gt;&lt;p&gt;In all the pages tested across the frontend apps (17 in total) we see between 3-6% reduction in the number of bytes a browser is having to download over the network. This saving is being converted to improvements in the visual metrics a users is seeing in the browser.&lt;/p&gt;&lt;h2 id=&quot;proposal&quot;&gt;Proposal&lt;/h2&gt;&lt;p&gt;The proposal is to enable Brotli compression at the edge using the &lt;a href=&quot;https://www.fastly.com/release-notes/q3-2020#brotli&quot;&gt;Fastly Brotli Compression LA&lt;/a&gt;. Browsers that support Brotli will receive these versions of the files. Any browsers that don&#39;t will receive the gzip version of the file. This can therefore be applied using the progressive enhancement methodology. In doing so we MUST update the configuration VCL so as to automate this process. During testing it was simply enabled via the Fastly user interface.&lt;/p&gt;&lt;p&gt;We SHOULD also purge the static cache once completed as it was found during testing that certain Fastly PoP&#39;s continued to serve the uncompressed version of the file since there was a mismatch between &lt;code&gt;Accept-Encoding&lt;/code&gt; header from the browser and &lt;code&gt;Content-Encoding&lt;/code&gt; version available on the CDN. Once the cache was purged this issue was rectified. However, this issue would rectify itself over time even without the manual purging, since Rails inserts an MD5 fingerprint into the filename of each file. Once a file is changed it will have a new URL, thus invalidating the old cache.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Enabling HTTP/3 on GOV.UK</title>
    <link href="https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/" />
    <updated>2023-09-13T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable HTTP/3 on GOV.UK. It was originally posted on the &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs&quot;&gt;GOV.UK RFCs GitHub repo&lt;/a&gt;. It can be &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/blob/4d28064a477ab225864651f7c912c5aeae6c2fc1/rfc-139-enable-http3.md&quot;&gt;viewed here&lt;/a&gt;. I&#39;m posting it to my blog so as to own my own work and hopefully give it more visibility.&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;HTTP/3 is the latest version of the HTTP protocol that is currently in use in production by Facebook and Google, as well as many others. It offers web performance improvements over HTTP/2, specifically when connections with high packet loss are being used. Users on unstable mobile connections and rural areas should see the most improvement with this new protocol adoption.&lt;/p&gt;&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;/h2&gt;&lt;p&gt;According to the latest data (October 2020) by &lt;a href=&quot;https://www.measurementlab.net/&quot;&gt;MLab&lt;/a&gt;, the UK has a median download speed of 20 MBit/s and 7.2 MBit/s upload speed. However, exporting this dashboard data to a spreadsheet paints a very different picture.&lt;/p&gt;&lt;p&gt;The data shows that there are parts of the UK that have connection speeds less than 0.3 MBit/s for both download, and upload. This is roughly equivalent to a 2G connection. It&#39;s worth noting that as these are median values, many users in these areas will be seeing much lower connection speeds than this in reality. These areas have a connection speed that is 98.5% slower than the median for the whole country.&lt;/p&gt;&lt;p&gt;Users in these areas still have a need to access government services and the information that GOV.UK offers, but under these conditions it won&#39;t be a very pleasant experience. These are the users that will benefit from a transition to HTTP/3.&lt;/p&gt;&lt;h2 id=&quot;brief-history-of-http&quot;&gt;Brief history of HTTP&lt;/h2&gt;&lt;p&gt;Each iteration of the HTTP protocol has set out to improve the web performance of the previous version of the protocol.&lt;/p&gt;&lt;h3 id=&quot;http-1-0&quot;&gt;HTTP/1.0&lt;/h3&gt;&lt;p&gt;The first version of the HTTP protocol came with a number of web performance issues, a primary issue being that only a single asset could be downloaded on a Transmission Control Protocol (TCP) connection before the connection was then closed. There was no way for these connections to persist for multiple asset downloads. This resulted in a lot of wasted negotiation time to establish separate TCP connections for each asset.&lt;/p&gt;&lt;h3 id=&quot;http-1-1&quot;&gt;HTTP/1.1&lt;/h3&gt;&lt;p&gt;HTTP/1.1 fixed the persistent connections issue, allowing multiple assets to be downloaded over a single connection once established. Unfortunately this didn&#39;t fully solve the web performance issues seen with the HTTP protocol (stemming from the TCP layer).&lt;/p&gt;&lt;p&gt;Under HTTP/1.1 the browser must download 100% of an asset before it can move onto the next asset. Meaning it can only download a single asset at a time per connection, the other assets are blocked. This is called Head-of-Line Blocking (HOL Blocking). Browser vendors quickly tried to solve this issue by allowing a browser to open up to 6 TCP connections per domain, so 6 assets can be downloaded in parallel.&lt;/p&gt;&lt;p&gt;Unfortunately a number of issues still exist with this method:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Most sites have many more than 6 assets per page, so HOL blocking is still occurring in almost every page load under HTTP/1.1.&lt;/li&gt;&lt;li&gt;Each TCP connection is independent and have no knowledge of each other. All 6 are fighting for limited bandwidth which in turn can saturate the network.&lt;/li&gt;&lt;li&gt;Many websites resorted to &quot;domain sharding&quot; (hosting assets across multiple domains), to allow a browser to open up more that 6 TCP connections. This in turn exacerbated the problem mentioned in point 2.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Another solution needed to be found.&lt;/p&gt;&lt;h3 id=&quot;http-2&quot;&gt;HTTP/2&lt;/h3&gt;&lt;p&gt;HTTP/2 is a major iteration on the HTTP protocol. It started life as the &lt;a href=&quot;https://en.wikipedia.org/wiki/SPDY&quot;&gt;SPDY protocol&lt;/a&gt; created by Google, which was eventually used as the foundation for HTTP/2. One major shift in this version is the ability to multiplex stream assets over a single TCP connection.&lt;/p&gt;&lt;p&gt;Each TCP packet now includes a set of &quot;frames&quot; that contains additional meta information about each stream. Each asset is downloaded over a separate stream and then reconstructed by the browser. This allows multiple assets to be downloaded at the same time over a single TCP connection, removing the HOL blocking at the application (HTTP) layer. An example of this setup can be seen in the diagram below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An example of HTTP/2 frames within TCP packets.&quot; decoding=&quot;async&quot; height=&quot;317&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/RvZJvAeo7K-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/RvZJvAeo7K-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/RvZJvAeo7K-600.webp 600w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/RvZJvAeo7K-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Source: &lt;a href=&quot;https://calendar.perfplanet.com/2020/head-of-line-blocking-in-quic-and-http-3-the-details/&quot;&gt;&quot;Head-of-Line Blocking in QUIC and HTTP/3: The Details&quot;&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/programmingart&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Robin Marx&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Unfortunately this didn&#39;t solve all the HOL blocking issues. HTTP/2 is built upon TCP: a protocol that offers the reliable transmission of packets. TCP&#39;s job is to make sure that the packets leaving one device arrive on the other device in exactly the same order, and with no missing packets. If packet loss occurs, TCP re-transmits the packets. Packets behind the missing packet cannot be processed until retransmission occurs, so all assets being transferred on that single TCP connection are held up.&lt;/p&gt;&lt;p&gt;This is where we are currently at with GOV.UK. The vast majority of users on strong, stable connections will have seen improved performance from enabling HTTP/2. Unfortunately, some users may have seen performance drop if they use a connection with high packet loss. This is due to the HOL blocking that now exists at the network layer. This is a primary issue HTTP/3 sets out to solve.&lt;/p&gt;&lt;h3 id=&quot;http-3&quot;&gt;HTTP/3&lt;/h3&gt;&lt;p&gt;To solve the HOL blocking issue at the network layer, HTTP/3 uses a completely new transport layer protocol called QUIC. QUIC initially stood for &quot;Quick UDP Internet Connections&quot;, but the IETF now don&#39;t use it as an acronym. It is simply the name of the protocol. QUIC is built upon the User Datagram Protocol (UDP), so isn&#39;t bound by the restrictions of TCP. It has been designed so that the network layer now understands the the concept of independent streams, which was restricted to the application layer under HTTP/2:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Difference between TCP and QUIC when downloading assets.&quot; decoding=&quot;async&quot; height=&quot;481&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/IosdaYFZgQ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/IosdaYFZgQ-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/IosdaYFZgQ-600.webp 600w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/IosdaYFZgQ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the illustration above, if packet 2 is lost (using HTTP/2), packets 3-8 are blocked until retransmission occurs. Using QUIC if packet 2 is lost only packets 1 and 3 are blocked since they exist in the same stream. The other streams can progress as normal since they are fully independent. QUIC doesn&#39;t require in-order packet transmission across the whole connection, but it does still retain ordering within each resource stream.&lt;/p&gt;&lt;h2 id=&quot;the-effect-of-packet-loss&quot;&gt;The effect of packet loss&lt;/h2&gt;&lt;p&gt;Using WebPageTest we can roughly simulate the effect that packet loss has on performance for our users. For testing I picking a fairly content heavy page like the &lt;a href=&quot;https://www.gov.uk/government/history/past-prime-ministers&quot;&gt;past Prime Ministers&lt;/a&gt; page and a low spec Moto G4 Android device. For the connection speed I&#39;ve picked on one of the worse case scenarios from the &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1UTRucwLY2VC4944-cKunL9yUPJkjDKXDdIPk6GY3610/edit#gid=0&quot;&gt;MLab data&lt;/a&gt;. I&#39;m emulating the connection the village of Colintraive has, which reportedly has a 450/230 Kbps connection speed. This is a connection speed equivalent to a 2G or slow 3G connection.&lt;/p&gt;&lt;h3 id=&quot;visual-filmstrip&quot;&gt;Visual filmstrip&lt;/h3&gt;&lt;p&gt;Below we can see a comparison of the page load under different packet loss conditions using HTTP/2. The top is the baseline, with 0% packet loss, progressing all the way down to 3% packet loss:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Comparing the visual filmstrip of the effect of packet loss on a page load&quot; decoding=&quot;async&quot; height=&quot;283&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/YHPkd15I_l-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/YHPkd15I_l-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/YHPkd15I_l-600.webp 600w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/YHPkd15I_l-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see, the visually complete metric sits at 9.5 seconds with no packet loss, but jumps to 13.5 seconds with 3% packet loss. This is a 42% increase.&lt;/p&gt;&lt;h3 id=&quot;visual-progress-graph&quot;&gt;Visual progress graph&lt;/h3&gt;&lt;p&gt;The visual progress graph clearly shows the effect packet loss has on page load times. As the packet loss increases, the page load slows down:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The visual progress graph comparing the percentage completeness of a page over time.&quot; decoding=&quot;async&quot; height=&quot;360&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/kVxIdBMbyO-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/kVxIdBMbyO-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/kVxIdBMbyO-600.webp 600w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/kVxIdBMbyO-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;HTTP/3 + QUIC is designed to help improve the page load speed for users experiencing these connection conditions in the real world.&lt;/p&gt;&lt;h3 id=&quot;connection-quality-measurement&quot;&gt;Connection quality measurement&lt;/h3&gt;&lt;p&gt;Is it possible to measure the connection quality that our users are experiencing when visiting GOV.UK. Fastly provide a &lt;a href=&quot;https://developer.fastly.com/reference/vcl/variables/client-connection/&quot;&gt;range of Varnish Configuration Language (VCL) options&lt;/a&gt; that we can use to monitor the quality of user connections should we wish to capture this data:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-ploss/&quot;&gt;client.socket.ploss&lt;/a&gt; - An estimate of the packet loss on the current connection. Provides a ratio from 0-1. 0 = no loss.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-tcpi-rtt/&quot;&gt;client.socket.tcpi_rtt&lt;/a&gt; - The round trip time (RTT) for the current TCP connection to the client. RTT is used for loss detection and congestion control across the connection.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-tcpi-rttvar/&quot;&gt;client.socket.tcpi_rttvar&lt;/a&gt; - Measures the variance in RTT of the connection. The higher the variance value, the less stable the connection.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-tcpi-delivery-rate/&quot;&gt;client.socket.tcpi_delivery_rate&lt;/a&gt; - Gives a recent value for the delivery bandwidth to the client in bytes per second.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;our-users&quot;&gt;Our Users&lt;/h2&gt;&lt;p&gt;Since HTTP/3 + QUIC offers better performance for users on unstable connections, it is essentially fixing performance for those who were left behind during the transition to HTTP/2. These users are likely to be those who are in the most need of this performance increase.&lt;/p&gt;&lt;p&gt;Since COVID-19 many people have had to work from home. Many will be in rural areas where high-speed internet access either isn&#39;t available, or is expensive. In more populated area many users could still be on ageing cable / DSL connections with &lt;a href=&quot;https://www.broadbandchoices.co.uk/guides/broadband/what-is-contention-ratio&quot;&gt;high contention ratios&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The aim of this transition to HTTP/3 is to allow an ever more diverse level of user connection quality to access GOV.UK, essentially maximising access for all users, no matter what connection they use. By focusing on the long-tail of our users with poor connections, we can effectively shorten it and in doing so make the internet &quot;good enough&quot; to use in more situations. This is all without effecting performance for users already on a good internet connection.&lt;/p&gt;&lt;h3 id=&quot;browser-support&quot;&gt;Browser support&lt;/h3&gt;&lt;p&gt;The support for HTTP/3 + QUIC in browsers is visible on &lt;a href=&quot;https://caniuse.com/?search=http3&quot;&gt;Caniuse&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;HTTP/3 and QUIC browser usage screenshot from CanIUse.com.&quot; decoding=&quot;async&quot; height=&quot;296&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/QL4CkVb8Rf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/QL4CkVb8Rf-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/QL4CkVb8Rf-600.webp 600w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/QL4CkVb8Rf-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The browsers support for HTTP/3 at the time of writing is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Chrome - Enabled (see &lt;a href=&quot;https://blog.chromium.org/2020/10/chrome-is-deploying-http3-and-ietf-quic.html&quot;&gt;Chromium blog&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Edge (chromium version) - Enabled&lt;/li&gt;&lt;li&gt;Firefox - Enabled in the next version (88)&lt;/li&gt;&lt;li&gt;Safari 14 - Disabled (currently behind a feature flag)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Looking at the March 2021 analytics, approximately 55% of GOV.UK users are using a browser that can now take advantage of HTTP/3 + QUIC. This is close to the 66% that the CanIUse data estimates for HTTP/3 usage globally.&lt;/p&gt;&lt;h2 id=&quot;usage-on-the-web&quot;&gt;Usage on the web&lt;/h2&gt;&lt;p&gt;Although the HTTP/3 and QUIC specifications are still in draft, they are both used in production on large sites with billions of daily page views:&lt;/p&gt;&lt;h3 id=&quot;youtube&quot;&gt;YouTube&lt;/h3&gt;&lt;p&gt;In the image below we see Chrome 89 (latest stable) connecting to YouTube and using &lt;code&gt;h3-Q050&lt;/code&gt;. This is HTTP/3 with the latest version of Google QUIC (&lt;code&gt;Q050&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;YouTube using HTTP/3 + QUIC as the transport protocol.&quot; decoding=&quot;async&quot; height=&quot;613&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/sQSHA0WvMt-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/sQSHA0WvMt-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/sQSHA0WvMt-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;google-search&quot;&gt;Google Search&lt;/h3&gt;&lt;p&gt;In the image below we see Chrome 89 (latest stable) connecting to Google Search and using &lt;code&gt;h3-Q050&lt;/code&gt;. This is HTTP/3 with the latest version of Google QUIC (&lt;code&gt;Q050&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Google Search using HTTP/3 and QUIC.&quot; decoding=&quot;async&quot; height=&quot;375&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/LE-twq1SfC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/LE-twq1SfC-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/LE-twq1SfC-600.webp 600w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/LE-twq1SfC-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;facebook&quot;&gt;Facebook&lt;/h3&gt;&lt;p&gt;Facebook are using HTTP/3 and QUIC in production, but are using the IETF version of QUIC (&lt;code&gt;h3-29&lt;/code&gt;). Below we see Firefox 88 connecting via HTTP/3:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Facebook servers are also HTTP/3 and QUIC enabled.&quot; decoding=&quot;async&quot; height=&quot;500&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/3p80k65Uwv-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/3p80k65Uwv-300.webp 300w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/3p80k65Uwv-600.webp 600w, https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk/3p80k65Uwv-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;other-advantages-to-http-3-quic&quot;&gt;Other advantages to HTTP/3 + QUIC&lt;/h2&gt;&lt;p&gt;I&#39;ve only covered a single advantage related to packet loss with web performance above, but HTTP/3 + QUIC comes with a number of other advantages:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Improvement in connection startup times via TLS v1.3 + &lt;a href=&quot;https://blog.cloudflare.com/introducing-0-rtt/&quot;&gt;0-RTT&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Better security via end-to-end encryption at the transport layer&lt;/li&gt;&lt;li&gt;Establish an ecosystem that is easier to evolve from our current TCP usage&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Eventually our users will also be able to take advantage of &lt;a href=&quot;https://tools.ietf.org/html/draft-paulo-quic-migration-00&quot;&gt;Connection Migration&lt;/a&gt;, where QUIC end-points will be able to migrate connections to different IP addresses and network paths at will.&lt;/p&gt;&lt;p&gt;Connection migration is an optional part of the specification that isn&#39;t currently implemented anywhere. Once enabled it will allow a users device to seamlessly transition between multiple QUIC connections (e.g. Cellular to known Wi-fi). This is achieved by introducing the concept of a connection ID that is carried by QUIC packets, which can be used to identify a connection.&lt;/p&gt;&lt;p&gt;A user is likely to find this useful if they are live streaming data (web conferencing, large file upload / download, real-time navigation etc). A live demo of this technology in cation can be &lt;a href=&quot;https://www.youtube.com/watch?v=In1aALwdahY&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Since this is optional in the QUIC specifications, realistically its practical adoption may be a number of years away.&lt;/p&gt;&lt;h2 id=&quot;summary-2&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Our move to HTTP/3 + QUIC offers web performance gains to users on unstable, high packet loss connections. These users were left behind in the migration to HTTP/2 across the web platform. These web performance gains will come with no loss in performance to users already on a strong, stable connection. All our users will have an improvement in security, as well as improvements in connection startup times.&lt;/p&gt;&lt;p&gt;It has never been more important to fix issues for our long-tail users, since many will now be working from home due to the &lt;a href=&quot;https://www.gov.uk/coronavirus&quot;&gt;Coronavirus (COVID‑19)&lt;/a&gt;. Users no longer working in an office environment may now be limited to unstable connections in rural areas, or tethering via limited mobile data plans, or using a connection in populated inner-city area with a high local contention ratios.&lt;/p&gt;&lt;p&gt;Any browsers that don&#39;t support HTTP/3 + QUIC will fallback to HTTP/2 + TCP. Therefore HTTP/3 can be enabled using the progressive enhancement methodology.&lt;/p&gt;&lt;h2 id=&quot;proposal&quot;&gt;Proposal&lt;/h2&gt;&lt;p&gt;Fastly have now announced HTTP/3 + QUIC is available to all customers as it is out of &lt;a href=&quot;https://www.fastly.com/blog/http-3-and-quic-are-now-available-for-our-entire-customer-base-at-no-additional-charge&quot;&gt;Limited Availability (LA) and the beta phases&lt;/a&gt;. Enabling HTTP/3 + QUIC for us involves switching it on via the Fasty UI. No modifications are needed to our application code, or servers. And since we have already invested work in optimising for HTTP/2, we are already well prepared for a migration to HTTP/3. So no modifications are needed to our frontend applications.&lt;/p&gt;&lt;p&gt;There is a CNAME config change required if we don&#39;t use a dedicated Fastly IP address once enabled in the UI. From the &lt;a href=&quot;https://docs.fastly.com/en/guides/enabling-http3-for-fastly-services#sending-your-http3-traffic-to-fastly&quot;&gt;documentation&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Unless you use Fastly&#39;s dedicated IP addresses, then as a final step to enabling HTTP/3, you must ensure the DNS records of your domains are routing users to the correct HTTP/3 enabled Fastly addresses by using one of the following CNAMEs:&lt;/p&gt;&lt;/blockquote&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;0-RTT enabled?&lt;/th&gt;&lt;th&gt;IPv4 and IPv6 dual stack routing&lt;/th&gt;&lt;th&gt;IPv4-only routing&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;code&gt;dualstack.n.sni.global.fastly.net&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;n.sni.global.fastly.net&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;&lt;code&gt;dualstack.m.sni.global.fastly.net&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;m.sni.global.fastly.net&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;I know GOV.UK currently has IPv6 enabled, so if a CNAME change is required I propose we use 0-RTT = No (&lt;code&gt;dualstack.m.sni.global.fastly.net&lt;/code&gt;), since I belive Firefox is still the only browser that supports it. We can always enable this at a later date quite easlily once it becomes more promenant in browsers.&lt;/p&gt;&lt;p&gt;Note: According to Richard Towers, we currently don&#39;t use dedicated Fastly IP&#39;s so the CNAME change will be required, this shouldn&#39;t be a major issue because we have a couple of levels of CNAME in place (we don&#39;t need to talk to JISC). It&#39;s also worth noting that TTL on prod is 300 seconds (5 mins), which should be short enough for the change when made.&lt;/p&gt;&lt;h2 id=&quot;date-to-be-enabled&quot;&gt;Date to be enabled&lt;/h2&gt;&lt;p&gt;I&#39;ve opened an additional &lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/blob/103acd22a74b32d4ea9dd321bc0ab05ac664aa82/rfc-147-enable-speedcurve-http-protocol-capture.md&quot;&gt;RFC-147&lt;/a&gt;, that will allow us to distinguish users using HTTP/2 and HTTP/3 in SpeedCurve RUM. I believe we should impliment this RFC first then allow it to capture user data for 1-2 months, before we enable HTTP/3. This will allow us to quantify the before / after change in our RUM metrics, which would make for an interesting PR piece from a web performance and user focus point of view. So I&#39;d propose to enable HTTP/3 some time in July / August 2022.&lt;/p&gt;&lt;h2 id=&quot;http-3-enabled-on-gov-uk&quot;&gt;HTTP/3 enabled on GOV.UK!&lt;/h2&gt;&lt;p&gt;I&#39;m very pleased to say that HTTP/3 + QUIC from Fastly was enabled on GOV.UK on the 11th January 2024! See the tweet thread &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1745843848809484419&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;here&lt;/a&gt; for more information.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Migrating from Github Pages to Netlify &amp; Cloudflare</title>
    <link href="https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/" />
    <updated>2021-09-06T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/</id>
    <content type="html">&lt;p&gt;So in recent months I&#39;ve had a &lt;a href=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/&quot;&gt;fair bit going on in my life&lt;/a&gt; which has held me back in many ways, but also pushed a few things up the priority list too. One of the areas that has been bumped up the list is the current setup of this very blog. So I decided to clean up the years of &quot;it still works, why would I change it&quot;, technical debt. Isn&#39;t it incredible how many years you can do this for!&lt;/p&gt;&lt;h2 id=&quot;dreamhost&quot;&gt;Dreamhost&lt;/h2&gt;&lt;p&gt;Just a very brief look back on the history of the blog and allow me to vent a little. Feel free to jump to the &lt;a href=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/#github-pages&quot;&gt;next section&lt;/a&gt; if this is of no interest to you.&lt;/p&gt;&lt;p&gt;I started using Dreamhost for my hosting &amp; domain registrar back in April 2006, just over 15 years ago! At the time this was because they offered a lot in terms of functionality all rolled into one. You could easily purchase your domain and set up the hosting quickly. So this became my default option for friends and families websites. The initial version of the blog used WordPress (~v2.0). At the time this was a great way to learn how to work with PHP/MySQL, and WordPress felt like the only real quick ‘self-hosted&#39; option available (I&#39;m sure many will disagree with that statement).&lt;/p&gt;&lt;p&gt;Over the years my Dreamhost control panel just filled with random domains and projects that weren&#39;t maintained. So in the past 3 months I decided to clean this all up. Remove all the old unused domains, and migrate any friends&#39; websites over to other hosting options, so I&#39;d no longer need to pay for a service I really don&#39;t need. My plan was to try and get friends to open their own Dreamhost account and basically get the Dreamhost team to do an ‘internal transfer&#39; between accounts. Simple right?!&lt;/p&gt;&lt;p&gt;So I contacted Dreamhost telling them my current situation and my plan. Their response was basically: &quot;Sorry to hear of your situation...sure we can help you do that… for this one time admin fee of $99.00 USD...&quot;. Isn&#39;t capitalism great! Not that I&#39;d expect it to be free, but after 15 years and the fact that I&#39;m trying to get friends to sign up and pay for their services moving forwards, you&#39;d have thought they would have the foresight to waive the fee.&lt;/p&gt;&lt;p&gt;I neither had the drive or the mental capacity to argue the point so left it at that and decided that Dreamhost isn&#39;t a company I want to be dealing with anymore, especially not with all the other options available on the market now! So time to transfer everything away, including them as a domain registrar, which is pretty much all it&#39;s been used for with this blog for many years anyway.&lt;/p&gt;&lt;h2 id=&quot;github-pages&quot;&gt;Github pages&lt;/h2&gt;&lt;p&gt;This blog is built using the static site generator called &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt;. It works okay, but can be a bit of a dependency hell when updating as it is all built on Ruby. The great thing about this setup is that it integrates really well with Github (GH) Pages. There&#39;s a whole guide on how to set up a new site really quickly &lt;a href=&quot;https://pages.github.com/&quot;&gt;here&lt;/a&gt;. So this is what I did for many years. I didn&#39;t even bother setting up a custom domain, the blog just existed on &lt;code&gt;nooshu.github.io&lt;/code&gt; (more on this later). Once configured, just write your posts in &lt;a href=&quot;https://daringfireball.net/projects/markdown/&quot;&gt;Markdown&lt;/a&gt;, push your changes to the repository and GH pages will handle the rest.&lt;/p&gt;&lt;p&gt;The downside with this setup is:&lt;/p&gt;&lt;p&gt;It&#39;s fairly limited. You can only use specific Jekyll plugins supported by GH pages. So you can&#39;t take advantage of all plugins available. It&#39;s a paid for only option if using a private repository (which my blog is). I don&#39;t want to change this restriction.&lt;/p&gt;&lt;p&gt;In all honesty it&#39;s mainly the paid for option that I&#39;m looking to remove. Not to be too blunt about it all, but should anything happen in the coming months, future payments wouldn&#39;t be made and the blog would soon be taken offline.&lt;/p&gt;&lt;h2 id=&quot;netlify&quot;&gt;Netlify&lt;/h2&gt;&lt;p&gt;This is where &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt; steps in. Netlify is a Git-based workflow and powerful serverless platform that can be used to build, deploy, and collaborate on web apps. It comes with a fantastic ‘Starter&#39; plan for personal projects, hobby sites, or experiments. I can&#39;t even tell you how simple it is to set up a site if you&#39;re already using Jekyll. Literally point Netlify towards your site repository, a minute or so later you&#39;ll have yourself an exact copy of the site (although on a randomly assigned URL, but we will get to this part soon). If this hasn&#39;t convinced you to give it a try, it is also been supported and maintained by a fantastic bunch of amazing developers like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/sarah_edo&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Sarah Drasner&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/zachleat&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Zach Leatherman&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/philhawksworth&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Phil Hawksworth&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now technically you &lt;em&gt;could&lt;/em&gt; pretty much stop there. Let Netlify handle the custom domain, DNS, Hosting, SSL, deployment etc as &lt;a href=&quot;https://www.netlify.com/blog/2017/03/28/why-you-dont-need-cloudflare-with-netlify/&quot;&gt;you don&#39;t actually need Cloudflare&lt;/a&gt;. But where&#39;s the fun in that?? So let&#39;s layer on Cloudflare anyway!&lt;/p&gt;&lt;h2 id=&quot;cloudflare&quot;&gt;Cloudflare&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://www.cloudflare.com/&quot;&gt;Cloudflare&lt;/a&gt; are one of the big market leaders when it comes to Content Distribution Networks (CDN) on the web. But don&#39;t let their huge size put you off, they still have an excellent &lt;a href=&quot;https://www.cloudflare.com/plans/&quot;&gt;free plan&lt;/a&gt; that anyone can use to speed up their website. It&#39;s incredible the features they offer on their free plan:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/http3-the-past-present-and-future/&quot;&gt;HTTP/3 + QUIC&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/introducing-0-rtt/&quot;&gt;0-RTT&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.cloudflare.com/en-gb/features-page-rules/&quot;&gt;Page Rules&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There really is so much there for a standard website or a hobbyist looking to do some &lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/&quot;&gt;web performance experimentation&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Another bonus is that they can also act as a &lt;a href=&quot;https://www.cloudflare.com/en-gb/products/registrar/&quot;&gt;domain registrar&lt;/a&gt; either for new domains, or to transfer existing domains to them. So this is what I&#39;ve done. My Dreamhost domains have now all been transferred to Cloudflare and are all in a single place.&lt;/p&gt;&lt;h2 id=&quot;setting-it-all-up&quot;&gt;Setting it all up&lt;/h2&gt;&lt;p&gt;Enough of the babbling, let&#39;s dig into setting it all up!&lt;/p&gt;&lt;h3 id=&quot;domains&quot;&gt;Domains&lt;/h3&gt;&lt;p&gt;First thing I did was to start the domain transfer process. Dead simple to do. Make sure the domain is unlocked, grab the auth code from the previous registrar then wait 5 days… It&#39;s annoying that you wait 5 days for this but Dreamhost doesn&#39;t give you an option to bypass the ‘cooling off&#39; period. It would be great if there was an option to start the transfer process right away, but alas it is what it is. I did this via &lt;a href=&quot;https://www.cloudflare.com/en-gb/products/registrar/&quot;&gt;Cloudflare Registrar&lt;/a&gt;, but any will work in mostly the same way.&lt;/p&gt;&lt;h3 id=&quot;netlify-2&quot;&gt;Netlify&lt;/h3&gt;&lt;p&gt;As mentioned above, Netlify is so simple to get setup. Click the &quot;New site from git&quot; button, select your repo and configure your build settings.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;New site from Git button image on Netlify.&quot; decoding=&quot;async&quot; height=&quot;88&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/unXqKJjVi4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/unXqKJjVi4-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/unXqKJjVi4-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/unXqKJjVi4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Clicking the button will bring up the following options:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Create a new site dialog with Netlify.&quot; decoding=&quot;async&quot; height=&quot;513&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/73LxxQo4mK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/73LxxQo4mK-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/73LxxQo4mK-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/73LxxQo4mK-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;A few minutes later you&#39;ll have a new site on a random Netlify URL that you can easily access.&lt;/p&gt;&lt;h3 id=&quot;ssl-certificate-dns&quot;&gt;SSL Certificate + DNS&lt;/h3&gt;&lt;p&gt;This is where it got slightly more complicated. By default you could leave Netlify to handle the DNS settings and SSL certificate, but since we want to front our site with Cloudflare we have to make a few changes.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;SSL Certificate&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;So we aren&#39;t going to use Netlify to provision the SSL certificate, we are going to be using Cloudflare. This is important if you want to use the CDN functionality of Cloudflare. As it is the &quot;front&quot; of the website, we are going to be using Netlify as the origin server.&lt;/p&gt;&lt;p&gt;An SSL certificate provisioned by cloudflare will allow encryption of traffic between the origin (Netlify) and the CDN (Cloudflare). I &lt;em&gt;believe&lt;/em&gt; this is also important when it comes to using the HTTP/3 + QUIC functionality that Cloudflare offers on their free plan. This is because TLS is very much &lt;a href=&quot;https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6ef36d1e-d91e-43e0-8732-f3e66ba9ea64/protocol-stack-h2-h3.png&quot;&gt;embedded within the QUIC protocol&lt;/a&gt;, rather than a separate layer like it is in HTTP/2. So if Netlify were to handle the SSL certificate, it would limit our ability to use HTTP/3. &lt;strong&gt;Note&lt;/strong&gt;: this is just an educated guess of mine. As I could only get HTTP/3 + QUIC to work when Cloudflare provisioned the SSL certificate.&lt;/p&gt;&lt;p&gt;So first you need to generate an origin certificate in the Cloudflare control panel for your domain:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Create origin certificate in Cloudflare.&quot; decoding=&quot;async&quot; height=&quot;339&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/3_mHjSCQId-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/3_mHjSCQId-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/3_mHjSCQId-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/3_mHjSCQId-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I enabled Full (strict) mode here that requires the Cloudflare CA certificate.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Setting the Cloudflare to Full (strict) SSL settings.&quot; decoding=&quot;async&quot; height=&quot;538&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/pgBcagzcfk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/pgBcagzcfk-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/pgBcagzcfk-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/pgBcagzcfk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Next you need to enter this certificate info into Netlify via the &quot;Site settings → Domain Management → HTTPS&quot;. Click the ‘Add (or Update) custom certificate&#39;.&lt;/p&gt;&lt;p&gt;Install your custom certificate by copy &amp; pasting the corresponding fields into the textareas provided. You will also need to provide the ‘intermediate&#39; certificate details. For this, go to the &lt;a href=&quot;https://developers.cloudflare.com/ssl/origin-configuration/&quot;&gt;Cloudflare Origin configuration page&lt;/a&gt; and download the &lt;a href=&quot;https://developers.cloudflare.com/ssl/origin-configuration/origin-ca#4-required-for-some-add-cloudflare-origin-ca-root-certificates&quot;&gt;Cloudflare Origin RSA PEM&lt;/a&gt; file and open it in your favorite text editor. Copy &amp; paste the contents into the last textarea.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Install custom certificate dialog in Netlify.&quot; decoding=&quot;async&quot; height=&quot;820&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/WYj1n9VOAS-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/WYj1n9VOAS-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/WYj1n9VOAS-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/WYj1n9VOAS-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once completed your HTTPS settings in Netlify should look similar to this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;HTTPS settings in Netlify once the certificate is installed.&quot; decoding=&quot;async&quot; height=&quot;465&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lB2ZCArWBH-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lB2ZCArWBH-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lB2ZCArWBH-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lB2ZCArWBH-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;That&#39;s about it for SSL certificates. You&#39;ve now installed the Cloudflare origin certificate on your origin server (Netlify) so they can talk to each other over an encrypted connection.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;DNS&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Okay, here&#39;s the part that I don&#39;t mind admitting took me bloody ages to figure out! When it comes to Domain Name System (DNS) I know enough to be dangerous, but not enough to be useful! So there was a fair amount of head scratching and trial and error to get this all setup. It doesn&#39;t help that these settings take a while to propagate across the internet. So you never know if they are still propagating, or if you just broke something! For ease, here are the settings I currently have in place in my Cloudflare DNS panel:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;DNS record settings in Cloudflare.&quot; decoding=&quot;async&quot; height=&quot;195&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/QhD_vCR6Yb-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/QhD_vCR6Yb-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/QhD_vCR6Yb-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/QhD_vCR6Yb-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;First we have an &lt;code&gt;A&lt;/code&gt; record pointing to Netlify&#39;s load balancer IP address (&lt;code&gt;75.2.60.5&lt;/code&gt;). More info on this is available on their &quot;&lt;a href=&quot;https://docs.netlify.com/domains-https/custom-domains/configure-external-dns/#configure-an-apex-domain&quot;&gt;Configure external DNS for a custom domain&lt;/a&gt;&quot; page.&lt;/p&gt;&lt;p&gt;Second we have a &lt;code&gt;CNAME&lt;/code&gt; record pointing &lt;code&gt;www&lt;/code&gt; to &lt;code&gt;nooshu.com&lt;/code&gt;. I don&#39;t want the ‘www&#39; in my URL, so it is just being pointed to the bare domain (&lt;code&gt;https://nooshu.com&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;Note that for both of these records the ‘Proxy Status&#39; column is set to ‘Proxied&#39; (notice the orange cloud). This is important as we want to make sure the hostname is running through Cloudflare as our CDN provider.&lt;/p&gt;&lt;p&gt;Under this panel I pointed the domain to the Cloudflare Nameservers (&lt;code&gt;braelyn.ns.cloudflare.com&lt;/code&gt; and &lt;code&gt;cartman.ns.cloudflare.com&lt;/code&gt;). Finally in the DNS page I enabled &lt;a href=&quot;https://www.icann.org/resources/pages/dnssec-what-is-it-why-important-2019-03-05-en&quot;&gt;DNSSEC&lt;/a&gt;. This isn&#39;t required, but it adds a layer of extra security by protecting against forged DNS answers. It comes for free with the free plan, so may as well I guess! (Unless anyone knows otherwise?)&lt;/p&gt;&lt;p&gt;That&#39;s it for the Cloudflare DNS page! A little word or warning: if you go back to the Netlify ‘Domain Management&#39; panel you will see some scary looking warning messages next to your custom domains.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Netlify DNS settings with errors that can be ignored.&quot; decoding=&quot;async&quot; height=&quot;237&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/RHJhi8aT_V-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/RHJhi8aT_V-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/RHJhi8aT_V-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/RHJhi8aT_V-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;These are fine to ignore. It&#39;s simply because the domain&#39;s DNS isn&#39;t being handled by Netlify, it&#39;s been handled by Cloudflare.&lt;/p&gt;&lt;h3 id=&quot;netlify-forms&quot;&gt;Netlify Forms&lt;/h3&gt;&lt;p&gt;This is an optional extra. If your site requires a contact form you can&#39;t go wrong with Netlify. It comes with the ability to manage forms and submissions without any server-side code or JavaScript. It&#39;s all done via &lt;a href=&quot;https://www.netlify.com/products/forms/&quot;&gt;Netlify Forms&lt;/a&gt;. It is dead simple to set up and comes with built-in spam protection via &lt;a href=&quot;https://docs.netlify.com/forms/spam-filters/&quot;&gt;Akismet and an optional honeypot field&lt;/a&gt;. All for free (up to a certain usage limit).&lt;/p&gt;&lt;h3 id=&quot;email&quot;&gt;Email&lt;/h3&gt;&lt;p&gt;Next up is email setup. Ideally I wanted &lt;code&gt;name@domain.com&lt;/code&gt; to forward to my GMail account, but Cloudflare doesn&#39;t &lt;a href=&quot;https://community.cloudflare.com/t/cloudflare-domains-email-forwarding/197040/2&quot;&gt;handle emails at all&lt;/a&gt;. Thankfully I found an excellent free service to do this called &lt;a href=&quot;https://improvmx.com/&quot;&gt;ImprovMX&lt;/a&gt;. Simply enter the alias you require and the email address you want to forward too, then follow the instructions given. You enter all these details into your Cloudflare DNS panel. An example of my setup is below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Cloudflare DNS settings for setting up an email address.&quot; decoding=&quot;async&quot; height=&quot;125&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/jRas9_3WF--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/jRas9_3WF--300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/jRas9_3WF--600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/jRas9_3WF--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;That&#39;s all there is to that really. Quick and simple!&lt;/p&gt;&lt;h3 id=&quot;redirects&quot;&gt;Redirects&lt;/h3&gt;&lt;p&gt;This was a big concern for me at first. My blog has been on the default Github domain for a number of years (&lt;code&gt;nooshu.github.com&lt;/code&gt;). So what would happen if I suddenly changed to &lt;code&gt;nooshu.com&lt;/code&gt;? Would all the links built up over the years just break? Github pages is very limited as to what you can do technically. It&#39;s not like it uses Apache for example, where you can use something like &lt;code&gt;mod_rewrite&lt;/code&gt; to create redirects.&lt;/p&gt;&lt;p&gt;Thankfully Github does come with some useful functionality to help in this situation. Navigate to ‘Settings → Pages → Custom domain&#39; page. Enter the domain you wish to serve the site from into the input box. Behind the scenes this will create a &lt;code&gt;CNAME&lt;/code&gt; file in the root of the repository that will redirect users to the new domain.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Setting the custom domain in the Github panel.&quot; decoding=&quot;async&quot; height=&quot;100&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/vGmpv0Acjn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/vGmpv0Acjn-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/vGmpv0Acjn-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/vGmpv0Acjn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So in my case &lt;code&gt;nooshu.github.com&lt;/code&gt; redirects to &lt;code&gt;nooshu.com&lt;/code&gt; automatically. Assuming you keep the rest of your URL structure the same, you should be all good to go!&lt;/p&gt;&lt;h3 id=&quot;web-mentions&quot;&gt;Web Mentions&lt;/h3&gt;&lt;p&gt;I use &lt;a href=&quot;https://www.w3.org/TR/webmention/&quot;&gt;Webmentions&lt;/a&gt; on my blog. I actually wrote about how I do it &lt;a href=&quot;https://nooshu.com/blog/2019/11/18/implementing-webmentions/&quot;&gt;here&lt;/a&gt;. The ‘issue&#39; with using web mentions via the &lt;a href=&quot;https://webmention.io/&quot;&gt;Webmention.io service&lt;/a&gt; is that it is tied to a particular domain. But what happens once the domain is changed? Suddenly all the webmentions that were associated with &lt;code&gt;nooshu.github.io&lt;/code&gt; are no longer pulled in since the blog has moved to &lt;code&gt;nooshu.com&lt;/code&gt;. Thankfully after a brief chat with &lt;a href=&quot;https://twitter.com/kevinmarks&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Kevin Marks&lt;/a&gt; on Twitter a simple solution was found.&lt;/p&gt;&lt;p&gt;In my JavaScript I&#39;m querying for any webmentions that are associated with the &lt;code&gt;nooshu.github.io&lt;/code&gt; domain. The &lt;code&gt;webmention.io&lt;/code&gt; API allows you to query multiple domains at the same time. So I just &lt;a href=&quot;https://gist.github.com/Nooshu/521c754d1a0da7b7917351fa6b22886c#file-webmention-js-L36&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;modified my JavaScript&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; to look for mentions from both domains. This allows for mentions from the past (&lt;code&gt;nooshu.github.io&lt;/code&gt;) and also any mentions in the future (&lt;code&gt;nooshu.com&lt;/code&gt;) to be collected and parsed. Simple!&lt;/p&gt;&lt;h2 id=&quot;web-performance&quot;&gt;Web Performance&lt;/h2&gt;&lt;p&gt;This final section of the post was the really interesting part for me. How to get the best performance out of the new Netlify + Cloudflare setup. It&#39;s worth mentioning upfront that by default it really &lt;strong&gt;isn&#39;t&lt;/strong&gt; optimal. Here&#39;s the WebPageTest result for this site before any changes were made. Here I&#39;m testing on a real Moto G4 on a 3G Fast connection. So essentially a low spec device on a slow connection.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;WebPageTest score from before all the tweaks were applied to the blog.&quot; decoding=&quot;async&quot; height=&quot;160&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/TArrpAi5xk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/TArrpAi5xk-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/TArrpAi5xk-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/TArrpAi5xk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So the &lt;a href=&quot;https://www.webpagetest.org/result/210831_AiDcQR_1c8c05bd7a25ec9c714a81bc31a84296/&quot;&gt;test result&lt;/a&gt; isn&#39;t actually too bad in terms of performance. A Largest Contentful Paint (LCP) and visually complete of around 2 seconds on a low spec device is a good place to start. But there are three scores that stand out:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Security: E&lt;/li&gt;&lt;li&gt;Cache static content: F&lt;/li&gt;&lt;li&gt;First Byte: B&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;All these scores are very much intertwined, but I will go through each of them individually the best I can..&lt;/p&gt;&lt;h3 id=&quot;security&quot;&gt;Security&lt;/h3&gt;&lt;p&gt;Since this is a small static blog with no admin area or any form of login, security isn&#39;t a major issue. But it would be nice to get an ‘A&#39; in this anyway. Thankfully it really isn&#39;t that hard to do if you are using Cloudflare as a CDN. Security expert &lt;a href=&quot;https://twitter.com/Scott_Helme&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Scott Helme&lt;/a&gt; covers most of what to do in his blog post &quot;&lt;a href=&quot;https://scotthelme.co.uk/security-headers-cloudflare-worker/&quot;&gt;The brand new Security Headers Cloudflare Worker&lt;/a&gt;&quot;. If you are looking for a guide on setting up a Cloudflare Worker, I&#39;ve written a &lt;a href=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/&quot;&gt;post here&lt;/a&gt; you can follow. The issue with the score is that the site is being served with some missing security headers:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: There&#39;s no need to add these missing headers using a Worker anymore as Cloudflare has just released some new functionality called &lt;a href=&quot;https://blog.cloudflare.com/transform-http-response-headers/&quot;&gt;&quot;HTTP response headers with Transform Rules&quot;&lt;/a&gt;, see my &lt;a href=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/#using-http-response-headers-with-transform-rules&quot;&gt;update below&lt;/a&gt; on how to do this.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Security score came out as a D when automatically scanned.&quot; decoding=&quot;async&quot; height=&quot;174&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/gYllIBW4f6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/gYllIBW4f6-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/gYllIBW4f6-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/gYllIBW4f6-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see from the scan above by &lt;a href=&quot;https://securityheaders.com/&quot;&gt;securityheaders.com&lt;/a&gt;, we are missing 5 security headers (in red). By following Scott&#39;s guide and a &lt;a href=&quot;https://scotthelme.co.uk/goodbye-feature-policy-and-hello-permissions-policy/&quot;&gt;small tweak related to ‘Feature-Policy` header&lt;/a&gt; you can easily get it up to an A+. Here&#39;s the headers I have included in my Cloudflare Worker:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; securityHeaders &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;Content-Security-Policy&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;upgrade-insecure-requests&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;Strict-Transport-Security&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;max-age=31536000; preload; includeSubDomains&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;X-Xss-Protection&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1; mode=block&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;X-Frame-Options&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;DENY&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;X-Content-Type-Options&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;nosniff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;Referrer-Policy&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;strict-origin-when-cross-origin&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;Permissions-Policy&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fullscreen=(), geolocation=(), camera=()&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This code, combined with Scott&#39;s boilerplate code adds the headers to the HTML response from the CDN. Now you don&#39;t necessarily need to use a Worker to add all these headers. You could use functionality that &lt;a href=&quot;https://docs.netlify.com/routing/headers/&quot;&gt;Netlify has available&lt;/a&gt; where you can modify the headers coming from the origin using a &lt;code&gt;_headers&lt;/code&gt; or &lt;code&gt;netlify.toml&lt;/code&gt; file. But I didn&#39;t have much luck with this route. Certain headers were being stripped somewhere in the process so I eventually just reverted to using a Worker to do it. &lt;a href=&quot;http://twitter.com/SimonHearne&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Simon Hearne&lt;/a&gt; has an &lt;a href=&quot;https://simonhearne.com/2019/http-headers-fast-and-secure/#configuring-headers-on-netlify&quot;&gt;excellent blog post&lt;/a&gt; all about this, where he managed to get it to work if you are interested. The above setup will also allow you to submit your domain to the &lt;a href=&quot;https://hstspreload.org/&quot;&gt;HSTS Preload list&lt;/a&gt; too, ensuring that the domain (and all subdomains) are only accessible with a valid SSL certificate.&lt;/p&gt;&lt;p&gt;Once the worker is appending the missing headers to your HTML response the score jumps from an E to an A+! Star pupil indeed! All using a Cloudflare Worker that comes for free on the free plan. Win-win all round!&lt;/p&gt;&lt;h3 id=&quot;using-http-response-headers-with-transform-rules&quot;&gt;Using HTTP response headers with Transform Rules&lt;/h3&gt;&lt;p&gt;As mentioned above Cloudflare have just relesed functionality that allows you to easily modify response headers without the need of a Worker. It&#39;s all done via the Cloudflare UI.&lt;/p&gt;&lt;p&gt;Just select your site then navigate to &#39;Rules&#39; --&gt; &#39;Transform Rules&#39;. Then create a new set of response rules like below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Add the response headers when a GET request is seen.&quot; decoding=&quot;async&quot; height=&quot;191&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/BkYpSTnsVN-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/BkYpSTnsVN-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/BkYpSTnsVN-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/BkYpSTnsVN-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You &lt;a href=&quot;https://twitter.com/rboulton/status/1469062262061154315&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;can&#39;t currently use the expression builder to target only the HTML response&lt;/a&gt;, but you can paste in the following expression if you only want to target a specific &lt;code&gt;content-type&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;http.request.method eq &quot;GET&quot; and any(http.response.headers[&quot;content-type&quot;][*] contains &quot;text/html&quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Also worth nothing that this feature is on the list to be &lt;a href=&quot;https://twitter.com/rboulton/status/1469073236948303875&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;added to the expression builder&lt;/a&gt;. If you don&#39;t only target your HTML, your finely crafted &lt;code&gt;Cache-Control&lt;/code&gt; asset headers will be decimated and all overwritten with &lt;code&gt;public, max-age=60&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Then I&#39;ve simply added 9 static rules to the response from Cloudflare:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The 9 static response headers I&#39;ve added can be seen in this screenshot. They are listed as text below if required.&quot; decoding=&quot;async&quot; height=&quot;409&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/C4B8P_in7n-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/C4B8P_in7n-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/C4B8P_in7n-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/C4B8P_in7n-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The headers I added can be found below, the top 8 bump the security score from E to A+ on &lt;a href=&quot;https://snyk.io&quot;&gt;Snyk.io&lt;/a&gt;. The final header is a specific preload header for this blog and can be ignored.&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/** Important security related headers**/
Cache-Control: public, max-age=60
Content-Security-Policy : upgrade-insecure-requests
Strict-Transport-Security : max-age=2592000
X-Xss-Protection : 1; mode=block
X-Frame-Options : DENY
X-Content-Type-Options : nosniff
Referrer-Policy : strict-origin-when-cross-origin
Permissions-Policy: fullscreen=(), geolocation=(), camera=()

/**Specific preload for my blog**/
Link: ; rel=preload; as=image; nopush, ; rel=preload; as=image; nopush&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once this is done you can deploy and strip all the headers from the worker that exist in the &lt;code&gt;securityHeaders&lt;/code&gt; object in the Worker code. Done!&lt;/p&gt;&lt;p&gt;Many thanks to the Cloudflare team who quickly removed a restriction with the &lt;code&gt;Strict-Transport-Security&lt;/code&gt; header &lt;a href=&quot;https://twitter.com/rboulton/status/1463181949355368449&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;in a couple of hours&lt;/a&gt;. Amazing work!&lt;/p&gt;&lt;h3 id=&quot;cache-static-content&quot;&gt;Cache static content&lt;/h3&gt;&lt;p&gt;This is where I first spotted a real performance issue, and many thanks to &lt;a href=&quot;https://twitter.com/anthony_ricaud&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Anthony Ricaud&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for helping me solve this issue. Logging into the Cloudflare dashboard, on the ‘overview&#39; page it gives you a whole set of useful graphs. The interesting one was the ‘Percent Cached&#39; graph:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Cache hit ratio in Cloudflare without any tweaks sits at 2%.&quot; decoding=&quot;async&quot; height=&quot;124&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/tFO3IwGk4N-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/tFO3IwGk4N-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/tFO3IwGk4N-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/tFO3IwGk4N-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see from the graph above, it&#39;s sitting around a 2% cache rate. For a static site this is truly shocking. There&#39;s literally nothing dynamic going on with any of the pages. Why was the cache hit percentage so terrible?&lt;/p&gt;&lt;p&gt;To help debug the issue I used a handy feature that Chrome has built-in, where you can expose a &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1433475671859384325&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;custom header in the browsers network tab&lt;/a&gt;. In the case of Cloudflare you want to be exposing the &lt;code&gt;cf-cache-status&lt;/code&gt; header. There&#39;s an explanation of what this header means &lt;a href=&quot;https://getfishtank.ca/blog/cloudflare-cdn-cf-cache-status-headers-explained&quot;&gt;here&lt;/a&gt;. A quick TL;DR; is that we want as many of our assets to have a value of &lt;code&gt;HIT&lt;/code&gt;, which means the asset is stored on the CDN and served to our user from their closest Point of Presence (PoP). As you can see from the image below, we were far from the case!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;You can see why the cache hit percentage is so poor by examining the network tab and the cf-cache-status header.&quot; decoding=&quot;async&quot; height=&quot;604&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/YsTCEmuh1h-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/YsTCEmuh1h-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/YsTCEmuh1h-528.webp 528w&quot; width=&quot;528&quot;&gt;&lt;/p&gt;&lt;p&gt;There&#39;s not a single &lt;code&gt;HIT&lt;/code&gt; in the above image! No wonder the graph percentage was so low! So what&#39;s going on here? Well this is where we have a collision between the Netlify and Cloudflare headers. By default many of the assets served by Netlify come with a &lt;code&gt;Cache-Control&lt;/code&gt; header value of &lt;code&gt;public, max-age=0, must-revalidate&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;These are the default header values coming from the Netlify origin.&quot; decoding=&quot;async&quot; height=&quot;274&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/kDJ6ufzfCY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/kDJ6ufzfCY-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/kDJ6ufzfCY-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/kDJ6ufzfCY-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Using &lt;a href=&quot;https://twitter.com/jaffathecake&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Jake Archibald&lt;/a&gt;&#39;s &quot;&lt;a href=&quot;https://jakearchibald.com/2016/caching-best-practices/&quot;&gt;Caching best practices &amp; max-age gotchas&lt;/a&gt;&quot; and &lt;a href=&quot;https://twitter.com/csswizardry&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Harry Roberts&lt;/a&gt; handy &quot;&lt;a href=&quot;https://csswizardry.com/2019/03/cache-control-for-civilians/&quot;&gt;Cache-Control for Civilians&lt;/a&gt;&quot; we can start to make sense of these somewhat confusing header values:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;public&lt;/code&gt; - this is good, no issue here.&lt;/li&gt;&lt;li&gt;&lt;code&gt;max-age=0&lt;/code&gt; - This is bad, it &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#preventing_caching&quot;&gt;forces the cache to revalidate&lt;/a&gt; (clears the cache).&lt;/li&gt;&lt;li&gt;&lt;code&gt;must-revalidate&lt;/code&gt; - Here we are forcing the browser to go back to the server to revalidate and check for a new resource, Harry writes about it in detail &lt;a href=&quot;https://csswizardry.com/2019/03/cache-control-for-civilians/#must-revalidate&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The important thing to realise is that even though Cloudflare is fronting the domain, it still looks at, and in many cases, just forwards these headers to the user. So Netlify is effectively asking the CDN not to store the asset and go back to the origin to revalidate the asset on every request. This isn&#39;t exactly what we want! This puts load on the origin and also adds to our users Time to First Byte (TTFB), since their nearest PoP isn&#39;t being used.&lt;/p&gt;&lt;p&gt;So you may be asking why exactly are Netlify serving up these headers that are very aggressive in terms of anti-caching? Well Netlify has a superb build process that integrates incredibly well with lots of different static build tools. Honestly, if you haven&#39;t tried it you really should! Often you can point Netlify at a Github repository and 2-3 mins later you&#39;ll have a fully hosted, working, static site you can share with friends and family. One of the huge advantages to this is that you can commit a change to Github and minutes later it is reflected on the live site. No more FTPing changes up to production! Or in other words, Continuous Delivery / Continuous Deployment (CD). For this to work you need to ensure that a developer (or user) has the latest assets from the server. There&#39;s no point deploying a change to a site if the browser continues to use an already cached version in a users browser!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: There are ways around this by using something called &lt;a href=&quot;https://guides.rubyonrails.org/asset_pipeline.html#what-is-fingerprinting-and-why-should-i-care-questionmark&quot;&gt;asset fingerprinting&lt;/a&gt;. What this does is generate a unique filename based on the contents of the file (usually a MD5 or SHA hash). So for example &lt;code&gt;styles-2f3f4bb27f.css&lt;/code&gt;. Now whenever this file changes, even by a single byte, the filename will also change, thus creating a new URL and invalidating the cache. I&#39;ve done this for the CSS and JavaScript on this blog which you can see if you view your DevTools network panel.&lt;/p&gt;&lt;p&gt;Now, Netlify has no idea if you are fingerprinting your assets, so it can&#39;t assume you are. Instead they make sure a user gets the latest assets by defaulting to the &lt;code&gt;Cache-Control&lt;/code&gt; headers above. But remember you can override them if you wish using the &lt;a href=&quot;https://docs.netlify.com/routing/headers/&quot;&gt;header options&lt;/a&gt; mentioned earlier.&lt;/p&gt;&lt;p&gt;So what did I do to solve the issue? Well I created a simple &lt;code&gt;netlify.toml&lt;/code&gt; file and placed it in the root of my project:&lt;/p&gt;&lt;pre class=&quot;language-py&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-py&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# netlify.toml&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# remove `immutable` if you don&#39;t have file fingerprinting available&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.css&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=31536000, immutable&quot;&lt;/span&gt;
    Vary &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Accept-Encoding&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# remove `immutable` if you don&#39;t have file fingerprinting available&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.js&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=31536000, immutable&quot;&lt;/span&gt;
    Vary &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Accept-Encoding&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.ico&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=31536000&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.json&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=31536000&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.png&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=2419200, must-revalidate, stale-while-revalidate=86400&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.jpg&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=2419200, must-revalidate, stale-while-revalidate=86400&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.jpeg&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=2419200, must-revalidate, stale-while-revalidate=86400&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.svg&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=2419200, must-revalidate, stale-while-revalidate=86400&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.avif&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=2419200, must-revalidate, stale-while-revalidate=86400&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.webp&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;values&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  Cache&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Control &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=2419200, must-revalidate, stale-while-revalidate=86400&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&#39;s nothing particularly clever about this file. I&#39;m basically pulling out particular file extensions and changing their &lt;code&gt;Cache-Control&lt;/code&gt; header values. I&#39;m sure there are ways to simplify this file, if there are please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know!&lt;/a&gt;. &lt;strong&gt;Note&lt;/strong&gt;: You may notice I&#39;ve used &lt;code&gt;immutable&lt;/code&gt; on my CSS and JS files. This is because I have fingerprinting in action on these files. I&#39;d recommend removing &lt;code&gt;immutable&lt;/code&gt; if you don&#39;t.&lt;/p&gt;&lt;p&gt;Let&#39;s see what this change looks like in the network panel on our temporary Netlify domain:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Headers from the origin once modified.&quot; decoding=&quot;async&quot; height=&quot;233&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lZ7TXxByk0-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lZ7TXxByk0-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lZ7TXxByk0-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/lZ7TXxByk0-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Well the Netlify origin headers are certainly looking a lot better! You can see the headers we&#39;ve set in the &lt;code&gt;netlify.toml&lt;/code&gt; file are reflected in the responses. Let&#39;s see now how this translates when we run it through Cloudflare:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Status of cache headers once run through Cloudflare to examine the caching status.&quot; decoding=&quot;async&quot; height=&quot;174&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/ky9JOwSAgC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/ky9JOwSAgC-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/ky9JOwSAgC-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/ky9JOwSAgC-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here I&#39;m using Chrome and exposing the &lt;code&gt;cf-cache-status&lt;/code&gt; header for debugging as mentioned above. As you can see it&#39;s looking a lot healthier! Here we now have 6 assets with a status of &lt;code&gt;HIT&lt;/code&gt; and 2 with a status of &lt;code&gt;DYNAMIC&lt;/code&gt;. So most of our page assets now reside in the CDN cache.&lt;/p&gt;&lt;p&gt;We&#39;re getting there, but what are those &lt;code&gt;DYNAMIC&lt;/code&gt; cf-cache-status&#39;s all about? Well Cloudflare still thinks these assets are changing a lot, so they are never cached. The CDN will always go back to the origin for these assets, which is not what we want for a static site. So how do we fix this issue?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Page Rules&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is where the awesomeness of Cloudflare and their free plan delivers again. They have functionality called &lt;a href=&quot;https://www.cloudflare.com/en-gb/features-page-rules/&quot;&gt;Page Rules&lt;/a&gt; available to us. Page Rules give you fine-grained control over how Cloudflare handles certain URLs (or URL patterns). Luckily for us, they give you 3 rules on the free plan. More than what we actually need, we&#39;ll only need 1!&lt;/p&gt;&lt;p&gt;With the page rules we want to tell Cloudflare to ‘Cache Everything&#39;. Basically everything served from our origin is cacheable. We also want to tweak the &lt;a href=&quot;https://blog.cloudflare.com/edge-cache-expire-ttl-easiest-way-to-override/&quot;&gt;Edge Cache Expire TTL&lt;/a&gt;. This is the easiest way to override any existing headers coming from our origin. We are going to do this for the whole domain, so I set &lt;code&gt;nooshu.com/*&lt;/code&gt; for the URL pattern:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Edit the page rules dialog in the Cloudflare admin panel.&quot; decoding=&quot;async&quot; height=&quot;337&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/0-EajVSAOl-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/0-EajVSAOl-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/0-EajVSAOl-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/0-EajVSAOl-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Let&#39;s now see what our CDN caching looks like with the above page rules set:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Looking at the cache hit rate once the page rules are enabled.&quot; decoding=&quot;async&quot; height=&quot;170&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/OARpT1Uy_G-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/OARpT1Uy_G-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/OARpT1Uy_G-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/OARpT1Uy_G-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Well look at that! 8 out of 8 assets with a &lt;code&gt;cf-cache-status&lt;/code&gt; of &lt;code&gt;HIT&lt;/code&gt;! Awesome, we&#39;re looking a lot better!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The minimum TTL you can see on the free plan is 2 hours. This means that if you make an update on your origin the CDN won&#39;t check for an updated version of the file for 2 hours, unless you purge the CDN cache. I&#39;m personally fine with this limitation. If you decide to pay for the &#39;Pro&#39; plan this can be reduced to as little as 30 seconds. So it really depends what functionality you need and what you are willing to pay for. You can always do a custom cache purge on a single URL if needed in an emergency, so that&#39;s an option too.&lt;/p&gt;&lt;p&gt;We&#39;re not quite finished yet. Notice how the HTML page has a &lt;code&gt;must-revalidate&lt;/code&gt; value. This value is redundant since our &lt;code&gt;max-age&lt;/code&gt; is now set to 1 year, so let&#39;s remove it. You could either do this via the &lt;code&gt;netlify.toml&lt;/code&gt; or our Worker code. I personally prefer to do it at the Edge so have done it in the Worker. Simply add the following in the Worker &lt;code&gt;securityHeaders&lt;/code&gt; object above:&lt;/p&gt;&lt;p&gt;&lt;code&gt;&quot;Cache-Control&quot;: &quot;public, max-age=60&quot;,&lt;/code&gt;&lt;/p&gt;&lt;p&gt;I&#39;d also recommend renaming the object to something new, since it no longer only contains security headers. Adding this line overrides the &lt;code&gt;must-revalidate&lt;/code&gt; value sent out in the response from Netlify.&lt;/p&gt;&lt;p&gt;I&#39;m not the only person to write about this problem, &lt;a href=&quot;https://twitter.com/vladiliescu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Vlad Iliescu&lt;/a&gt; has also written about it &lt;a href=&quot;https://vladiliescu.net/caching-with-cloudflare-and-netlify/&quot;&gt;here&lt;/a&gt;, if you want a more detailed explanation.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Bonus: Link Preload Header`&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This part is completely optional, and totally depends on your domain and use case. Using a Worker it is dead simple to add and test the &lt;a href=&quot;https://www.w3.org/TR/preload/#example-2-using-http-header&quot;&gt;Link Preload HTTP Header&lt;/a&gt;. This is a header that is sent along with the HTML file that tells the browser to immediately request certain assets you list in the header value. This can be useful for images and fonts for example. Assets that are listed in CSS files have to wait for the CSS to be downloaded and parsed by the browser, before a request can be made to the server to download them. By preloading the asset, it can be cached by the browser and used immediately when needed.&lt;/p&gt;&lt;p&gt;Be warned though, use the preload functionality &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1431905187124371461&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;sparingly&lt;/a&gt;, and be sure to test that it actually &lt;em&gt;improves&lt;/em&gt; the performance of your website! Read all about the puzzle that is preloading assets in &lt;a href=&quot;https://andydavies.me/blog/2019/02/12/preloading-fonts-and-the-puzzle-of-priorities/&quot;&gt;this blog post&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;. Here it is in action &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1433155825724502021&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;on this blog&lt;/a&gt;. This is the single line I added to the Worker:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token property string-property&quot;&gt;&quot;Link&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;/images/image1.png&gt;; rel=preload; as=image; nopush, &amp;lt;/images/image2.svg&gt;; rel=preload; as=image; nopush, &amp;lt;/images/image3.svg&gt;; rel=preload; as=image; nopush&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So what&#39;s the result of all this tweaking on our cache percentage in the Cloudflare control panel? Well it looks a lot healthier:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Cloudflare overview graph once the tweaks are made hits around 87%.&quot; decoding=&quot;async&quot; height=&quot;114&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/G4v9mbgexp-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/G4v9mbgexp-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/G4v9mbgexp-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/G4v9mbgexp-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see from the image above, we&#39;ve gone from a truly terrible 1.98% cache, all the way up to 87.35% cached, and this is growing hour by hour! Not bad considering how little work we&#39;ve actually had to do.&lt;/p&gt;&lt;h3 id=&quot;first-byte&quot;&gt;First Byte&lt;/h3&gt;&lt;p&gt;The last point to cover is the First Byte score which was a respectable ‘B&#39;. For it to be bumped up to an ‘A&#39; we need the TTFB to be around 1 second (1004 ms). Thankfully we&#39;ve already done all the work to reduce this TTFB time. Our request no longer goes all the way back to the origin (Netlify). Instead it is cached by the CDN and is delivered to our user from their closest geographic PoP. When it comes to latency the &lt;a href=&quot;https://hpbn.co/primer-on-latency-and-bandwidth/#speed-of-light-and-propagation-latency&quot;&gt;speed of light is the limit&lt;/a&gt;, so the closer the server is to the user, the better!&lt;/p&gt;&lt;p&gt;So let&#39;s take a look at the final score from WebPageTest with all the above tweaks applied:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;WebPageTest score once all the tweaks are made looks much better. All green across the board!&quot; decoding=&quot;async&quot; height=&quot;156&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/32Z3hxLfVo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/32Z3hxLfVo-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/32Z3hxLfVo-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/32Z3hxLfVo-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;That&#39;s a lot of green! Considering this is on a low spec device on a 3G Fast connection, more powerful devices on faster connections are going to get an even better experience. Here are some raw stats from &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=210831_AiDcQR_1c8c05bd7a25ec9c714a81bc31a84296-l:Before%20Tweaks,210903_BiDc28_329d0efa3e272281815e22d165f94fb3-l:After%20Tweaks&amp;medianMetric=SpeedIndex&amp;bg=000000&amp;text=ffffff&quot;&gt;comparing my two sets of test runs&lt;/a&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;16.4% reduction in visual complete time&lt;/li&gt;&lt;li&gt;25% reduction in TTFB&lt;/li&gt;&lt;li&gt;22.3% reduction in load time&lt;/li&gt;&lt;li&gt;16.8% reduction in LCP&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And a comparison filmstrip:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Filmstrip from WebPageTest comparing before and after results.&quot; decoding=&quot;async&quot; height=&quot;536&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/nCAvl3vXWN-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/nCAvl3vXWN-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/nCAvl3vXWN-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/nCAvl3vXWN-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;And no web performance dick measuring contest would be complete without a Lighthouse report including animated fireworks:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Lighthouse score after the test gives 100 across all scores. Fireworks are also visible on the image.&quot; decoding=&quot;async&quot; height=&quot;375&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/26ChvDPKBm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/26ChvDPKBm-300.webp 300w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/26ChvDPKBm-600.webp 600w, https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify/26ChvDPKBm-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Well considering this was only supposed to be a &quot;quick blog post&quot;, it&#39;s turned into a bit of a beast! There&#39;s a lot in the post, so let&#39;s recap what we&#39;ve covered and what infusing Netlify &amp; Cloudflare brings to the table for a static sites like mine:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;HTTP/3 + QUIC&lt;/li&gt;&lt;li&gt;0-RTT&lt;/li&gt;&lt;li&gt;Cloudflare Workers&lt;/li&gt;&lt;li&gt;Preload Link HTTP Header&lt;/li&gt;&lt;li&gt;DNSSEC&lt;/li&gt;&lt;li&gt;Excellent cacheability of static assets&lt;/li&gt;&lt;li&gt;SEO consideration moving from Github pages to custom domain&lt;/li&gt;&lt;li&gt;Netlify&#39;s amazing Continuous Delivery pipeline&lt;/li&gt;&lt;li&gt;Netlify Forms integration&lt;/li&gt;&lt;li&gt;Easy email integration via ImprovMX&lt;/li&gt;&lt;li&gt;Free hosting on Netlify&lt;/li&gt;&lt;li&gt;Free CDN usage via Cloudflare&lt;/li&gt;&lt;li&gt;Page Rules for fine-grained cache control&lt;/li&gt;&lt;li&gt;Improved security&lt;/li&gt;&lt;li&gt;Improved web performance&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&#39;s staggering the amount of functionality and features that are available (for free!) on the web nowadays.&lt;/p&gt;&lt;p&gt;Hopefully you&#39;ve found this post interesting and useful! As always, feedback and improvements are always welcome. Just &lt;a href=&quot;https://twitter.com/TheRealNooshu/&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know via Twitter&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;06/09/21: Initial blog post published.&lt;/li&gt;&lt;li&gt;06/09/21: Added missing aknowlegements for &lt;a href=&quot;https://twitter.com/anthony_ricaud&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Anthony Ricaud&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for pointing me in the right direction on how to solve the Cloudflare caching issue!&lt;/li&gt;&lt;li&gt;06/09/21: Thanks again to &lt;a href=&quot;https://twitter.com/anthony_ricaud&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Anthony Ricaud&lt;/a&gt; for spotting a number of errors in the post. This is why you let folks proof-read posts first!&lt;/li&gt;&lt;li&gt;23/11/21: Updated the Worker code to remove the added headers and use the new &#39;HTTP response headers with Transform Rules&#39; from Cloudflare.&lt;/li&gt;&lt;li&gt;09/12/21: Updated the post to include the correct expression allowing you to &lt;em&gt;only&lt;/em&gt; target the HTML response with the Response Transform rules. Thanks to &lt;a href=&quot;https://twitter.com/rboulton&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Richard Boulton&lt;/a&gt; for his help fixing this issue.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>We&#39;ve spotted something on your scan...</title>
    <link href="https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/" />
    <updated>2021-05-12T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/</id>
    <content type="html">&lt;p&gt;So a word of warning, this isn&#39;t a blog post about web performance. I&#39;m afraid those posts may need to be paused for the moment.&lt;/p&gt;&lt;p&gt;If you&#39;ve already read the original post you can jump straight to the &lt;a href=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/#postBottom&quot;&gt;latest update here&lt;/a&gt;, or all updates &lt;a href=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/#updates&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: I feel the need to point this out. This post is about a brain tumor and cancer. It&#39;s important to realise that the doctors considerations, and ultimately treatment, are unique to my situation. Anything mentioned in this post shouldn&#39;t be taken as gospel. If a reader finds themselves in a similar situation, please do take the advice of your own specialists. What works (or doesn&#39;t work) for me &lt;strong&gt;will&lt;/strong&gt; 100% be different for other people.&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;diagnosis&quot;&gt;Diagnosis&lt;/h2&gt;&lt;p&gt;To be fully upfront about all this, on Wednesday 21st April 2021 I was diagnosed with a brain tumor. Completely out of the blue. In the morning I was busy working away still trying to get &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1369343971303055367&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Brotli&lt;/a&gt; compression tested and enabled on &lt;a href=&quot;https://www.gov.uk&quot;&gt;GOV.UK&lt;/a&gt;, 7 hours later I was given the news that I had a large brain tumor on my right frontal lobe. Hell of a day, huh!&lt;/p&gt;&lt;p&gt;So you may be asking: &quot;Why are you writing about this?&quot;. Well, there are 3 main reasons:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;I want to get this info out of my head (hah!) while I can still remember it.&lt;/li&gt;&lt;li&gt;From the conversations I&#39;ve had with people since this happened, people are fascinated with how it was discovered.&lt;/li&gt;&lt;li&gt;To offer help to others in a similar situation that I (and my family) are currently in.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;I&#39;ve seemingly learned a fair amount about myself and the topic in the past 3 weeks. If you&#39;d like to know more feel free to read on, or if you&#39;d prefer to stick to web performance I have plenty of &lt;a href=&quot;https://nooshu.com/tags/webperf/&quot;&gt;blog posts here&lt;/a&gt; which are much more light hearted!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: We have now &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1390377138860265474&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;enabled Brotli on GOV.UK&lt;/a&gt;, thanks to &lt;a href=&quot;https://twitter.com/jaffathecake/status/1392732352795848709&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Jake Archibald&lt;/a&gt; for the humorous reminder :)&lt;/p&gt;&lt;h2 id=&quot;what-happened&quot;&gt;What happened?&lt;/h2&gt;&lt;p&gt;This is a question I&#39;ve been asked a lot. From nurses, doctors, pharmacists, out of hours doctors, and family and friends. So I&#39;ll give you a rundown of what happened below:&lt;/p&gt;&lt;h3 id=&quot;initial-signs-something-wasnt-right&quot;&gt;Initial signs something &quot;wasn&#39;t right&quot;&lt;/h3&gt;&lt;p&gt;Back in October 2020 I started to develop issues with my lower left back and left leg. Multiple consultations with the doctors were of the opinion it was &lt;a href=&quot;https://www.nhs.uk/conditions/sciatica/&quot;&gt;sciatica&lt;/a&gt;. I was given strong painkillers, told to look at getting physio, and make adjustments to my working environment. This actually all made sense at the time. Working from home for 8 months, with less physical exercise, and generally working more hours than usual, all could have easily caused this issue. Unfortunately, no matter what I tried it didn&#39;t solve the issue. I tried: a standing desk, new office chair, new shoes, lots more walks, physio. Nothing seemed to be helping. It got so bad at one point I was crawling on my hands and knees at 3am to go to the bathroom because I was in so much pain. Not exactly what you expect to be doing at the age of 39!&lt;/p&gt;&lt;p&gt;After several months the back and leg pain seemed to fade, but it then turned into extreme heel pain in my left foot, which the doctor said was likely to be &lt;a href=&quot;https://www.nhs.uk/conditions/plantar-fasciitis/&quot;&gt;plantar fasciitis&lt;/a&gt;. Apparently runners tend to get this a lot, but I certainly wasn&#39;t running! So yes, a fun 6 months!&lt;/p&gt;&lt;p&gt;But things have developed further and over the past 4 - 6 weeks I have experienced 3 seemingly very minor seizures. Throughout each I was fully conscious. I will try to describe each of them as I remember them the best I can.&lt;/p&gt;&lt;h3 id=&quot;seizure-1-standing-at-my-desk&quot;&gt;Seizure 1: Standing at my desk&lt;/h3&gt;&lt;p&gt;I now have a desk that allows me to both &lt;a href=&quot;https://www.posturite.co.uk/product/oploft&quot;&gt;sit and stand very easily&lt;/a&gt;. I&#39;ve got into the habit of standing for meetings with people while on a video call, but when it comes to &quot;real work&quot;, I always sit. This tends to give me a good balance of both sitting and standing throughout the day.&lt;/p&gt;&lt;p&gt;For the meeting in question I just so happened to be on mute and wasn&#39;t speaking at the time. I suddenly felt quite dizzy and had to lean on my desk a little. At first I thought I was having some sort of panic attack. I don&#39;t remember the topic of discussion for the meeting, but at the time it happened I was thinking &quot;maybe this is making me anxious?&quot;. I do remember having a really strange out-of-body experience (OBE) while it was happening. It&#39;s hard to describe, but it&#39;s almost like coming to the realisation that that &quot;you are fully in control of everything you are doing&quot; and &quot;you have an effect on your surroundings and the people you interact with&quot;. It feels more like a powerful emotion than anything physical. Or another way I&#39;ve described it is imagine someone turned off the auto-pilot in your head. You are now having to consciously breathe and blink for yourself, as well as think about all the other things going on too.&lt;/p&gt;&lt;p&gt;Within 10 - 12 seconds this intense feeling had passed and I suddenly felt fine again. It was the first seizure I&#39;d had, so I thought nothing of it, and simply moved on with the rest of the day.&lt;/p&gt;&lt;h3 id=&quot;seizure-2-driving-in-the-car&quot;&gt;Seizure 2: Driving in the car&lt;/h3&gt;&lt;p&gt;This was around 2 weeks later. Now this one scares me now that I know more! Technically I wasn&#39;t moving at the time, I was parked at traffic lights waiting for the light to turn green so I could then pull across a carriageway into the local supermarket.&lt;/p&gt;&lt;p&gt;Suddenly, I had the same OBE I&#39;d felt before, where I felt like someone had pulled &quot;me&quot; out of the back of my own head. In front of me the lights turned green and I managed to pull forwards and start moving a little, so I had some control of my feet. Unfortunately I didn&#39;t have the strength in my left arm to fully disengage the handbrake, so the warning alarm sounded in the car to tell me it was still engaged. I was fully conscious. My brain was telling my hand to push down and release the handbrake, but my hand simply couldn&#39;t. It was a very strange experience where your arm and hand seemingly has a mind of its own.&lt;/p&gt;&lt;p&gt;Because the alarm was beeping, my wife looked over and mentioned that the handbrake wasn&#39;t down, but she didn&#39;t notice anything else in me that looked different. I didn&#39;t try to speak to her, and I&#39;m not even sure if I&#39;d have been able too if I&#39;d tried. Again, within 10 - 12 seconds this had all passed and I was fully able to move and control the car. We went to the shops and returned home with no further issues. Once again I put it down to a panic attack, but as it all happened so quickly I dismissed it and carried on with life. At this point I hadn&#39;t linked the 2 separate events together.&lt;/p&gt;&lt;h3 id=&quot;seizure-3-sitting-at-my-desk&quot;&gt;Seizure 3: Sitting at my desk&lt;/h3&gt;&lt;p&gt;This is the one that happened on the 21st April, the day I was diagnosed. It was around 10:50am and I was sitting at my desk working on a document. Suddenly the OBE I&#39;d felt before came back. Only this time I lost control of both lower arms. I was unable to raise them to the keyboard to continue typing. It then developed further and I felt like I&#39;d lost control of the lower half of my face. Almost like someone was gently pulling down on both sides of my cheeks and I was unable to stop them.&lt;/p&gt;&lt;p&gt;I realised this was happening, as I was fully conscious. During this time I must have tried to move, as I&#39;d started to lean to the right side of my chair. I was rotating slowly, unable to really control my movements. Again as with the other seizures, within 10 - 12 seconds this all passed and I could again control what I was doing. But this time it was different.&lt;/p&gt;&lt;p&gt;With this seizure I&#39;d been sitting at my desk, nothing strenuous was happening at all. It was quiet and I was calm. Because of this I was able to notice that I simply couldn&#39;t control my hands and that my face had &quot;drooped&quot;. This immediately alerted me into thinking that I was having a mini-stroke. Now, I&#39;ve seen the &lt;a href=&quot;https://www.youtube.com/watch?v=CO2k2Tk23G0&quot;&gt;FAST campaign adverts on TV&lt;/a&gt;, and realised a stroke can become very serious if not acted on quickly, so that really freaked me out!&lt;/p&gt;&lt;h2 id=&quot;nhs-111&quot;&gt;NHS 111&lt;/h2&gt;&lt;p&gt;I immediately opened the &lt;a href=&quot;https://111.nhs.uk/&quot;&gt;NHS 111 website&lt;/a&gt; (the UK non-emergency advice service), to fill in the details of what had just happened.&lt;/p&gt;&lt;p&gt;Right on the front page of this service it says:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Check it&#39;s not an emergency - Call 999 now if you have: signs of a stroke - face dropping on one side, can&#39;t hold both arms up, difficulty speaking seizure (fit) - someone is shaking or jerking because of a fit&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;At this point I was thinking, &quot;okay.. I&#39;m going to have to phone 999&quot; (for the first time ever in my life). But before I did I thought I&#39;d best discuss it with my wife downstairs. I walked downstairs and calmly told her about what had just happened and also the other brief episodes I&#39;d had weeks before (as I hadn&#39;t mentioned them to her). We both agreed that 999 would be the best option.&lt;/p&gt;&lt;p&gt;It was a strange conversation with the 999 dispatcher. They were talking to me, &quot;the patient&quot;, asking if I was fully conscious and able to lift my arms, move my face etc. These are all standard checks for a stroke. I was able to do everything they were asking me to do with ease. At this point I felt like a total fraud, and I was simply wasting their time. I further described what had happened and that it had now passed, but I still felt a little weak in my lower arms.&lt;/p&gt;&lt;p&gt;I was categorised as a priority 2 (I believe) and told an ambulance would be with us within the next 5 minutes. So I quickly started to pack a few things just incase I needed to leave with them for the hospital sharpish.&lt;/p&gt;&lt;h3 id=&quot;ambulance&quot;&gt;Ambulance&lt;/h3&gt;&lt;p&gt;Within 5 minutes there was an ambulance outside the door. Two very nice paramedics came in the house and questioned me about how I was feeling and what had happened. They checked my blood pressure, checked my blood oxygenation level, asked me to focus on their fingers, push &amp; pull with my arms against theirs etc. These are all standard checks for suspected stroke patients. It turns out they couldn&#39;t find any issues, which was embarrassing. It got me thinking: &quot;had I just imagined this all happening?&quot; and &quot;am I just overacting and wasting their time?&quot;.&lt;/p&gt;&lt;p&gt;One of the paramedics decided to speak to the specialist unit at the hospital to ask for advice, as they were unsure what would be best (and so was I!). After a 5-10 minute discussion I had 2 options:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;the paramedics would leave and I&#39;d take this up with my doctor to see what was happening. This could take weeks.&lt;/li&gt;&lt;li&gt;come with them in the ambulance and go to hospital for additional checks.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;As I was still thinking this could be early signs of a stroke, I (thankfully!) decided that going to the hospital was the best course of action. Even if just to rule out the possibility of a stroke! So that&#39;s what I did. I got in the back of the ambulance and 20 minutes later I arrived at the hospital and was admitted to a very busy ward.&lt;/p&gt;&lt;h2 id=&quot;hospital&quot;&gt;Hospital&lt;/h2&gt;&lt;p&gt;I&#39;m afraid not much really happened at this point for an hour or so. I had chats with nurses and the doctors on duty, describing what had happened as I mentioned above. The doctor looking after me was a little perplexed by it all, as it was very unusual. I can&#39;t remember the exact term he used, but I believe he was thinking these symptoms sounded like some sort of dissociative disorder. This was because of the OBE I&#39;d described to him.&lt;/p&gt;&lt;h3 id=&quot;computerized-tomography-ct-scan&quot;&gt;Computerized Tomography (CT) scan&lt;/h3&gt;&lt;p&gt;As part of the standard checks I was given a CT scan. Not only that, they injected me with a special dye called &#39;contrast material&#39;. One of the side effects of this contrast is you feel like you&#39;ve wet yourself about 20 seconds after they inject you with it. I honestly thought I&#39;d had an accident when it happened. Apparently it&#39;s related to the kidneys flushing the dye through your system. Either way, it feels weird!&lt;/p&gt;&lt;p&gt;Due to the ward being extremely busy I needed to wait for the results to be examined. This probably took around 5 hours. The first four hours seemed to go quite quickly. I had my noise cancelling headphones and a good book I was enjoying, so I was entertained (although slightly uncomfortable in the high back chairs and a face mask for COVID).&lt;/p&gt;&lt;h2 id=&quot;we-ve-spotted-something&quot;&gt;We&#39;ve spotted something&lt;/h2&gt;&lt;p&gt;About 5 minutes before being shown into a private room, the doctor looking after me came up to me and said the sentence:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;We&#39;ve not forgotten about you, we&#39;ve just spotted something on your scan&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Strangely enough, I couldn&#39;t concentrate on my book or music anymore. That was a long 5 minute wait! Well, the 5 minutes passed and the doctor came back over and asked me to follow him into one of the rooms. &quot;Fine&quot;, I thought, &quot;how bad could it be?&quot;. But a few seconds later I could immediately tell something wasn&#39;t right about his body language and manner. He seemed to be struggling to find the right words. And then he came out with it:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I&#39;m really sorry to tell you this, but we&#39;ve found a brain tumor on your right frontal lobe&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I have to admit the rest of the conversation is mostly lost due to the enormity of what I&#39;d just been told. It felt like the ground dropped from beneath me. But I do remember him saying it &quot;was large&quot;, and that &quot;if you were to wish to have a brain tumor, this would be the type you would want to have&quot;. A strange statement now I think about it, but one that has been confirmed since.&lt;/p&gt;&lt;p&gt;After about 3 or 4 minutes of chatting about who knows what, I just wanted to get out of the room. I actually felt quite sorry for the doctor. He must have done this so many times with patients over the years. I can&#39;t imagine it ever gets any easier. I have a huge respect for anyone who has to do this on a daily basis, it must be totally crushing at times.&lt;/p&gt;&lt;p&gt;I quickly managed to find the words to thank him for letting me know and promptly left the room to try and work out how the hell I was going to tell my wife this over the phone. We&#39;d been in constant communication all day via messenger, since due to the COVID restrictions she wasn&#39;t allowed to stay with me. I knew she would be worried sick at home. It was around 7pm and she&#39;d be getting the kids ready for bed.&lt;/p&gt;&lt;p&gt;I finally mustered up the courage to phone her and tell her what had been said. Again, I don&#39;t really remember the conversation at all. But an hour or so later she&#39;d managed to arrange childcare and she was on her way over to pick me up. She arrived and I think we were both still in shock. We didn&#39;t really say much, other than repeating exactly what I remembered from the doctors conversation. That evening I was sent away with a concoction of drugs to start taking immediately, and then told to return to the hospital tomorrow morning 9am for an &lt;a href=&quot;https://www.nhs.uk/conditions/mri-scan/&quot;&gt;Magnetic resonance imaging (MRI) scan&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;so-what-is-it&quot;&gt;So what is it?&lt;/h3&gt;&lt;p&gt;See, now that&#39;s the million dollar (pound) question isn&#39;t it. Not all tumors are the same. Some are benign, some are fast growing, slow growing, large, small etc. The CT scan could only show &lt;em&gt;something&lt;/em&gt; was there, but not in the detail needed for a more conclusive result of exactly &lt;em&gt;what&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;At this point I&#39;m going to skip ahead. I had the MRI scan the next day. Then a few days later another &#39;wet yourself&#39; inducing CT scan. The full body CT scan was clean, showing that there were no other tumors or areas of concern in my body. Great news!&lt;/p&gt;&lt;h2 id=&quot;meet-my-tumor&quot;&gt;Meet my tumor&lt;/h2&gt;&lt;p&gt;Time to cut to the chase. This is my brain tumor. There are many like it, but this one is mine. I&#39;ve nicknamed him Gary Glia. Gary because it is a &lt;a href=&quot;https://names.darkgreener.com/#gary&quot;&gt;seemingly unpopular name nowadays in the UK&lt;/a&gt; (can&#39;t imagine why...), so it seemed quite appropriate. And &lt;a href=&quot;https://en.wikipedia.org/wiki/Glia&quot;&gt;Glia&lt;/a&gt; because it is the type of cell in my brain that has gone tumorous.&lt;/p&gt;&lt;p&gt;Gary is an unpaying tenant that has taken up residence in my head. What a prick!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Top view of the tumor.&quot; decoding=&quot;async&quot; height=&quot;453&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/-UY5O-y32Z-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/-UY5O-y32Z-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/-UY5O-y32Z-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/-UY5O-y32Z-814.webp 814w&quot; width=&quot;814&quot; title=&quot;Gary from the top&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Side view of the tumor.&quot; decoding=&quot;async&quot; height=&quot;464&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/KtRtbnXsqF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/KtRtbnXsqF-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/KtRtbnXsqF-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/KtRtbnXsqF-814.webp 814w&quot; width=&quot;814&quot; title=&quot;Gary from the side.&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Back view of the tumor.&quot; decoding=&quot;async&quot; height=&quot;457&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/wWLHtXjZPw-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/wWLHtXjZPw-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/wWLHtXjZPw-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/wWLHtXjZPw-814.webp 814w&quot; width=&quot;814&quot; title=&quot;Gary from the back.&quot;&gt;&lt;/p&gt;&lt;p&gt;A few days later I had a meeting with the neurosurgeon where I was shown the above images. Now full respect to the neurosurgeon I spoke too, I like him. He was straight down the line and straight to the point. No sugar coating, no forewarning about the size of the tumor. After 30 minutes of chatting about all of the above, he simply pulled his phone out and showed me image number 1 you see above. You could say I wasn&#39;t exactly prepared for the image, a understatement if ever there was one. But in all honesty I don&#39;t think I ever would be. So it was a good tactic. Make it quick and painless, just like ripping off a band-aid!&lt;/p&gt;&lt;p&gt;So, what do we know about Gary?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;He&#39;s big... look at the size of this unit. I&#39;m amazed I can even function at all to be honest!&lt;/li&gt;&lt;li&gt;He&#39;s of the type called &lt;a href=&quot;https://www.macmillan.org.uk/cancer-information-and-support/brain-tumour/oligodendroglioma&quot;&gt;oligodendroglioma&lt;/a&gt; and currently believed to be a Grade 2 (slow growing) tumor. Wondering how you pronounce that? Isn&#39;t &lt;a href=&quot;https://www.youtube.com/watch?v=eYYMPR14PLI&quot;&gt;YouTube great&lt;/a&gt;!&lt;/li&gt;&lt;li&gt;He&#39;s very rare, with &lt;a href=&quot;https://www.cancer.gov/rare-brain-spine-tumor/tumors/oligodendroglioma&quot;&gt;only 1,217 people diagnosed with this type in the US every year&lt;/a&gt; out of a population of 328 million!&lt;/li&gt;&lt;li&gt;He&#39;s a number of years old. The neurosurgeon said a couple of years, but I personally suspect he could be 7 or 8 years old due to other symptoms in my past I now think could be related.&lt;/li&gt;&lt;li&gt;He&#39;s a prick.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So I&#39;ve basically won a really shit version of the lottery... but then again maybe not. It could be so, &lt;a href=&quot;https://en.wikipedia.org/wiki/Glioblastoma&quot;&gt;so much worse&lt;/a&gt;! I actually have options that I go into in the next section. There are people out there who are given this devastating news that include words like: &#39;Stage 4&#39;, &#39;aggressive&#39;, &#39;inoperable location&#39; and are literally given months to live, and there&#39;s nothing that can be done. Thankfully, I&#39;m not in that place!&lt;/p&gt;&lt;h3 id=&quot;the-positives&quot;&gt;The positives&lt;/h3&gt;&lt;p&gt;I don&#39;t want to depress everyone, so let&#39;s focus on some of the positives I&#39;ve since found out:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It&#39;s believed to be a slow growing tumor, that is currently classed as the less aggressive type (oligodendroglioma rather than anaplastic oligodendriogliomas).&lt;/li&gt;&lt;li&gt;It originated and is primarily located in my brain. I have no other areas in my body showing tumors.&lt;/li&gt;&lt;li&gt;It responds really well to chemotherapy. If I turn out to be &lt;a href=&quot;https://oncologypro.esmo.org/education-library/factsheets-on-biomarkers/1p-19q-co-deletion-in-glioma&quot;&gt;1p/19q Codeleted&lt;/a&gt; then chemotherapy is even better.&lt;/li&gt;&lt;li&gt;My symptoms (seizures) can be managed via drugs, which now look to have minimal side effects.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;what-happens-next&quot;&gt;What happens next?&lt;/h2&gt;&lt;p&gt;I&#39;ve had a really great set of meetings with some very knowledgeable folks from Oxford University and the John Radcliffe hospital in Oxford. There are three options available to me:&lt;/p&gt;&lt;h3 id=&quot;1-do-nothing-and-active-monitoring&quot;&gt;1 - Do nothing and active monitoring&lt;/h3&gt;&lt;p&gt;Basically manage the symptoms and look at how the tumor progresses over time. Go for regular checkups and see how long I hold out.&lt;/p&gt;&lt;h3 id=&quot;2-biopsy-to-determine-the-exact-type&quot;&gt;2 - Biopsy to determine the exact type&lt;/h3&gt;&lt;p&gt;At the moment Grade 2 is suspected because the CT scan hasn&#39;t shown the usual telltale signs of Grade 3 (which is bright white &#39;spots&#39; in the scans). Doing this would require brain surgery to remove a small portion of the tumor to be tested. I&#39;d then need to recover from this, before a more specific treatment plan is created.&lt;/p&gt;&lt;h3 id=&quot;3-remove-as-much-as-possible-via-major-surgery&quot;&gt;3 - Remove as much as possible via major surgery&lt;/h3&gt;&lt;p&gt;This is the last option, and the one I&#39;ve decided to go for. This is where the surgeon open&#39;s my head and will try to remove as much of the tumor as safely as possible. Once completed, additional surgery may be required to remove any tumor that is left over, or radiotherapy and (or) chemotherapy can be used to finish it off.&lt;/p&gt;&lt;p&gt;This surgery can sometimes take up to 9 hours, and it will most likely involve me being awake for it too (so not under general anaesthetic). I find that part both fascinating and terrifying all at the same time! The reason for this is because they want to be able to ask me questions while they are operating e.g. &quot;move your left hand&quot;, &quot;blink your eyes&quot;, &quot;speak to us&quot;. That way, they can be sure they are minimising the damage to the good parts of my brain. It blows my mind (hah!) that it&#39;s possible to &lt;a href=&quot;https://www.bbc.co.uk/news/av/uk-england-london-51557044&quot;&gt;even do this&lt;/a&gt;, but I&#39;m very glad they can!&lt;/p&gt;&lt;p&gt;But, as with all types of surgery (especially on the brain) there are some inherent risks:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;10-15% chance that it could cause a stroke or paralysis&lt;/li&gt;&lt;li&gt;less than 5% chance of a seizure occurring during surgery. This can be very bad indeed, and will result in the operation being stopped immediately&lt;/li&gt;&lt;li&gt;small chance of temporary paralysis on my left hand side&lt;/li&gt;&lt;li&gt;small chance of infection due to the head being open for a number of hours&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;decision&quot;&gt;Decision&lt;/h2&gt;&lt;p&gt;As mentioned above, looking at the size of Gary, and weighing up the risks, 3 seems like the only viable option to me that could lead to the best overall outcome. Considering I&#39;m 39 and not in my 70&#39;s, compared to the alternative of doing nothing, it feels like a simple decision to make. So that&#39;s what I&#39;m doing. I refuse to &lt;a href=&quot;https://poets.org/poem/do-not-go-gentle-good-night&quot;&gt;go gentle into that good night&lt;/a&gt;, so it&#39;s time to make plans to evict Gary Glia from my head.&lt;/p&gt;&lt;p&gt;It&#39;s also worth remembering: neurosurgeons are amazing people and they do this day in, day out, as their job! This is just a walk in the park for them...&lt;/p&gt;&lt;h3 id=&quot;moving-forwards&quot;&gt;Moving forwards&lt;/h3&gt;&lt;p&gt;So what happens now? Well I&#39;m now on the waiting list for the surgery, and I have additional monitoring and scans coming up. It&#39;s basically a case of waiting for a date and managing the symptoms. If it is that the seizures come back, or anything else gets worse, then things get reassessed by the specialists and the plan changes. I&#39;m continuing to work as much as possible, and spending time with family. For me it&#39;s all about keeping my thoughts away from the dark places they could so very easily slip into, which I fully admit they did for the first week after diagnosis.&lt;/p&gt;&lt;h2 id=&quot;support&quot;&gt;Support&lt;/h2&gt;&lt;p&gt;So here&#39;s a section I&#39;ve compiled for readers who may find themselves in the same position as I&#39;m now in. Thankfully, brain tumors are rare, even more so if of the type oligodendroglioma and anaplastic oligodendroglioma, so there may not be many of you out there. But if there are, here are a few support links to help you out:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.macmillan.org.uk/cancer-information-and-support/brain-tumour/oligodendroglioma&quot;&gt;Macmillan cancer support - Oligodendroglioma&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.thebraintumourcharity.org/brain-tumour-diagnosis-treatment/types-of-brain-tumour-adult/oligodendroglioma/&quot;&gt;The Brain Tumour Charity - Oligodendroglioma&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.braintumoursupport.co.uk/&quot;&gt;Brian Tumor Support Charity&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://braintumouraction.org.uk/&quot;&gt;Brain Tumour Action Charity&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://brainstrust.org.uk/&quot;&gt;Brain Trust Charity&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;As you can see, there&#39;s lots of support out there. Macmillan and the Brain Tumour Charity have been fantastic over the past few weeks. The work they do for people needs to be talked about more. They really are unsung heros working behind the scenes to make people&#39;s lives better, in some of the most challenging times they will ever face.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Pro tip&lt;/strong&gt;: Be &lt;em&gt;very very&lt;/em&gt; wary of &#39;Googling&#39; your type of tumour and reading random forums. There&#39;s a lot of scary stats and topics out there that may be totally fictional, or only apply for that specific individuals case! A &#39;random Google&#39; is a dangerous path to take. Trust me I did it and quickly stopped! If you can, stick with &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/&quot;&gt;approved sources&lt;/a&gt; and those recommended by some of the charities listed above. Or speak to the team looking after you for advice.&lt;/p&gt;&lt;h3 id=&quot;practical-steps&quot;&gt;Practical steps&lt;/h3&gt;&lt;p&gt;Now, I realise there are some really terrible topics to have to talk about in the section below, but unfortunately they need to be considered. I&#39;ve actually found the practical side of dealing with all this really useful. I&#39;m completely powerless in terms of a future date for the surgery and the results of said surgery. But what I &lt;em&gt;can&lt;/em&gt; do is make preparations for eventualities that &lt;em&gt;may&lt;/em&gt; happen. It may not seem like much, but with the practical side it feels like I&#39;m actually doing &lt;em&gt;something&lt;/em&gt; useful. I&#39;m moving the ball ever so slightly forwards.&lt;/p&gt;&lt;p&gt;So here are a few topics you could (should) start to think about:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Admin tasks&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Make sure you share passwords with your other half! I recommend setting up a &lt;a href=&quot;https://1password.com/families/&quot;&gt;family 1Password account&lt;/a&gt;. With a shared vault you can securely share documents, passwords, notes, all in the cloud. No need for unsecure notebooks with written passwords that could be lost or stolen!&lt;/li&gt;&lt;li&gt;Cancel your driving licence and remove yourself from the car insurance. In the UK you won&#39;t be allowed to drive if you have a brain tumor / seizures.&lt;/li&gt;&lt;li&gt;Make sure your partner is listed on household bills, and can talk to the companies.&lt;/li&gt;&lt;li&gt;Is your paperwork filed away in a single place and ordered logically?&lt;ul&gt;&lt;li&gt;Does your partner have easy access to it all?&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Do you have a safe? If so, do they know how to get into it? Share the combination / backup key with them.&lt;/li&gt;&lt;li&gt;Start to make a list of what you will need to (eventually) take to the hospital. Maybe even start to pack a bag straight away. It&#39;s not much, but at least it&#39;s there ready to go when needed. Involve your partner in this planning too.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Finances&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Check if your employer offers any pension scheme / death in service benefits. Is your partner listed correctly on these?&lt;/li&gt;&lt;li&gt;Ensure your partner can speak / access bank accounts if required.&lt;/li&gt;&lt;li&gt;Share details with your partner about your life insurance, income protection, mortgage payment protection etc. Maybe even speak to an Independent Financial Adviser (IFAs) about all this to check it is all in order.&lt;/li&gt;&lt;li&gt;Are there any extra benefits you are now eligible for e.g. &lt;a href=&quot;https://www.gov.uk/pip&quot;&gt;Personal Independence Payment (PIP)&lt;/a&gt;? The charities listed above can help you with advice on this.&lt;/li&gt;&lt;li&gt;Consider setting up a joint account with your partner, and have shared money &quot;just in case&quot;.&lt;/li&gt;&lt;li&gt;Create a spreadsheet with information about your household utility providers e.g. who&#39;s your electricity provider. Include account numbers, contact numbers, cost per month, payment method etc&lt;ul&gt;&lt;li&gt;On the same sheet you could include details about finances (Credit Cards, loans, higher purchases etc)&lt;/li&gt;&lt;li&gt;Include details about pensions, banks, building societies you are with including account numbers and contact details.&lt;/li&gt;&lt;li&gt;Remember to securely store this information (see the 1Password recommendation above)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;If in the UK you will now most likely be eligible for &lt;a href=&quot;https://www.nhs.uk/nhs-services/prescriptions-and-pharmacies/who-can-get-free-prescriptions/&quot;&gt;free prescriptions on the NHS&lt;/a&gt;. You can apply for this through your local doctors surgery. So make sure you do this, as you&#39;ll likely going to need a fair few drugs over the coming months / years!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Documents&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Make sure you have a last will and testament prepared. Make sure executor&#39;s are informed and listed correctly.&lt;/li&gt;&lt;li&gt;Ensure you have your &lt;a href=&quot;https://www.moneysavingexpert.com/family/power-of-attorney/&quot;&gt;Lasting Power of Attorney&lt;/a&gt; in place with your partner listed, as well as backups. You can &#39;do it yourself&#39;, or speak to an specialist advisor for this. I opted for an advisor, I don&#39;t have the mental capacity or time at the moment to do it manually.&lt;/li&gt;&lt;li&gt;Make a list of questions to ask nurses / doctors etc. I&#39;ve &lt;a href=&quot;https://docs.google.com/document/d/1Hk23n-1vKpxyLLvqekyJfh0stlHYZwkudhnck3XzNOM/edit?usp=sharing&quot;&gt;shared the list I made here&lt;/a&gt; that you can copy if you are struggling.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Above all else remember, you can make plans and be prepared, but even the most well-made plans can go pear-shaped quickly. It simply isn&#39;t possible to pre-think and pre-plan everything for every eventuality. So there&#39;s only so much you &lt;em&gt;can&lt;/em&gt; do. But much that is listed above could be considered &quot;good practice&quot; for any adult anyway, even if you aren&#39;t in the same situation I&#39;m in.&lt;/p&gt;&lt;h3 id=&quot;plans-for-this-blog-post&quot;&gt;Plans for this blog post&lt;/h3&gt;&lt;p&gt;So what are the plans for this blog post? Well, I plan to keep this post updated with my progress for those who are interested. This will be the only blog post where I mention Gary the prick. Basically it will be a self-contained blog post about a topic I never expected to be living, let alone writing about! And as with my web performance blog posts, I&#39;ll try to keep them as &#39;living documents&#39; that are constantly updated as things change. I will continue to use the &#39;Post changelog&#39; below in exactly the same way.&lt;/p&gt;&lt;h2 id=&quot;thanks&quot;&gt;Thanks&lt;/h2&gt;&lt;p&gt;So I have a huge number of folks to thank who have been amazing over the past few weeks:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;My wife, who has been so incredibly brave throughout this. Immediately looking for information and support that is out there (both emotional and practical). She amazes me every day.&lt;/li&gt;&lt;li&gt;My parents, sister, brother-in-law and immediate both sides of the family who have rallied around to help us in whatever way they can. At the moment COVID restrictions make this hard, but this should become easier in the UK very soon. I can&#39;t thank them enough.&lt;/li&gt;&lt;li&gt;Line managers from GDS, both past and present, who have helped ease my mind when it comes to the logistics of work in the future.&lt;/li&gt;&lt;li&gt;The Frontend community at GDS. I feel so lucky to be able to head up and work with such an amazing group of talented and dedicated individuals. They inspire me every day.&lt;/li&gt;&lt;li&gt;A certain ex-GDS colleague who shall remain nameless. They have been a fountain of knowledge in so many different areas that, in all honesty, I didn&#39;t even realise existed 3 weeks ago!&lt;/li&gt;&lt;li&gt;And last, but certainly not least, the wonderful paramedics, doctors, nurses, and all workers of the &lt;a href=&quot;https://www.nhs.uk/&quot;&gt;National Health Service&lt;/a&gt;. The quality of care I&#39;ve received so far has been truly outstanding. It&#39;s all the more impressive considering the huge strain COVID is placing on the whole system at the moment. They really are heros!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And finally, thank you reader for taking the time to read the post. It wasn&#39;t at all easy to write, but I feel so much better for actually taking the time to do it. Hopefully some of you find it useful. If helps even just a single person get through such a monumental, life changing event such as this, then it was totally worth my time writing it.&lt;/p&gt;&lt;h2 id=&quot;updates&quot;&gt;Updates&lt;/h2&gt;&lt;h3 id=&quot;another-seizure-monday-10th-may&quot;&gt;Another seizure - Monday 10th May&lt;/h3&gt;&lt;p&gt;On Monday night I had a really &#39;fuzzy&#39; head. That&#39;s the best way I can describe it. It was almost like I&#39;d taken some sort of sleeping tablet. I also had a &#39;prickly&#39; head and scalp, quite sensitive to touch. I went to bed early but woke up at 2:30am with the same OBE I&#39;d experienced before. I think I had another seizure in my sleep. I can&#39;t be 100% sure as I fell to sleep again right after it. I just remember waking up to the same feeling.&lt;/p&gt;&lt;p&gt;On speaking to the team of specialists they said it&#39;s most likely from lack of sleep, my body adjusting to the drugs and general stress with everything going on at the moment. They aren&#39;t too concerned and my medication will be staying at the same level for the moment. If it gets worse in the coming weeks they will look to up the amount of anti-seizure medication.&lt;/p&gt;&lt;p&gt;I just need to work out what the triggers are, and the warning signs of one occurring. It&#39;s a whole new world for me, so I don&#39;t recognise the signs yet. You can live 39 years in a body, and still have no idea what is going on with it...&lt;/p&gt;&lt;h3 id=&quot;messages-of-support-thursday-13th-may&quot;&gt;Messages of support - Thursday 13th May&lt;/h3&gt;&lt;p&gt;I&#39;ve been absolutely blown away by the huge number of messages of support I&#39;ve had since writing this post. I &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1392526600047140864&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;posted it on Twitter&lt;/a&gt; and &lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:6799090494472245248/&quot;&gt;on LinkedIn&lt;/a&gt; and have received literally hundreds of messages. I&#39;ve tried to respond to as many as I can, but I may not have seen them all. So sorry if I haven&#39;t replied! Just to say thank you to everyone who has reached out. I really do appreciate the support. It&#39;s all been quite overwhelming, and will admit to welling up a few times from the sheer number of them. What a crazy couple of days.&lt;/p&gt;&lt;h3 id=&quot;neurosurgery-pre-assessment-clinic-saturday-15th-may&quot;&gt;Neurosurgery pre-assessment clinic - Saturday 15th May&lt;/h3&gt;&lt;p&gt;On the 20th May I have another appointment at the hospital. This is the pre-assessment for the surgery. I&#39;m not 100% sure what is involved, but I&#39;m going there with a list of questions I&#39;m hoping to get answered. On speaking to a senior member of the team during the week the timeline for surgery has been revised down from 3 months to 4-6 weeks (assuming other higher priority cases don&#39;t bump me down the list). This is good news, but also makes me a little anxious! I was mentally preparing myself for having 3 months to get used to the idea of awake open-head surgery. 4-6 weeks seems way too soon! But it is what it is. I&#39;ll get used to it, it&#39;s not like I really have much of an option...&lt;/p&gt;&lt;h3 id=&quot;yet-another-seizure-wednesday-19th-may&quot;&gt;Yet another seizure - Wednesday 19th May&lt;/h3&gt;&lt;p&gt;A short update. I had a very short &#39;mini&#39; seizure in the morning just after waking up, around 6:10am. I was feeling perfectly fine, then suddenly my face felt quite warm. I could feel something was about to happen. I had the feeling of losing control of my face again (drooping), then started rocking gently backwards and forwards in my seat as has happened before. I was fully aware and conscious during this time. Around 10 seconds later I was back in control. It was nowhere near as intense as some of the other seizures I&#39;ve had, but it&#39;s still a seizure. So it&#39;s important to keep the specialists informed. I believe we may look into upping the dosage of the &lt;a href=&quot;https://www.nhs.uk/medicines/levetiracetam/&quot;&gt;Keppra&lt;/a&gt; drug I am on in the coming days.&lt;/p&gt;&lt;h3 id=&quot;pre-operative-assessment-appointment-thursday-20th-may&quot;&gt;Pre-operative assessment appointment - Thursday 20th May&lt;/h3&gt;&lt;p&gt;So today was an interesting day. I learnt a lot about the upcoming treatment and the weeks ahead. The more information I have at the better, as planning anything is virtually impossible at the moment! I had a 2.5 hour meeting with a friendly senior nurse on the team looking after me. She answered all my questions and gave me a rundown of the coming weeks, months, and years. I&#39;ve tried to break what was said down into sections following a chronological order of future events. Ultimately the reason for the consultation was to prepare me for surgery in the future, which is a good thing. Although I&#39;m still very nervous about the thought of a whole team poking around in my head, the information given to me today has really helped me understand it all.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Future seizures&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I need to start logging any previous and future seizures, this is very important. Ultimately the plan is to achieve a sustained period where I don&#39;t have any seizures, while keeping me on the minimal amount of drugs possible (due to drug side effects). There&#39;s a fine balance to achieve this, and we aren&#39;t there yet. I seem to be getting one seizure a week at the moment. I&#39;d say my drugs will be increased in the coming days. I&#39;ve also been asked to try to speak the next time a seizure occurs. The team has questions like: &quot;Am I able to talk at all?&quot;, &quot;Does it cause slurred speech?&quot;, &quot;Are the words I want to say totally different to what comes out of my mouth?&quot;. The team are interested to know how a seizure affects my speech, since this can give an insight into the parts of the brain being affected by the tumor. So that&#39;s on my to-do list for next time!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Neuropsychology assessment&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I&#39;m scheduled to have a neuropsychology assessment in the coming weeks. I didn&#39;t realise how important this actually was until today. It&#39;s going to be a long process, possibly a 3 hour+ meeting where members of the team will conduct many memory and cognitive tests on me. Ultimately, this testing will be used as to set a &#39;baseline&#39; for various aspects of my current brain function. This baseline will then be used both &lt;em&gt;during&lt;/em&gt; and &lt;em&gt;after&lt;/em&gt; the surgery.&lt;/p&gt;&lt;p&gt;It was explained to me that during surgery they will be constantly asking me questions and asking me to move parts of my body. While this is happening they will be manipulating and testing neurons in my brain to see what effect it has on my cognitive ability. Only once the surgeon is 100% happy that said part of the brain is safe to remove, will it actually be removed. As you can imagine this is going to be a long process with a tumor the size of mine! The baseline will also be used to compare brain and memory function during the recovery period. Without this baseline it would be extremely difficult, if not impossible, to measure the effect the surgery has had on me.&lt;/p&gt;&lt;p&gt;Only after this baseline has been established will my surgery be scheduled. That&#39;s how important this assessment is!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Pre-surgery preparation&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I&#39;ve been given a set of items to use the night before surgery.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Antibacterial body wash&lt;/li&gt;&lt;li&gt;Antibacterial cream (for my nose to combat &lt;a href=&quot;https://www.nhs.uk/conditions/mrsa/&quot;&gt;MRSA&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Antibacterial mouthwash&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Basically I need to be as germ free as possible before having surgery. I will be using all three of these the day before and on the morning of the surgery. Body wash all over, leave for 1 minute then rinse. Rub the cream up my nose 3 times throughout the day and in the morning. Mouthwash 3 times the day before, and again in the morning of surgery. Doesn&#39;t sound pleasant, but if it means there&#39;s more chance of success I&#39;m more than happy to do it!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The surgery&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Now this is where it all starts to get very interesting, as the process was explained to me in detail so that I know what to expect on the day. I will likely be told on a random Tuesday in the future that I&#39;ve been scheduled for surgery on the following Friday. So I&#39;ll only get 3 days notice! That&#39;s going to be an interesting week! I&#39;ll have a COVID test 2 days before, then on the Thursday I will go into the ward and prepare for surgery the following day.&lt;/p&gt;&lt;p&gt;I will have another MRI scan and a brain mapping scan. The brain mapping scan is used to map the coordinates and direction of travel which the surgeon and team will enter my skull. All this is planned to the smallest detail. Nothing like Dr. Nick off The Simpsons (thankfully!).&lt;/p&gt;&lt;p&gt;The surgery is likely to last 5 hours, 3 of those with me being awake. They will put me to sleep for the first hour to prepare me since it sounds a little unpleasant. I will be attached to a metal frame via a set of small screws that are drilled into my skull. I will then be woken up and won&#39;t be able to move my head. My head will already be open. Upon waking I will be surrounded by a team of people and won&#39;t be able to see or feel anything that is happening behind me. The team will continue to ask me questions and to do things while they operate and slowly remove the tumor (using the methodology mentioned earlier). Once they are happy, they will again put me to sleep for an hour, patch me back up and I will be sent to recovery. It all sounds so simple doesn&#39;t it!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recovery&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Assuming the surgery goes well I will be admitted to the recovery ward for around 24 hours. I&#39;ll have a tube coming from my head to allow fluid from the wound to drain safely away, as well as various other pipes attached to my body. The aim is to get me up and mobile (at least a little) within the first 24 hours. This is to reduce the chance of blood clots, as there&#39;s a raised chance these can happen for up to 3 weeks after surgery. If all goes well and I don&#39;t show signs of seizures, I will be sent home. How crazy is that, major brain surgery to home within 24 hours! If it is that there are complications, I&#39;m unable to move etc, I will be kept in for longer. It&#39;s all a complete unknown at the moment.&lt;/p&gt;&lt;p&gt;Once Gary the prick has been extracted, he will be tested to see what exactly he is (or was), and this data will be used to inform my ongoing treatment plan. Genetic marker testing (e.g for the &lt;a href=&quot;https://oncologypro.esmo.org/education-library/factsheets-on-biomarkers/1p-19q-co-deletion-in-glioma&quot;&gt;1p/19q Codeleted gene&lt;/a&gt;) will also be conducted, and this data will also be used to plan the best treatment moving forwards. The &#39;default&#39; treatment for this type of tumor is surgery, so it&#39;s likely I will have more head screwing fun in the future! Radiotherapy can be used once, and chemotherapy can be used multiple times to clear Gary&#39;s leftovers should it be needed. We shall only know more after the messy uninvited squatter has been forcibly removed.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Back to &#39;normal&#39; and work&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I&#39;m likely to need a minimum of 6-8 weeks off work. In this time I will likely exhibit changes in mood, behavior, concentration, and personality. This is all perfectly normal due to the fact that my brain will be adjusting both physically and chemically to the tumor not being there anymore. If I require further surgery and/or treatment like chemotherapy/radiotherapy, I may need a little longer to recover. Again this won&#39;t be known until after surgery.&lt;/p&gt;&lt;p&gt;The key is to take this slowly. I already know this is going to be an incredibly frustrating time for me. I know myself. I&#39;m not one for sitting around doing very little. I&#39;m always on the go and doing &lt;em&gt;something&lt;/em&gt;. But I&#39;m going to have to live with it for a few months. Maybe I can start watching all of the box sets &amp; movies I&#39;ve always wanted to watch! Or start tackling the tonne of books I&#39;ve never read over the years. I mean after all, it sounds better than the alternative!&lt;/p&gt;&lt;h3 id=&quot;a-bumpy-weekend-saturday-22th-sunday-23rd-may&quot;&gt;A bumpy weekend - Saturday 22th - Sunday 23rd May&lt;/h3&gt;&lt;p&gt;So it was a bit of a bumpy weekend in terms of how I&#39;ve been feeling. I&#39;m now on anti-seizure medication that I will be taking for the foreseeable future, maybe even forever depending on how surgery goes.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Saturday&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Day went well until about 10:30am, suddenly I felt extremely tired, sat on the sofa and promptly fell to sleep. Woke up 10 mins later to find my 4 year old son had covered me in cushions and was laughing his little head off as I woke up. I was completely and utterly confused as to what had happened. The medication I&#39;m on mentions drowsiness / tiredness, so I put it down to that. I didn&#39;t feel 100% all day, generally quite weak. I had the same feeling around 6:30pm where I could barely lift my arms and just wanted to sleep (this is not at all like me, as I generally hardly sleep!). This feeling passed after an hour or so, after a little food.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Sunday&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Went to the park with my wife and children, no issues at all. Around 1pm I had a sudden feeling of weakness. Went to the living room, sat on the sofa, promptly fell to sleep. My 4 year old son is very opportunistic, again covered me in cushions, again in hysterics when I woke up 20 mins later. He&#39;s going to be a fun teenager...&lt;/p&gt;&lt;h3 id=&quot;seizure-in-the-kitchen-monday-24th-may&quot;&gt;Seizure in the kitchen - Monday 24th May&lt;/h3&gt;&lt;p&gt;Due to everything that happened over the weekend, which we assumed were the side effects of the drugs I contacted the team of specialists looking after me early in the morning via email. I knew because of this they would be in contact with me later in the day.&lt;/p&gt;&lt;p&gt;Around 7:30am I was standing in the kitchen having just prepped the kids breakfast. I suddenly had what I can only describe as an emotionally driven feeling that &lt;em&gt;something&lt;/em&gt; was about to happen. I could feel something building. I immediately recognised something wasn&#39;t right and sat down next to my wife at the kitchen table. She could see something wasn&#39;t right either, so stood up next to me and so as to shield me from the children. I suddenly couldn&#39;t really control my movements and started gently rocking backwards and forwards. I was conscious and remembered that I&#39;d been asked to speak the next time this happened. I said out loud &quot;Can you hear me?&quot; to my wife. It sounded perfectly clear in &lt;em&gt;my&lt;/em&gt; head, but to my wife it sounded slightly slurred. She could still make out what I was trying to say though. Apparently while this was happening my eyes were very wide, but that could really have just been panic on my part. It all passed so quickly as they always seem to do.&lt;/p&gt;&lt;p&gt;Going back to the emotion I felt. I&#39;m not sure if this was something triggered by my subconscious, as in &quot;bubbling up&quot; to tell me &quot;stuff is going on here, get ready for it&quot;, or if it was me thinking about a seizure and then this triggered it because I thought of it. I&#39;m thinking the former to be honest.&lt;/p&gt;&lt;p&gt;On speaking to the specialist nurse she seemed to think that the episodes that occurred over the weekend weren&#39;t side effects of the drugs. These were actually a different type of seizure that were happening over a longer period of time. Well at least now I know! Because of this my anti-seizure medication has been increased, and will be increased again in a week. The aim is to have a long period of time between each, or even stop them completely before surgery. So we shall see over the next week if this increase actually works.&lt;/p&gt;&lt;h3 id=&quot;obtained-my-full-scan-data-tuesday-25th-may&quot;&gt;Obtained my full scan data - Tuesday 25th May&lt;/h3&gt;&lt;p&gt;When in one of my follow-up meetings (20th May) with a senior nurse I happened to ask the question if I could have a copy of the original photos from the scan. All I had at the time was a screenshot from a video call that was shown to me off the neurosurgeons smartphone (what a crazy sentence, isn&#39;t modern technology amazing!). I was told to visit the hospitals radiology department, fill out a form at reception and it could all be arranged. On the form the interesting part for me was the following section:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Section of the image scan application form where you get to choose how they send the data to you.&quot; decoding=&quot;async&quot; height=&quot;404&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/yx3n_-Fsz9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/yx3n_-Fsz9-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/yx3n_-Fsz9-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/yx3n_-Fsz9-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The top section allows you to choose &lt;em&gt;how&lt;/em&gt; they send you the data. For a patient it was either as an online link or as an encrypted CD. It shows you how old the form is, no mention of USB sticks, or even DVD media! I chose the option where an online link is sent to me via email.&lt;/p&gt;&lt;p&gt;It&#39;s also worth noting the sentence below this box:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The patient is entitled to a copy of all the Radiology imaging we retain at this trust. However if specific episodes are required, please specify below.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So depending on your local NHS trust, you should be able to do this yourself (should you be interested).&lt;/p&gt;&lt;p&gt;I received an automated email saying this could take up to 28 days, but 2 days later I had an email from a member of the team saying that my scans were scheduled for the overnight upload and I&#39;d receive the link the following day. Upon receiving the link they sent me a one time password (OTP) via my mobile number that allowed me to login. The web performance of the system was shocking. Took around 20 seconds to load on a 50 Mbit connection and a modern browser! Thank you JavaScript...&lt;/p&gt;&lt;p&gt;Once I clicked on the download button I realised this wasn&#39;t just a set of high resolution images, as it was a zip file for 1.3 GB of data! Intriguing! It took me a number of attempts to actually download the data, and I had close friends try too. Their server connection was slow and unreliable, and it also doesn&#39;t seem to support &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests&quot;&gt;HTTP range requests&lt;/a&gt;, so it wasn&#39;t possible to resume a download after already managing to grab 880 MB of it! Best thing to do I found was to wait until later in the evening. I managed to get a download speed of around 2.7 MB per second, so it only took around 15 minutes in the end.&lt;/p&gt;&lt;p&gt;The reason the zip file is so huge is because it is sent in a standardized format for use in specialist tools that can interpret the data. Here&#39;s me thinking it would be just a few images... nope! It&#39;s literally all the images from all scans completed so far! Hence the box on the form stating which scans you require.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of lots of thumbnails of my head. All the images captured in the scans!&quot; decoding=&quot;async&quot; height=&quot;693&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/iGUsp9cZJ8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/iGUsp9cZJ8-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/iGUsp9cZJ8-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/iGUsp9cZJ8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I believe there are around 4,000-5,000 images in total from 1 CT and 1 MRI scan! Thankfully there are tools that you can use to view these images. And not just view them, they allow you to navigate through all the scans. Each image is essentially a &quot;slice&quot; of the brain. The tools allow you to move through all 3 axis in near real time, with each of the corresponding panels changing depending on the input from the others.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the three navigation axis from my MRI scan.&quot; decoding=&quot;async&quot; height=&quot;536&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/B-HzKUEsa5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/B-HzKUEsa5-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/B-HzKUEsa5-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/B-HzKUEsa5-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above image you can see my MRI scan. It&#39;s essentially an orthogonal view of my head, with each of the panels represents a different axis:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Left panel: Top of my head (Axial view)&lt;/li&gt;&lt;li&gt;Top right: Front to back of my head (Coronal view)&lt;/li&gt;&lt;li&gt;Bottom left: Side of my head (Sagittal view)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The solid blue line you see in the views is the current plane I have selected to see. As you move this plane, all three images change accordingly. Notice how the blue line on the axial view (left panel) matches that of the coronal view (top right) in terms of position. The resulting image in the sagittal view (bottom left) is the image that corresponds with this selection I have made.&lt;/p&gt;&lt;p&gt;It&#39;s so incredibly clever and absolutely fascinating (to me at least)! I spent the best part of 2-3 hours looking at my own brain scans. I wish I knew what it all meant, but I plan on quizzing the experts when I get the chance. I&#39;d like to write a blog post all about it in the future. So, watch this space!&lt;/p&gt;&lt;p&gt;For those interested in doing the same the tools I used are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/nroduit/Weasis/&quot;&gt;Weasis&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://horosproject.org/&quot;&gt;Horos&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Horos takes it a step further, as it allows you to view your scan in 3D, and even &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1397459707422547968&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;export an animation&lt;/a&gt; to show all your friends at the next soirée you attend! So yes, if you ever fancy looking at your own insides in detail, remember you can request your CT and MRI scan data!&lt;/p&gt;&lt;h3 id=&quot;cloudy-head-seizures-friday-28th-may&quot;&gt;Cloudy head seizures - Friday 28th May&lt;/h3&gt;&lt;p&gt;So I think I&#39;ve started to recognise the signs of having a seizure. But this isn&#39;t an obvious type of seizure that started this whole saga off 5 weeks ago. This looks to be a different type that I touched on in a previous update. In my &#39;Seizure spreadsheet log&#39; I&#39;ve written:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Weakness in arms, &quot;cloudy&quot; thinking. Feel it around my face, tingling in forehead, almost throbbing sensation. Sleepy. Hard to keep my eyes open. Muscles changing in my face (odd tightening up from the middle). Feeling coming on for a number of hours. Feels like it is building up to a seizure. Brain feels &quot;heavy&quot;. Went to sleep for 40 mins, felt like something &#39;lifted&#39;, may have had another seizure in my sleep. Had the same out of body experience while sleeping.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It&#39;s hard to say, but I think I may have had another more obvious seizure while resting. I remember it happening so I did try to move my arms and they seemed fine. So who knows! Urghhh! But after my &#39;sleep&#39; for 40 minutes or so I felt a whole lot better.&lt;/p&gt;&lt;p&gt;Also, I&#39;ve noticed my left foot (heel) has started to hurt, which is one of my initial symptoms that I was blaming on sciatica. So I&#39;m going to be mentioning that at my next catchup with the team.&lt;/p&gt;&lt;h3 id=&quot;date-for-the-memory-test-tuesday-1st-june&quot;&gt;Date for the memory test - Tuesday 1st June&lt;/h3&gt;&lt;p&gt;So I&#39;ve now been given a date for the baseline memory test. This is happening on the 29th June, a whole month away. Not ideal, and I&#39;m a little torn with the date. In some ways I want it sooner to &quot;get it out the way&quot;, but in other ways I want it later. At least when it&#39;s later it gives me more time to &#39;prepare&#39;. I still feel I have so much general life administration left to complete before the &#39;big day&#39;.&lt;/p&gt;&lt;p&gt;The symptoms with my left foot still persist but tend to vary day to day. Because of this I may need to be given different drugs and / or have additional scans to see what Gary is up to. I&#39;d like to think he hasn&#39;t gotten even bigger in the last month, but it wouldn&#39;t surprise me!&lt;/p&gt;&lt;p&gt;So now I have the date. I&#39;m guessing the operation date will be 1-2 weeks after this. So around mid-July... or in other words around the time of the F1 at Silverstone... Damn you Gary!&lt;/p&gt;&lt;h3 id=&quot;back-on-the-steroids-thursday-3rd-june&quot;&gt;Back on the steroids - Thursday 3rd June&lt;/h3&gt;&lt;p&gt;Unfortunately over the past 2-3 days the symptoms in my legs and feet have returned. I was hoping that the anti-seizure drugs were the ones keeping these at bay, but it turns out it must have been the steroids which I came off a number of weeks ago. The team have decided to put me back on a short dose so as to improve my mobility (which I&#39;m very grateful for!). I&#39;ll be halving the dosage in a week, then likely coming off them again the week after. Since it takes a while for them to leave your system I&#39;m hoping that takes me up very close to a surgery date, at which point the root cause of the issue can be ejected from one&#39;s skull!&lt;/p&gt;&lt;h3 id=&quot;reduced-seizures-and-steroid-side-effects-monday-7th-june&quot;&gt;Reduced seizures and steroid side effects - Monday 7th June&lt;/h3&gt;&lt;p&gt;So the good news, the seizures seem to have reduced a lot over the past few days. I&#39;ve had very little to write in my seizure log for the team. I like this direction of travel. Unfortunately as happened before the steroids have decimated my sleeping pattern. I&#39;m currently writing this update at 00:40 and don&#39;t feel tired at all. And yesterday I went to bed / sleep at around 3am, then woke up at 6:50am. Apparently under 4 hours of sleep a night isn&#39;t the ideal amount you should be having! Oh well, at least I&#39;m getting lots more stuff done!&lt;/p&gt;&lt;h3 id=&quot;neurologist-appointment-thursday-10th-june&quot;&gt;Neurologist appointment - Thursday 10th June&lt;/h3&gt;&lt;p&gt;Due to the seizures I&#39;ve been having I was referred to the Neurologist who is part of the multidisciplinary team who are looking after me. It was a somewhat confusing meeting, but also very interesting that lasted around 40 minutes.&lt;/p&gt;&lt;p&gt;From previous conversations I&#39;ve had with doctors, I was under the impression that the tumor was likely causing the issues I&#39;d seen with walking in October (left back, leg, foot pain etc). But upon talking to the Neurologist and describing my symptoms, they seem to think that isn&#39;t the case and it&#39;s all purely coincidental. Confusing! I&#39;d prefer it to be the tumor causing it to be honest. Having 1 major brain operation and solving 2 problems, sounds like a good deal! But alas, we shall see when Gary finally buggers off into the hospital incinerator. Only then shall we know for sure!&lt;/p&gt;&lt;p&gt;What was interesting for me was the conversation around classifying my seizures. I&#39;ve been keeping a seizure log (yet another fun Google spreadsheet!). All fairly standard columns:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Date&lt;/li&gt;&lt;li&gt;Time&lt;/li&gt;&lt;li&gt;What was I doing&lt;/li&gt;&lt;li&gt;(Copious) Notes&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now I&#39;d already started to notice this myself: not all my seizures are alike, and since taking the anti-seizure medication I&#39;d started having different symptoms. The Neurologist confirmed this today. So I&#39;ve now added another column:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Is it a seizure involving the ‘core features&#39;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A convoluted column title I know, but I&#39;ll explain what It means. The ‘core features&#39; are effectively the most obvious (and serious symptoms). For me I&#39;ve identified them as:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The feeling of being ‘outside my own head&#39;&lt;/li&gt;&lt;li&gt;The emotionally driven feeling of everything being bigger than my own self (existentialism)&lt;/li&gt;&lt;li&gt;Any loss of motion and control over my body&lt;/li&gt;&lt;li&gt;Loss of consciousness (not happened as of yet thankfully!)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Basically if I have a seizure that displays these core features, then it is logged as one. The other seizures I&#39;m having are still logged in detail, but not listed as a ‘core feature&#39; seizure.&lt;/p&gt;&lt;p&gt;So why is this important? Well, without this classification every little seizure suddenly contributes to what looks like an escalating number of seizures. If this is the case then getting the balance for medication is difficult. I want to be on enough to stop the ‘core feature&#39; seizures, but not so much that I have other side effects. This classification allows me to pull out the really important ones and therefore allow the team looking after me to see if they indeed are escalating, or if they are being managed.&lt;/p&gt;&lt;p&gt;So there we go, top tip if you have a brain tumor. Keep a seizure log and look for patterns in what happens when they occur!&lt;/p&gt;&lt;h3 id=&quot;a-core-feature-seizure-friday-11th-june&quot;&gt;A &#39;core feature&#39; seizure - Friday 11th June&lt;/h3&gt;&lt;p&gt;After the call yesterday it was all going so well. Nothing I would class as a &#39;core feature&#39; seizure for the best part of 2 weeks. Unfortunately, one happened this evening at about 10 past 9pm. Watching the TV sitting on the floor, my wife just paused the TV to get something from the kitchen then I felt it. Again like an emotional response. Something was about to happen. I could feel it coming on. I tried to turn around and &#39;shake myself&#39; out of it and convince myself I could stop it from happening. But within 5 seconds I&#39;d lost control of my upper body and was shaking gently back and forth, unable to stop it.&lt;/p&gt;&lt;p&gt;As I&#39;ve been told to see if my speech was affected I made sure my wife could hear me this time. She had walked in the room and not noticed what was happening so played the TV. I just started saying &quot;Pause it&quot;, &quot;Pause it&quot; &quot;Pause it&quot; over and over again, then eventually I said &quot;I&#39;m having a seizure&quot;. Now I &lt;em&gt;could&lt;/em&gt; have stopped myself from speaking, but I wanted to make sure we knew what the effect of it was on my speech, so I continued talking. It turns out she could hear me fine, no slurred speech. Said I sounded quite normal. I guess that&#39;s a good thing? In total from the feeling to the end I&#39;d guess it lasted about 30-40 seconds. Even 2 hours after it I still feel weak across the back of my shoulder blades and upper arms.&lt;/p&gt;&lt;p&gt;I&#39;m hoping it was due to it being a long and busy week, maybe just tired. Or the fact that I&#39;m now weaning myself off the steroids on the request of the Neurologist. I&#39;m not sure! But to be honest, if I only get them once every 2 weeks, I&#39;ll take that at the moment!&lt;/p&gt;&lt;h3 id=&quot;a-sleepy-seizure-thursday-17th-june&quot;&gt;A sleepy seizure - Thursday 17th June&lt;/h3&gt;&lt;p&gt;All has been quite quiet on the seizure front for the past few days. I&#39;ve been gradually feeling weaker as the steroids pass through my system and my body takes over producing whatever it needs to produce... (can you tell I&#39;m an expert in this doctor stuff!)&lt;/p&gt;&lt;p&gt;I&#39;ve been getting very tired, very quickly. One moment I&#39;m fine, 5 mins later I can hardly keep my eyes open. This happened around 5pm, just after having dinner with the kids. So I needed to go sit down in the living room. Within a few minutes I&#39;d fallen asleep. While sleeping I&#39;m 99% sure I had a seizure. Unfortunately due to the fact that I was asleep I&#39;m struggling to classify it, as I&#39;m unsure if I lost control of my arms etc, since I wasn&#39;t moving.&lt;/p&gt;&lt;p&gt;The reason I believe I had one was because I remember coming too, and it felt like my brain split in two. Suddenly a pressure was released and my head felt ‘open&#39;. I didn&#39;t even realise I had that pressure there; it was only once it had been released I noticed the difference. I woke up a few minutes later and felt so much better, so maybe that&#39;s how it works? The ‘pressure&#39; builds in my head and I don&#39;t feel better until it is released. I&#39;m not sure, I&#39;m still getting used to the whole having seizures, and what triggers them.&lt;/p&gt;&lt;h3 id=&quot;an-epiphany-friday-18th-june&quot;&gt;An epiphany - Friday 18th June&lt;/h3&gt;&lt;p&gt;It&#39;s hard to stop yourself thinking about everything that is going on. It&#39;s one of the reasons I enjoy continuing to work. I get to escape from thinking about the current situation and feel ‘normal&#39; for a short period of time. Well, at least until I remember what is happening again…&lt;/p&gt;&lt;p&gt;So on Friday I had an epiphany about the whole situation. I came to the realisation that I&#39;m dying. Now I know what you&#39;re thinking… &quot;Damn, that got dark very quickly!&quot; I came to the conclusion via a little thought experiment.&lt;/p&gt;&lt;p&gt;Imagine the wonders of modern medicine didn&#39;t exist. No drugs, no ability to operate and remove Gary. What would be happening to me in this situation? The reason I know about any of this at all is because the tumor has got to a sufficient size to cause the seizures, so it must have crossed some tipping point recently. The tumor would be getting bigger, the number of seizures I&#39;d be having would be ever increasing. I believe the tumor would just be getting bigger and bigger, putting more and more pressure on my brain until it eventually caused so many issues that I passed away. I&#39;m actually on a path in my life where this would be happening to me, but thankfully I have options to try to change this path.&lt;/p&gt;&lt;p&gt;I like to think of all this as water slowly filling a bucket. If the bucket overflows, bad things happen. At the moment the drugs have created a hole in the bucket to help stop it overflowing, but eventually the bucket will overflow, even with the hole. The only way to stop it from overflowing is to stop the water, and that basically means removing Gary. So thank goodness for modern medicine, else there&#39;d be overflowing buckets all over the place!&lt;/p&gt;&lt;p&gt;Well that was an uplifting read wasn&#39;t it? Thanks for reading, have a great rest of the day! :)&lt;/p&gt;&lt;h3 id=&quot;a-very-good-roast-dinner-sunday-20th-june&quot;&gt;A very good roast dinner - Sunday 20th June&lt;/h3&gt;&lt;p&gt;Due to covid I&#39;d not been to see my sisters new house. Not so new anymore since she moved in in February 2020! With restrictions gradually lifting and testing readily available we&#39;d decided to go visit and have a Sunday dinner. The kids loved the adventure, investigating the house and going to visit a new park just 5 mins away.&lt;/p&gt;&lt;p&gt;We ate around midday. It had been a while since having roast dinner, especially since it was beef, so I may have eaten a little too much too quickly. About 5 minutes after finishing while just chatting at the table I suddenly felt incredibly tired. I simply couldn&#39;t keep my eyes open. My arms felt heavy and I found I couldn&#39;t concentrate on the flow of the conversation. There was no immediate seizure, but it felt like something was happening. Thankfully my sister had a spare room with a bed so I decided to go lie down to try to sleep it off.&lt;/p&gt;&lt;p&gt;I woke up around 1.5 hours later feeling much better. As I&#39;ve mentioned a couple of times before, I may have had a seizure in my sleep. The ‘pressure&#39; in my head seemed to lift it one point while sleeping, I remember it happening. It isn&#39;t really ‘pressure&#39; as such, but it&#39;s hard to describe. Something ‘lifted&#39; and I suddenly felt much better.&lt;/p&gt;&lt;p&gt;So there you have it. My sister now has quite a claim to fame. Her roast dinners are so good they gave her brother a seizure!&lt;/p&gt;&lt;h3 id=&quot;the-baseline-memory-test-tuesday-29th-june&quot;&gt;The baseline memory test - Tuesday 29th June&lt;/h3&gt;&lt;p&gt;At the beginning of the month I was told a date of the 29th of June for my memory test, so the whole month we&#39;ve been waiting for this day. I wouldn&#39;t say I&#39;d been nervous. Really just anxious to get it ‘out the way&#39;. I had no idea what was involved. All I knew was it was quite intensive and it would last a few hours. Thankfully you can&#39;t revise for a memory test, as it did feel like being back at school waiting for an exam. The testing involved a chat with the neuropsychologist for 1 hour and then some intensive testing that lasted 2 hours.&lt;/p&gt;&lt;p&gt;The first part was easy. I enjoyed chatting about the situation to test myself and see what I could remember. And also to verbalise my understanding of the situation. Now I&#39;m pretty sure the doctor had read all my medical notes, but there were points in the chat where I felt she was feigning ignorance about certain details. I&#39;m guessing this is a tactic used to try to get me to explain my thoughts and feelings so they gather a greater understanding of my current mood and attitude to the whole situation. Not that I mind at all, it&#39;s good to talk about all this. I often find I don&#39;t know how I&#39;m feeling until I try to explain it to another person.&lt;/p&gt;&lt;p&gt;The actual testing was really interesting. I was given around 12-14 different tests over a 2 hour period. Some were more difficult than others. There were tests that involved pronouncing words, and also remembering words. The tests always started very easy, then gradually got harder. So you&#39;d start off thinking how smart you were, then by the end think the complete opposite! But I guess that&#39;s the point when trying to look for a baseline result, each and every person doing the tests will find some more difficult than others.&lt;/p&gt;&lt;p&gt;I personally found one set of tests quite difficult. It involved thinking of as many words starting with a particular letter in 60 seconds. The only rules were they couldn&#39;t be a person&#39;s name or places. Any other word in the english language starting with that letter. There are that many surely it can&#39;t be that hard! Well apparently not. For some reason my brain was insistent on thinking of swear words starting with whatever letter I was given. So each and every time I&#39;d think to myself, &quot;No I can&#39;t say that word, it&#39;s rude…&quot;. Once you get into that little game with your brain (&quot;say that, it would be funny!&quot;), It&#39;s hard to stop! Thanks brain! I think I could only think of between 10-15 words in 60 seconds. Not many at all so I&#39;d say I&#39;m below average for that test!&lt;/p&gt;&lt;p&gt;The easiest tests I found were the ones involving patterns. Look at this sequence of colours and shapes then pick the next one in the sequence from a list. I think I did okay in those tests, but who knows! Maybe I&#39;m below average for those too.&lt;/p&gt;&lt;p&gt;The testing was intentionally hard as this is to try to gauge where you&#39;re at and set that baseline. After surgery they can then repeat the test and look at the difference between the two sets of results. I guess since there were lots of different types of tests, if they see a drop in the results for one type of test, it suggests a change in a certain area of the brain. It&#39;s all very clever!&lt;/p&gt;&lt;p&gt;Now with this date out the way, it&#39;s just waiting on a date for surgery. All I know is that it is likely to be at some point in July. So July is going to be an interesting month!&lt;/p&gt;&lt;h3 id=&quot;shielding-sunday-4th-july&quot;&gt;Shielding - Sunday 4th July&lt;/h3&gt;&lt;p&gt;Unfortunately there&#39;s been a number of positive covid cases at my children&#39;s school. Year 2 and year 5 have been isolating at home for 10 days. We&#39;ve been testing every day, and have been for a drive-through PCR test. Thankfully they all came back negative!&lt;/p&gt;&lt;p&gt;With both children back at school, and covid literally so close (there are a number of schools in Oxfordshire having similar issues), my wife and I decided that it would be best if I were to shield. It&#39;s pretty crappy to have to avoid your own children, but having waited 2.5 months for a surgery date I really don&#39;t want to lose it because I catch covid. That would be bad!&lt;/p&gt;&lt;p&gt;So I&#39;m now staying with my parents. It&#39;s the first time I&#39;ve lived with them in 21 years! I&#39;m still working and plan to do so, up until I get a surgery date. This will happen on a Tuesday at some point in July. Every Tuesday they triage the patient list and make plans for the week. So I&#39;ll receive a phone call saying surgery will be on Friday! You don&#39;t get much notice, only a couple of days. Although that&#39;s probably a good thing. I don&#39;t want to be dwelling on the fact that they are going to be removing part of my brain in a couple of days for too long!&lt;/p&gt;&lt;p&gt;When folks have asked me how I&#39;m feeling about it all, It&#39;s hard to describe. In some ways I want the surgery to happen right away to at least &quot;get it out the way&quot;. But in others I don&#39;t want it to happen at all, as it really is a watershed moment in my life. There&#39;s always going to be a ‘pre-surgery&#39; and ‘post-surgery&#39; me from beyond that point. Fingers crossed those two versions of me don&#39;t change much!&lt;/p&gt;&lt;p&gt;I&#39;m siding on the fact that I actually want it done now. My family and I have been in limbo for a few months now, so it would be good to be able to move forwards and start to actually plan things! I&#39;ve gained so much respect for people who are on transplant lists for years. Waiting for hearts, kidneys, livers etc. I&#39;ve only been waiting 2.5 months and it has felt like an eternity! I can&#39;t even imagine living your life waiting years for surgery. Not being able to properly plan anything because &quot;what if we get the call?&quot;.&lt;/p&gt;&lt;p&gt;My next update will likely be when I have an actual surgery date. Not too long I hope!&lt;/p&gt;&lt;h3 id=&quot;no-news-tuesday-6th-july&quot;&gt;No news - Tuesday 6th July&lt;/h3&gt;&lt;p&gt;So having waited around all day for a call from the hospital, it unfortunately never came, so no surgery this week. Annoying but at least it gives me a little more time to get a few more things done! Let&#39;s see what next Tuesday brings!&lt;/p&gt;&lt;p&gt;In other news, there&#39;s been a few more reports of covid in schools in Oxfordshire so I still feel sheilding was the right thing to do!&lt;/p&gt;&lt;h3 id=&quot;still-no-news-tuesday-13th-july&quot;&gt;Still no news - Tuesday 13th July&lt;/h3&gt;&lt;p&gt;Mid-way through July now and still no news about surgery. I was hoping to have it at the start of the month so I could at least start the recovery process and maybe have the latter end of the summer to do something &quot;normal&quot;, (whatever that means in 2021!).&lt;/p&gt;&lt;p&gt;Being on the waiting list for hospital appointments is pretty stressful at the moment as &lt;a href=&quot;https://www.theguardian.com/politics/2021/jul/12/boris-johnson-urges-covid-caution-amid-warnings-of-1000-hospitalisations-a-day&quot;&gt;covid cases are surging&lt;/a&gt;, I really feel for the folks working in the NHS at the moment. It must feel like a never-ending tsunami of stress and unpredictability.&lt;/p&gt;&lt;p&gt;Well at least I&#39;ve been given a date of &quot;July&quot;, that&#39;s a lot more than some people have! I&#39;ve a sneaking feeling it will be right at the end. Fingers crossed the next few weeks go quickly. Although, saying that, these past few months have been the longest of my life!&lt;/p&gt;&lt;h3 id=&quot;between-life-and-death-thursday-15th-july&quot;&gt;Between Life and Death - Thursday 15th July&lt;/h3&gt;&lt;p&gt;Earlier in the week I discovered there&#39;d been a short series on Channel 4 here in the UK called &lt;a href=&quot;https://www.channel4.com/programmes/brain-surgeons-between-life-and-death&quot;&gt;&quot;Brain Surgeons: Between Life and Death&quot;&lt;/a&gt;. Reading the synopsis it all sounds very interesting. I thought it would give me some insight into what to expect on the day. Unfortunately I wasn&#39;t mentally prepared to watch it. I made about 5 mins into the program and had to stop. It hadn&#39;t even got to any of the surgery yet and was just talking about the people and their families. I think that brought it all home how serious this all is, not just for me but for everyone around me. What finished me off was seeing the guy lie down on the operating table and realising in only a few weeks time I&#39;m going to be doing exactly the same. It&#39;s not a pleasant position to be in.&lt;/p&gt;&lt;p&gt;I mean let&#39;s be honest here, the clue is in the title: &quot;Between Life and Death&quot;. It probably isn&#39;t the best thing to be watching weeks before major brain surgery! Saying that, I do plan on watching it post-surgery. I&#39;ll have a very unique perspective of what it&#39;s like from ‘inside&#39; the process. I can then compare notes from ‘outside&#39; the process too.&lt;/p&gt;&lt;h3 id=&quot;close-but-no-brain-surgery-friday-23rd-july&quot;&gt;Close but no brain surgery - Friday 23rd July&lt;/h3&gt;&lt;p&gt;So this week has been quite a rollercoaster. For the past 3 weeks I&#39;ve been mentally preparing for a date of the 30th July for surgery. Now, I realise none of this is set in stone, but as that&#39;s the only date you&#39;re given it tends to be the one you focus on. Unfortunately I had an update at the start of the week stating that the date was going to have to be pushed back. It sounds like I was actually quite close, but a few emergency cases came in that took priority. So I&#39;m back to the 2 week holding pattern (which really sucks!).&lt;/p&gt;&lt;p&gt;Saying that, I was given some interesting information during the conversations I&#39;ve had with the team. Another MRI scan has been requested as I&#39;m coming up to the 3 month point since diagnosis. But this isn&#39;t your usual MRI scan. It&#39;s actually being pitched as the pre-surgery scan. The neurosurgeon (and team) would like to understand what Gary has been upto over the past 3 months. From what I understand, this will be a more detailed scan that can be used for surgery planning. I don&#39;t know if this means it will take longer, or if they just conduct more scans / more detailed analysis(?) I guess a question for the test operator when I meet them soon.&lt;/p&gt;&lt;p&gt;I was also given information that the operation is likely going to take longer than expected. The team usually conducts 2 operations on the Friday of each week. But the neurosurgeon is now considering only operating on me on the day, due to the size of Gary. I&#39;ve yet to decide if this is a good or bad thing? Good in the fact that they can take their time to extract &quot;the prick&quot;, but bad in the fact that it is needed (and I&#39;ll be awake for most of it!).&lt;/p&gt;&lt;p&gt;Fingers crossed the 6th August materialises. Folks have said to me that the waiting is the worst part. I&#39;ve no way of knowing that at the moment, but I&#39;m happy to agree. It is pretty bad! Let&#39;s see what next week brings I guess!&lt;/p&gt;&lt;h3 id=&quot;pre-surgery-mri-scan-monday-26th-july&quot;&gt;Pre-surgery MRI scan - Monday 26th July&lt;/h3&gt;&lt;p&gt;Today I had my pre-surgery MRI scan. Now that suggests that I have a date for surgery, but alas no, not yet. I&#39;m supposed to have an updated scan every 3 months to look at what Gary has been up to, so instead of having 2 scans in a short period of time, the standard scan has been upgraded to a pre-surgery scan. This means that the scan takes a little longer than usual (around 40 mins). They take more detailed scans and I assume maybe a few more types? This means there&#39;s no ‘blocker&#39; for surgery now. When I get a slot, I&#39;m not waiting on an MRI scan. This is good news!&lt;/p&gt;&lt;p&gt;The scan itself was fairly easy. There was a slight miscommunication so I was waiting around 30 mins longer than needed, but compared to how long I&#39;ve been waiting for surgery it&#39;s nothing. At least it is done! 40 minutes stuck in a cold, confined tube that screams at you like a ZX Spectrum isn&#39;t the most relaxing experience. At 35 mins in they came in and injected the contrast into my arm via the cannula (I hate cannulas, so uncomfortable!). It was cold as it ran up my arm. A few seconds later I could swear I had a mini-seizure. I had the ‘emotional&#39; response I&#39;m getting used to, but since my head was tightly strapped to the MRI table and I was lying down in a confined tube, I&#39;m not sure if it affected my movement. I&#39;m going to raise this with the next doctor I speak to. Maybe it&#39;s a known side effect?&lt;/p&gt;&lt;p&gt;As another Tuesday rolls around, here&#39;s hoping I get any news on a date for surgery. The wait is really starting to get me down.&lt;/p&gt;&lt;h3 id=&quot;one-confusing-week-sunday-1st-august&quot;&gt;One confusing week! - Sunday 1st August&lt;/h3&gt;&lt;p&gt;The last week has all been a little crazy. On Monday I had my 3 month MRI scan, which was also the pre-surgery MRI scan too. I&#39;ve mostly been on annual leave from work as there&#39;s just so much going on it&#39;s hard to concentrate on anything else.&lt;/p&gt;&lt;p&gt;On Tuesday I was back to work for the day, but something unusual happened. I was in a meeting with my incredibly supportive line manager, but 30 mins into our 45 minute video call I had to stop quite abruptly. I completely lost my train of thought and felt like I was on the cusp of having a seizure. Thankfully I can now recognise the telltale signs of this happening. This is a worrying sign as work has been a haven for me over the past 3 months. I&#39;ve been able to zone out of &quot;real life&quot; for a few hours and concentrate on work, completely oblivious to the fact that Gary even exists. For a few hours at least! It now seems that even this little haven is eroding.&lt;/p&gt;&lt;p&gt;On Wednesday I had a call from the hospital with the results of my Monday scan. It&#39;s a bit of a mixed bag. The good news is that the tumor hasn&#39;t grown in the past 3 months. The less good news is that there&#39;s still no date for surgery. The bad news is that some swelling can be seen around the tumor, also known as cerebral edema. I think this explains the symptoms I saw on Tuesday.&lt;/p&gt;&lt;p&gt;Thursday and Friday were a bit of a writeoff to be honest. All day I wasn&#39;t feeling 100%. Incredibly sleepy, dizzy and at times quite confused. It&#39;s strange, and it comes on so quickly. One minute you feel fine, next minute it all changes for no apparent reason. Thursday evening was particularly bad, where I could barely keep my eyes open or even really speak to my wife. I was completely disconnected from reality and didn&#39;t have the energy to even try to re-engage.&lt;/p&gt;&lt;p&gt;On Friday evening it got so bad that my wife decided to ask for advice from the 111 team (non-medical emergency number). We went through the triage process and eventually the very nice on-call GP was sent out to see me. She was satisfied I didn&#39;t need to go to hospital but did want to phone the specialists in the oncology and neurology departments. On speaking to the on-call neurology consultant who looked at my scans they recommended that I should be back on steroids to reduce the swelling in my brain.&lt;/p&gt;&lt;p&gt;Now here&#39;s the difficult part. The team I&#39;ve been under the care of for the past 3 months have been very keen to take me off steroids when I&#39;ve been on them, since they are quite strong and have some nasty side effects. Steroids take around 2 weeks to leave your system once you take them. So I&#39;ve been stuck between a rock and a hard place. Take the steroids and I could potentially compromise a future surgery date. Don&#39;t take them and my symptoms could get worse.&lt;/p&gt;&lt;p&gt;Weighing up these options, it was 100% my decision NOT to take the steroids until I&#39;ve been given the all-clear from the team looking after me at the hospital. There&#39;s no way I want to lose the opportunity of surgery because I took a drug that is now in my system for weeks. So due to the fact that I was having unpredictable symptoms from the swelling, I&#39;ve moved back with my parents for a few days. At least they can easily keep an eye on me without having to keep two young children entertained.&lt;/p&gt;&lt;p&gt;So there you have it, quite an eventful week! I&#39;m seriously hoping that the upcoming week is the one where I&#39;m given a surgery date. I don&#39;t think I can cope with much more waiting around &quot;treading water&quot;. Roll on Tuesday. Fingers crossed &lt;em&gt;the&lt;/em&gt; phone call!&lt;/p&gt;&lt;h2 id=&quot;the-brain-fog-has-lifted-friday-6th-august&quot;&gt;The brain fog has lifted! - Friday 6th August&lt;/h2&gt;&lt;p&gt;Yet another crazy week with quite a few highs and lows. I&#39;ve been using some of my annual leave to get a few things sorted before the surgery. If I&#39;m honest, I haven&#39;t managed to do much for the past few weeks which is very frustrating. Being so tired and dizzy makes it hard to concentrate. I&#39;ve been sleeping a lot and very rarely left the house.&lt;/p&gt;&lt;p&gt;Tuesday came and went with no real updates about a surgery date which was very disappointing. I did hear back from the team at the hospital about the steroids, so from Wednesday morning I started on them again (to reduce the swelling around the tumour). Wednesday and Thursday there was some improvement like they were beginning to make a difference. I had much more energy and I could finally walk properly again (as the issue with my foot has returned).&lt;/p&gt;&lt;p&gt;But it was today (Friday) where I&#39;ve really felt better. The best I have felt in weeks to be honest. I realised while having a shower this morning that I was actually thinking and planning out my whole day ahead of time. Multiple streams of thoughts about everything I wanted to get done. I&#39;d not had this ability for a few weeks. It has literally been quite ‘single threaded&#39; (there&#39;s a web developer term for you). Basically I&#39;ve only been able to focus on the single task at hand, nothing more. I must admit I didn&#39;t notice this change, it was only once I ‘reverted back&#39; to feeling normal again I realised it had happened. So yes, I went into overdrive with work today because I could finally do things again! I worked until 7:30pm (naughty I know) getting tonnes of stuff sorted, and started making plans for the department&#39;s stance on legacy browsers (cough... IE11), which should come into effect in March to June in 2022. So planning almost 10 months ahead of time. I simply couldn&#39;t have done that even 3 days ago.&lt;/p&gt;&lt;p&gt;Next week is going to be interesting. I have a couple of meetings with the hospital to go over symptoms and then speak to the neurosurgeon again. I have lots more questions for the team. So let&#39;s see what next week brings! Maybe some clarity on a date, like I&#39;ve had from the steroids over the past few days. That would be nice!&lt;/p&gt;&lt;h3 id=&quot;another-mixed-few-days-sunday-8th-august&quot;&gt;Another mixed few days - Sunday 8th August&lt;/h3&gt;&lt;p&gt;Well it has been an odd weekend symptoms wise. Friday was a very good day. The best one I&#39;d had in a while actually. I could finally think clearly again. Unfortunately I think I went a little bit overboard!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Saturday&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I really paid the price on Saturday. As Friday had gone so well, we decided to head out to an open day event for a charity. We were under the impression it wouldn&#39;t be particularly busy, but unfortunately not. It seemed like everyone had turned up. This isn&#39;t a bad thing in terms of covid as it was all outdoors and we all kept our distance. It was just the sheer number of people, sounds, noise, interactions… the general busyness of it all that broke my brain.&lt;/p&gt;&lt;p&gt;I lasted about an hour then had to admit defeat and go back to the car to sleep with some quiet music on for 2 hours. I believe this was a mixture of the tumour, swelling, and having just started taking steroids. Apparently steroids can heighten your senses, making sounds louder, colours brighter etc. I think it was a combination of it all that my brain just needed to get out of the situation.&lt;/p&gt;&lt;p&gt;Thankfully after a couple of hours of rest and getting home I felt much better, and ended up doing virtually very little for the rest of the day.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Sunday&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;After a great Friday and poor Saturday we decided that I best not risk doing much, so I mainly pottered around the house looking for little bits to keep me occupied. I&#39;ve certainly noticed that I&#39;m much more comfortable and settled while at home. I&#39;d assumed it was all a part of the past 18 months and the pandemic, but I&#39;m beginning to think it is all interrelated. I think (unknowingly) to me this tumour has been developing and growing and it has been changing how my brain works and thinks. Although I don&#39;t feel anxious about leaving home, there&#39;s always something in the back of my head where I can only fully relax once I&#39;m back.&lt;/p&gt;&lt;p&gt;Now, day to day you don&#39;t notice this, why would you after all? Such small changes. It&#39;s just like watching your children (or pets) grow up. Day to day you see no change, but looking back everything has changed. So when these subtle differences are compounded over months (and even years), then you see the huge change staring you in the face. The more I think about it, the more it makes sense. My brain is actively looking for a place with the least cognitive load. Familiar surroundings and people with complete predictability. This equates to a brain that doesn&#39;t need to do much (which is good when you have a brain tumour!).&lt;/p&gt;&lt;p&gt;Annoyingly on the flip side, the steroids seem to give me enough energy to put me on edge (almost twitchy), but then I don&#39;t have the energy or brain capacity to do anything particularly useful with it. Stuck between a rock and a hard place really. So the day was mainly resting and mentally preparing myself for another week of uncertainty.&lt;/p&gt;&lt;h3 id=&quot;the-week-that-will-never-end-wednesday-11th-august&quot;&gt;The week that will never end! - Wednesday 11th August&lt;/h3&gt;&lt;p&gt;Well, the title says it all really. This feels like it has been a very long week. I&#39;ve had 2 important meetings at the start of the week, and after those it&#39;s hard to believe there are still more days left to go!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Monday&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Since starting back on the steroids last week to reduce the swelling on my brain the team has wanted to keep a close eye on me. If there&#39;s one takeaway from the past 3 months it&#39;s that they don&#39;t want me on them! That&#39;s fine with me, I&#39;m happy to listen to the experts!&lt;/p&gt;&lt;p&gt;So I had a general catchup with the nursing team going over my symptoms since I started back on them again. I explained the highs and lows of the weekend and it all sounds like classic steroid side effects. One day you feel on the top of the world, the next day you are back in the depths. When you couple it together with the tumour, swelling, and the fact that steroids obliterate my sleeping pattern (3-4 hours a night when on them), it all makes sense really. I&#39;m on mild sleeping tablets to combat the issue, but they don&#39;t work too well.&lt;/p&gt;&lt;p&gt;The rest of the call was mainly to prepare me for the Tuesday call with the neurosurgeon. I&#39;d already started making a list of questions, but off the back of the conversation I now had a few more. The more information you can get from the person who will actually be operating on you the better really!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tuesday&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I worked in the morning as the call wasn&#39;t until the afternoon. For some reason I was very nervous about the whole thing, so I was glad to have the distraction. I&#39;m not quite sure why I was so nervous, but it&#39;s always in the back of my head: &quot;maybe they made a mistake, and the 2nd scan will show a much worse prognosis&quot;. Unlikely I know, as I&#39;m sure they would have mentioned it, but it&#39;s hard to stop thinking about it!&lt;/p&gt;&lt;p&gt;But overall the 45 minute conversation went really well! There&#39;s been no growth in the tumour size over the past 3 months which is amazing news. The tumour still looks to be of the type oligodendroglioma. The fact it hasn&#39;t grown point towards it still being grade II and not anaplastic oligodendriogliomas (grade III). We still won&#39;t know this for sure until 10 days after surgery, as that&#39;s how long it takes to get biopsy results back.&lt;/p&gt;&lt;p&gt;The swelling around the tumour isn&#39;t as bad as thought. Only slight swelling really. But this then points to the fact that the symptoms I&#39;ve been getting (dizziness, ‘fuzzy head&#39;, loss of concentration, disconnection with the world etc) all relate to the tumour itself. I&#39;m not sure which I prefer it to be, the tumour or the swelling!&lt;/p&gt;&lt;p&gt;I got to ask lots of great questions and get most of the answers I needed. One of the fundamental takeaways is that I &quot;must not get covid&quot;, (direct quote from the neurosurgeon). I&#39;m more than happy to keep to that! Having been double jabbed and not really going out, I&#39;d hope the chance is pretty slim anyway.&lt;/p&gt;&lt;p&gt;The main discussion was around a date for surgery, and this came up almost right away. The team were very apologetic that it had taken so long, but the pressures on the whole system at the moment are large. Now as I seem to have a grade II tumour that is large (but not aggressive) this drops me down the priority list somewhat. There are some types of brain tumour like the &lt;a href=&quot;https://en.wikipedia.org/wiki/Glioblastoma&quot;&gt;glioblastoma&lt;/a&gt; that are really really aggressive and can&#39;t be cured. The neurosurgeon explained that in these cases sometimes you only have a matter of days / weeks where you can make &lt;em&gt;any&lt;/em&gt; difference to a patient&#39;s life expectancy! So at the moment I&#39;m now top of the list, barring any emergency cases coming in. So in theory I should &lt;em&gt;hopefully&lt;/em&gt; be operated on in the next two weeks(!). Off the back of the meeting we are looking at a date of &quot;August&quot;. If this is the case, it fits well with the children going back to school in September, as I can see the risk of getting covid in this period increasing once they are back. And what with my instructions &quot;not to get covid&quot;, these two worlds don&#39;t fit together!&lt;/p&gt;&lt;p&gt;I asked about how big he thought my tumour was. I&#39;ve already exported a 3D animation of Gary which you can &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1397459707422547968&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;see here&lt;/a&gt;, but I wasn&#39;t sure actually how big ‘he&#39; is in terms of numbers. The neurosurgeon estimates that ‘he&#39; is taking up around 10-15% of my brain. That&#39;s pretty impressive! So I only have the capacity to use 85-90% of my brain anyway. I also asked the question if this is the type of tumour I could have had since birth, and I&#39;ve just grown up with it? The answer to that was a blunt &quot;no&quot;, so that&#39;s the end of that conversation then!&lt;/p&gt;&lt;p&gt;I found out a more about the operation itself and recovery period:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Operation&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;It is likely to take 6-9 hours, with me being awake for approximately 6 of those hours. That sounds like a hell of a long time to me, but the neurosurgeon assures me it will go quickly. He said I&#39;d be surprised how quickly it passes, so watch this space. I&#39;ll let you know after the event! A successful operation is where he manages to remove around 80% of the tumour, leaving the rest to be removed via other means (radiotherapy / chemotherapy / additional surgery).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recovery&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Now because the tumour is so large and deep into my brain, it comes with a few risks (as I&#39;ve mentioned before in the original post). One of the main risks is the fact that the tumour is wrapped around my motor cortex, which is the part of the brain that controls limb movement. As Gary is sitting in my right frontal lobe, any issues will occur on my left hand side (since the brain is reversed in this respect).&lt;/p&gt;&lt;p&gt;The neurosurgeon explained that there is something like a ‘switch&#39; that can effectively turn on and off if the brain is damaged (or changed). During the operation there&#39;s no warning that this will occur. One moment I could have full control over my left arm / hand, the next moment nothing. Quite a scary thought really! Thankfully in most cases this is only temporary and lasts 10-14 days. It&#39;s very rare that it is permanent. Over time, as the brain re-adjusts to being without the tumour, it relearns how to process limb movement and the ‘switch&#39; is turned back on. If this were to happen, my seemingly ridiculously short 24-48 hour stay in hospital will turn into a couple of weeks in rehabilitation. So there&#39;s just another unknown to throw into the mix!&lt;/p&gt;&lt;p&gt;So that was mainly what I learned from the first two days of the week! Quite a lot to take in, and there are still 3 days left to go! I plan to take it a little easy to process it all and start mentally preparing myself for the next two weeks, where there&#39;s a lot that can happen very quickly indeed. I feel I&#39;m about to reach a watershed moment in my life. There&#39;s forever going to be a pre-surgery me, and post-surgery me on the horizon.&lt;/p&gt;&lt;p&gt;Tune back in soon for more on the eviction of Gary ‘the prick&#39; Glia!&lt;/p&gt;&lt;h3 id=&quot;houston-we-have-a-date-tuesday-17th-august&quot;&gt;Houston, we have a date! - Tuesday 17th August&lt;/h3&gt;&lt;p&gt;So it&#39;s been over three months since I was first diagnosed, and I must admit I&#39;ve no idea where the time has gone! In some ways it feels like it has gone quickly, but in others it feels like it has been the longest three months of my life! Living your life in weekly chunks isn&#39;t much fun. Waiting for every Tuesday to come around for a surgery date is quite stressful. The day comes and goes with a whole range of emotions. A mixture of anxiety, excitement, all the way through to disappointment and sadness, then rinse and repeat for another week.&lt;/p&gt;&lt;p&gt;Thankfully the waiting now seems to be coming to an end as I&#39;ve finally been given a date for the operation: &lt;strong&gt;Friday 27th August&lt;/strong&gt;, or G-Day as I&#39;m calling it. G-Day stands for &quot;Gary get the f*ck out of my head&quot;-Day. The plan is for me to go into hospital the day before and stay overnight, with the operation happening on the Friday morning. It should last approximately 8-9 hours. Beyond that is a big unknown, so we&#39;ll have to see how it goes!&lt;/p&gt;&lt;p&gt;Unfortunately, there is always a chance that it could be cancelled at the last minute, or the hospital doesn&#39;t have a bed for me. If this happens there are multiple options. The nurse explained: &quot;Plan A is the aim, but plans B-E also exist too&quot;. We will only know the plan I&#39;m sticking to on Thursday. It will be a relief to get a hospital bed, as that&#39;s the final step before surgery!&lt;/p&gt;&lt;p&gt;In other news: I also found out that during recovery I&#39;m going to need to wear deep vein thrombosis (DVT) stockings for 1 month(!) after surgery. Night and day, 24/7! Apparently the risk of blood clots post-brain tumor surgery is high, so to reduce the risk the stockings are required. Now as a person who doesn&#39;t even like to wear socks to bed, this sounds horrendous! But if it means I don&#39;t get blood clots, well I guess it&#39;s a thing I&#39;m going to need to get used to. I think the next few months are going to be very difficult. A character building experience some might say...&lt;/p&gt;&lt;h3 id=&quot;last-day-at-work-friday-20th-august&quot;&gt;Last day at work - Friday 20th August&lt;/h3&gt;&lt;p&gt;Today has been a strange day. It was my final day &quot;in the office&quot;, as I&#39;m stopping work to prepare for my operation next week. I have to admit I&#39;m feeling a little sad about it. Work has been a real safetynet over the past few months. When I needed a distraction from all this, and an &quot;injection&quot; of normality I could always fall back on work. The work isn&#39;t easy. In fact it is incredibly frustrating at times. But that&#39;s part of the distraction. Being able to make plans for the department in the coming years is time consuming and slow, but ultimately very rewarding. I&#39;ve handed over as much as I can and tried to remove myself as &quot;a blocker&quot; from as many areas as I can, but it could be that certain areas stall until I get back to finally drive them forwards. Here&#39;s hoping for a quick recovery and staggered return to &quot;normality&quot;, whatever the hell that is in 2020/2021!&lt;/p&gt;&lt;h3 id=&quot;houston-we-have-a-seizure-friday-20th-august&quot;&gt;Houston, we have a seizure! - Friday 20th August&lt;/h3&gt;&lt;p&gt;Just when Friday wasn&#39;t strange and emotional enough, Gary steps in and makes things worse. 6:30pm rolls around and I&#39;m making a cup of tea in the kitchen. I suddenly feel something &quot;isn&#39;t right&quot;. I&#39;m standing and realise I need to sit down quickly before it happens. I start to walk the few meters across the kitchen to the nearest chair but don&#39;t make it in time. My body goes into a focal (partial) seizure.&lt;/p&gt;&lt;p&gt;As I&#39;m walking I say to my wife: &quot;I need to sit down&quot;, but the words came out slurred (they sounded fine to me). Luckily I manage to lean on the kitchen worktop when it happens, so as not to fall over. I gently start to rock back and forwards, this is out of my control. At that point Claire told me to keep speaking, as it&#39;s good to know what effect the seizure is having on my speech. At that moment I actually struggled to speak. That isn&#39;t through lack of ability, it was through lack of &quot;being bothered&quot;, would be the best way to describe it. There seemed like a real disconnect between myself and reality which I didn&#39;t have the energy or inclination to break down at the time.&lt;/p&gt;&lt;p&gt;The seizure seemed to pass quite quickly. Probably 20-30 seconds in total. I managed to sit down and take in what had just happened. Annoyingly I didn&#39;t feel any ‘release&#39; of pressure in my head which I&#39;ve had in the past sometimes. I actually didn&#39;t feel any difference at all.&lt;/p&gt;&lt;p&gt;So this was the first clear ‘core function&#39; seizure I&#39;ve had since the 11th June. There are two intriguing similarities between them: they both happened on a Friday after work, and both happened while coming off steroids. Now this may be a complete coincidence? I have no idea. Maybe I&#39;ll ask the team next week. Next Friday can&#39;t come soon enough!&lt;/p&gt;&lt;h3 id=&quot;time-slows-down-sunday-22nd-august&quot;&gt;Time slows down - Sunday 22nd August&lt;/h3&gt;&lt;p&gt;I knew this would happen. As soon as I had a date everything would shift. Suddenly time slows down as I&#39;m no longer living each day waiting for a phone call, I&#39;m living each day with a future date on ‘x number of days&#39; away. It certainly doesn&#39;t help that sleeping is difficult. Only sleeping 3-4 hours a night makes for very long days! I think this may be the longest week of my life. I need to find a distraction (that isn&#39;t work) to keep me occupied. Roll on next weekend and the Belgian Grand Prix. I will never be so happy as to be able to watch a Grand Prix knowing surgery is over!&lt;/p&gt;&lt;h3 id=&quot;cancelled-surgery-1-tuesday-24th-august&quot;&gt;Cancelled surgery #1 - Tuesday 24th August&lt;/h3&gt;&lt;p&gt;So today was a bit of a roller coaster. I woke up very early (I&#39;m still not sleeping too well), as I needed to complete my PCR test at 7am. This works out as 3 days before surgery on Friday. Once completed I went for a little walk to post it in a priority post box. I saw absolutely nobody at that time of the morning, which was handy for COVID. I just so happened to walk past my old secondary school too, which was a trip down &lt;a href=&quot;https://www.merriam-webster.com/dictionary/memory%20lane&quot;&gt;memory lane&lt;/a&gt;. Not much looked to have changed apart from some of the dilapidated classrooms have been knocked down and replaced with sports courts. I remember them being bad in 1998, I hate to think what they would have looked like 23 years later!&lt;/p&gt;&lt;p&gt;Unfortunately the positive start to the day wasn&#39;t to last. A few hours later I received an email from the hospital stating that my operation on Friday had been cancelled due to a staffing issue with the team. I do wonder if this is all related to COVID, as it seems very strange that an operation planned 1.5 weeks in advance suddenly doesn&#39;t have an important member of the team available. This is disappointing, but in all honesty I&#39;d much prefer they are 100% happy with the team and everyone is available, rather than a ‘settling&#39; for a team just to get the operation ‘done&#39;. So for the sake of another few weeks (after waiting 4 months), that&#39;s fine by me!&lt;/p&gt;&lt;p&gt;Note that I&#39;ve called this update ‘Cancelled surgery #1&#39;. I&#39;m hoping that there won&#39;t be a #2!&lt;/p&gt;&lt;h3 id=&quot;the-end-of-a-long-week-friday-27th-august&quot;&gt;The end of a long week - Friday 27th August&lt;/h3&gt;&lt;p&gt;Today was supposed to be the day of my surgery. Unfortunately it was cancelled on Tuesday. In some ways I&#39;m glad it was cancelled so early into the week, as that has given me time to get used to it all. If it had been cancelled the day before, that would have been even worse.&lt;/p&gt;&lt;p&gt;I&#39;ve now been signed off work by my GP until my surgery date. I&#39;ve managed to work for over 3 months, but the combination of the drugs and tumour are starting to take its toll. I can have dizzy spells and get tired very easily. One minute I feel fine, 5 minutes later I&#39;ll feel totally different, almost drugged / drunk (I don&#39;t drink). This all has had an impact on my concentration and work. This is especially true now that I&#39;m off the steroids. They gave a little boost that enabled me to power through, but without them it&#39;s difficult.&lt;/p&gt;&lt;p&gt;So all in all, a very long week. I&#39;ve started making plans for the coming weeks as I&#39;ve been given a very loose date of the 10th September for surgery. It&#39;s not guaranteed, but it&#39;s the only date we have to work towards at the moment, so it will have to do. With the children going back to school on the 2nd September I can see this being an interesting few weeks ahead!&lt;/p&gt;&lt;h2 id=&quot;a-promising-end-to-the-week-saturday-4th-september&quot;&gt;A promising end to the week - Saturday 4th September&lt;/h2&gt;&lt;p&gt;It has been a very busy week, lots going on! In preparation for recovery we&#39;ve had a third of the garage converted into an office. This frees up a room that can be used as a spare bedroom. This will be really useful if I need friends and family to stay and help during this period. Because of this, I&#39;ve been gradually cleaning up and making sure we can find things. I can&#39;t explain how frustrating it is not to be able to find tools in the garage. A 5 minute job takes 20-30 minutes just because I can&#39;t find the tools! Saying this, it&#39;s all looking great, not 100% yet, but I&#39;ll be able to potter about during recovery and finish it all off.&lt;/p&gt;&lt;p&gt;My kids are now back at school so I&#39;m self-isolating at my parents. The risk of me getting COVID has jumped since we can no longer control or monitor who the kids are interacting with. Since it takes around 48 hours for COVID to become detectable using a lateral flow test, I could easily catch it and not know about it for a few days. This would be bad in terms of the operation. It puts me at risk, as well as the whole 12-14 people team who will be involved in the operation.&lt;/p&gt;&lt;p&gt;I had some really positive news on Friday when the neurosurgeon who will be forcefully ejecting Gary, phoned me. He phoned for 3 reasons:&lt;/p&gt;&lt;p&gt;To apologise for the cancellation on the 27th. To check on how my seizures are at the moment. To see if I would be interested in taking part in a new feasibility study related to more detailed brain imaging.&lt;/p&gt;&lt;p&gt;In relation to point 1, although this was disappointing, I really appreciate the fact that he made the difficult decision to cancel surgery. It was made because a critical team member wasn&#39;t available. I&#39;d much prefer to go into a long operation with the correct team, than a team pulled together just to ‘get the job done&#39;. This makes me feel more comfortable about the surgery, both from the fact that I know the best team is involved, and also that the neurosurgeon has the team he wants around him. This must be a confidence boost for him too. Not that I expected (or even needed) an apology, it&#39;s nice to be given it. The NHS are doing a fantastic job considering the tidal wave of craziness they have been under for the past 18 months!&lt;/p&gt;&lt;p&gt;Point 2 is an interesting one. It&#39;s been a few weeks since I&#39;ve had a ‘core feature&#39; seizure, but that doesn&#39;t mean my brain hasn&#39;t tried! There are certain days where I feel quite ‘detached&#39; from the world. If not ‘detached&#39;, then I can be quite fuzzy headed where concentration is hard. I&#39;m no expert, but this seems to me as though my brain is trying to have a seizure, but the drugs I&#39;m on are stopping it. I keep a detailed seizure log and have been keeping the hospital team constantly updated with how I&#39;m feeling day to day.&lt;/p&gt;&lt;p&gt;There have been discussions this week about upping my drugs, or even putting me on a second drug to try to combat these episodes during the operation. If I were to have one during the operation it could be bad! Although now it sounds like the decision has been made not to change my medication. I&#39;m personally not concerned either way. I&#39;m happy to follow the team&#39;s advice. They are the experts afterall! According to the neurosurgeon it could be that the operation is adjusted to account for these ‘almost&#39; seizures, this could mean I&#39;ll be awake for less time, which is safer if I do have a seizure. But it also means they won&#39;t be able to take out as much of the tumour because they won&#39;t know if they are inadvertently taking out ‘good&#39; parts of my brain. Ultimately it&#39;s the neurosurgeon&#39;s decision, and he will be making it on the day. I just need to keep the team informed as to how this week goes. That&#39;s all I can do!&lt;/p&gt;&lt;p&gt;Point 3 is super exciting. I&#39;ve been offered the chance to take part in a new feasibility study related to Fluorodopa &lt;a href=&quot;https://en.wikipedia.org/wiki/Positron_emission_tomography&quot;&gt;PET Imaging&lt;/a&gt; in Glioma. I&#39;ll be administered intravenously with a mildly radioactive drug, called &lt;a href=&quot;https://en.wikipedia.org/wiki/Fluorodopa&quot;&gt;fluorodopa&lt;/a&gt;. The PET scanner then detects the radiation within the tumour. This imaging should give the team a lot more information about the internals of the tumour that a MRI &amp; CT scan can&#39;t show. So for example, the more aggressive parts of the tumour will show up on the scan, allowing the neurosurgeon to focus on those areas and make sure they are removed as a priority. In this instance the more data the team has the better, so I&#39;ll be consenting to taking part in the study. It&#39;s great to think that these scans could help with diagnosis of tumours in other people in the future. I&#39;m also keeping my fingers crossed that I will also have access to these more detailed scans when I request them all post-surgery.&lt;/p&gt;&lt;p&gt;All in all a very busy week! Next week could be G-day week! We shall see, I expect to hear more on Tuesday, so watch this space. Gary, your days could be well and truly numbered. 6 days and counting (hopefully!).&lt;/p&gt;&lt;h2 id=&quot;mri-and-pet-scan-day-wednesday-8th-september&quot;&gt;MRI &amp; PET scan day - Wednesday 8th September&lt;/h2&gt;&lt;p&gt;So it&#39;s G-day + 2, and all is still looking good for Friday! I had to sign a lot of forms today related to the feasibility study and the actual operation. It was mainly going over the risks associated with surgery and what to expect. I&#39;ve now been put on a 2nd type of seizure medication to try and head off the last of the seizure symptoms I&#39;m having (pun intended!). My first PCR test came back clear, and I had another PCR test today. I will most likely have another 2 this week. My poor nose!&lt;/p&gt;&lt;p&gt;The MRI scan went well today. It lasted around 40 minutes. Thankfully I didn&#39;t have a reaction when they injected the ‘contrast&#39; this time which is great. It was loud but tolerable. The doctors and staff at the hospitals (I went to 2 today) were all fantastic. I couldn&#39;t wish for a better team. I feel very fortunate to live in Oxfordshire.&lt;/p&gt;&lt;p&gt;The PET test unfortunately wasn&#39;t as pleasant. It was nothing to do with the fluorodopa, it was the fact that CT scanners really aren&#39;t built for comfort. The scan lasted a whole hour, and honestly it felt like a lot longer as I had no reference point for the time! I was strapped to a machine with my head restricted and my feet were cold. The worst part was that there didn&#39;t feel like there was enough padding behind my head. After 10 minutes the back of my head started to hurt, and once you notice it, it&#39;s hard to stop focusing on it. Especially when there&#39;s nothing else to do! Try it some time, it takes a surprising amount of energy to lie perfectly still for 1 hour while staring at the ceiling. I&#39;m very glad it is now done though. Apparently the scans came out really well and they got all the data they needed for Friday&#39;s operation!&lt;/p&gt;&lt;p&gt;I also found out a little about the surgery itself. I&#39;ll be lying on my left side for the operation with some sharp points tightly pushed against my skin to stop my head moving. I&#39;ll have a cannula in my right arm, as they want me to be able to move my left arm when I&#39;m asked too. Since there&#39;s a risk of partial paralysis on my left side, they will be testing for this throughout the operation. It sounds like this could be quite uncomfortable on my neck which is a worry, but I&#39;m hoping I&#39;m drugged up enough to really know about it!&lt;/p&gt;&lt;p&gt;So anyway, this is likely to be my last update for a little while, as I should be heading into the hospital around midday tomorrow. Fingers crossed Friday all goes well and I&#39;ll be able to post updates soon. This should include the PET scan imaging as well as a before / after image of my brain, minus Gary!&lt;/p&gt;&lt;p&gt;No matter what happens, thanks everyone for all your support, kind messages &amp; visits over the past 4 months. They have been very much appreciated. Here&#39;s hoping the rest of 2021 / 2022 improve!&lt;/p&gt;&lt;h2 id=&quot;thrust-into-the-post-surgery-world&quot;&gt;Thrust into the post-surgery world&lt;/h2&gt;&lt;p&gt;So apologies to readers waiting for an update, it&#39;s been quite a few months as I&#39;ve been a little busy in recovery, a lot of things have happened so this is going to be a long update! I&#39;ll pick up from where I left off on what I can only describe as the longest week of my life!&lt;/p&gt;&lt;h2 id=&quot;into-hospital-maybe-thursday-9th-september&quot;&gt;Into hospital maybe? - Thursday 9th September&lt;/h2&gt;&lt;p&gt;So Thursday was a &lt;em&gt;very&lt;/em&gt; long day. I hadn&#39;t been sleeping well all week, probably due to a combination of drugs and the stress of waiting for the operation. The fact that I was still having seizure-like symptoms didn&#39;t help either. So I found myself awake at 3am thinking what can I do with my time as I didn&#39;t think I&#39;d be able to sleep. So I decided to do something I&#39;d been putting off for months, simply because it isn&#39;t pleasant and not what you expect to be doing at the age of 39 years old. So I planned my own funeral.&lt;/p&gt;&lt;p&gt;My sister is the executor of my will, so I wrote a fairly brief email for her, listing my basic wishes. For example:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Cremation or burial&lt;/li&gt;&lt;li&gt;Religious or non-religious&lt;/li&gt;&lt;li&gt;Music / Dress Code&lt;/li&gt;&lt;li&gt;Flowers or donations to a (brain cancer) charity&lt;/li&gt;&lt;li&gt;Budget plans for payment&lt;/li&gt;&lt;li&gt;Where to spread my ashes&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I titled the subject line of the email &quot;Unpleasant email&quot; and used Gmail&#39;s scheduled send functionality to send it at a more sociable time. In hindsight I should have set the subject to &quot;&quot;Unpleasant email [Don&#39;t read in public]&quot;, as I think I caught her a little off guard. It turns out she read it while in the supermarket, which unfortunately made her cry down the bread aisle of her local Sainsbury&#39;s. I found this out in her reply back to me later in the day. The email chain is still difficult and upsetting for me to read even many months later.&lt;/p&gt;&lt;p&gt;So there you have it, learn a lesson from me: When you are sending an email about your funeral to your relatives make sure you consider when and where they might be when they read it! You can use the short checklist above as a starting point for your planning. Depressingly (for a 39 year old) I got much of this information from a checklist on the &lt;a href=&quot;https://funeralplan.ageuk.org.uk/viewpoint/funeral-planning-checklist&quot;&gt;AgeUK website&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;After that I still couldn&#39;t sleep so I wrote a draft &quot;Final&quot; blog post for this very blog, as I&#39;d already arranged for it to be published by a good friend and colleague at work should the worst happen. Thankfully as you can probably tell this preparation hasn&#39;t been needed! On a more positive note I now have an idea of what I want my funeral to be like at the ripe old age of 39!&lt;/p&gt;&lt;p&gt;Let&#39;s move past this deeply depressing section because the Thursday hasn&#39;t even started yet! Due to the COVID risk from my children I&#39;d been staying with my parents for a few weeks. So my parents and I would be making the 1.5 hour journey back to Oxfordshire in preparation for going into hospital later in the day (2pm).&lt;/p&gt;&lt;p&gt;Earlier in the month I&#39;d had multiple phone calls with the team looking after me as to how the days before surgery would go. We had a &quot;Plan A&quot; all the way to a &quot;Plan E&quot;. Plan A was: I&#39;d phone the hospital at 2pm the day before and a bed would be available. Travel to the hospital that day and stay overnight before the operation first thing on Friday morning. Of course nothing ever goes to plan! I phone at 2pm and there&#39;s no bed available and nobody to really speak to about it.&lt;/p&gt;&lt;p&gt;At this point I was in the middle of feeling terrible due to my brain wanting to have a seizure but the drugs stopping it from happening. So I went to bed for a few hours to try and sleep it off. During those hours my wife and eventually my mother tried to get a straight answer from the hospital to see if I would have a bed that night. Around 7pm we got confirmation from the hospital to say that a bed wasn&#39;t available but the operation was still going ahead in the morning. So I&#39;d be sleeping in my own bed the night before the operation and leaving early for the hospital in the morning. In hindsight this was actually a good thing as you will read later in the update.&lt;/p&gt;&lt;p&gt;I&#39;d just like to point out that this isn&#39;t a dig at the excellent work all the NHS admin staff are doing. If there are only a limited number of beds available there&#39;s not much you can do! Especially during Covid, which was on the rise in Oxfordshire at the time.&lt;/p&gt;&lt;h2 id=&quot;day-of-days-g-day-is-finally-here-friday-10th-september-day&quot;&gt;Day of Days (G-day is finally here) - Friday 10th September (day)&lt;/h2&gt;&lt;p&gt;G-day stands for G(ary get the f*ck out of my head)-day. And it&#39;s all a bit of a blur to be honest. I woke up early as again I couldn&#39;t sleep due to nerves. I was hoping to sleep a little in the car but wasn&#39;t feeling well at all. My brain felt ‘fuzzy&#39;, I couldn&#39;t concentrate and had very little energy. I couldn&#39;t have any breakfast due to the operation being in the morning so that didn&#39;t help either. We set off early so as to avoid any traffic. The last thing I needed was to get stressed because we were late!&lt;/p&gt;&lt;p&gt;When we arrived at my home I finally got to see my wife and children for the first time in a few weeks. Covid and cancer really don&#39;t mix well! My wife and I had already decided that she wouldn&#39;t be coming to the hospital with me. She knew she would get upset, which would likely upset me too, which isn&#39;t what you need just before a major operation. So she stayed at home and took the kids to school, to give the impression it was just another uneventful day! They knew that &quot;Daddy was going into hospital to have the lump in his head removed&quot;, but being young didn&#39;t fully realise the seriousness of it all.&lt;/p&gt;&lt;p&gt;I needed to be at the hospital for 9am to be admitted to the ward for the operation prep. Once there nobody was at the reception to admit me so I ended up speaking to another patient waiting there. He was also being operated on today by a different team. He&#39;d already had brain surgery 14 years earlier, the scar was clearly visible on his head where his hair hadn&#39;t grown back. We made small-talk for a while but as we were both nervous it felt quite forced.&lt;/p&gt;&lt;p&gt;The reception staff arrived and admitted me. I had a brief consultation with the consultant anesthetist as he wanted to check I hadn&#39;t eaten anything and wasn&#39;t allergic to any medicines that I knew of. All standard questions for pre-surgery. Interestingly this is the consultant anesthetist who wasn&#39;t available for my previous surgery date due to him being in Covid isolation. I guess the Neurosurgeon has a team that he likes to have around him, especially considering how big my tumour was and how long the operation would be! After the consultation I waited in reception for 5 minutes or so until one of the team led my mom and I to the bed I&#39;d eventually be in after surgery.&lt;/p&gt;&lt;p&gt;The other patient who I&#39;d been speaking to was in the bed opposite. Knowing that I wasn&#39;t on my own my mom left and went back to the car where my dad was waiting to take them home. It all happened so quickly we both said our ‘final&#39; goodbyes and that was that. I was more worried about her getting lost in the hospital than the actual operation at that point.&lt;/p&gt;&lt;p&gt;Now I&#39;d only been on my own for 1 or 2 minutes and the nurse came in to start pre-surgery prep. She took my blood pressure and oxygen levels etc. Then I had to put on what would quickly become the bane of my life for a month: &lt;a href=&quot;https://www.healthline.com/health/dvt/compression-stockings&quot;&gt;Deep Vein Thrombosis (DVT) leggings&lt;/a&gt;. The reason for wearing these is that stats show that after brain surgery you are 30% more likely to develop blood clots which can be fatal if they travel to the heart or brain.&lt;/p&gt;&lt;p&gt;Once prepped I got to meet my Neurosurgeon for the first time in person, he introduced himself and checked I was doing okay. Usually I would have shook his hand but due to Covid I didn&#39;t think it was appropriate. You&#39;d be surprised how much a little thing like that annoyed me. You&#39;re about to put your whole life and future into this person&#39;s hands and you can&#39;t even thank them with a handshake.&lt;/p&gt;&lt;p&gt;The Neurosurgeon left, and before I knew it I had a group of people around me leading me down to the operating theatre. I said goodbye and ‘good luck&#39; to the other patient in the room and out of the ward we went. I have to be honest I don&#39;t remember the walk to the operating theatre at all, or what we discussed on the way there. My brain was just trying to take in the enormity of the situation ahead of me. After 6 months of mentally preparing for this moment it is finally here!&lt;/p&gt;&lt;p&gt;We entered the side room to the operating theatre and the team started to work on me. They told me to get up and sit on the trolly in the centre of the room. I distinctly remember thinking &quot;This could be the last thing I ever do as a person&quot;, It took all my courage to get onto the trolley and I have to say it&#39;s the scariest thing I&#39;ve ever had to do in my whole life. There were 3-4 people in the room prepping me, for the most part I had no idea what they were all doing or saying to me. But I do remember them marking the right side of my neck with an arrow pointing up to my head in black permanent marker and then them telling me they would be putting a cannula into my right hand, then once done they would put me to sleep and put something called and &lt;a href=&quot;https://www.edwards.com/gb/devices/Catheters/Arterial-Cannulae&quot;&gt;Arterial cannula&lt;/a&gt; directly into my artery in my right arm. They also put 2 other cannulas into the top of each of my feet. I&#39;m very glad they did all this while I was asleep as I hate cannulas at the best of times! The last three sounded deeply unpleasant.&lt;/p&gt;&lt;p&gt;So that was that, cannula in the right hand, they said they were injecting me with anaesthetic and it would feel cold running up my arm and that I may have a strange taste in my mouth. I felt the anaesthetic reach the right side of my neck and I passed out.&lt;/p&gt;&lt;p&gt;During the time I was passed out the team prepared me for surgery. I was moved onto my left hand side and my head was placed in a metal contraption with 4 very sharp points sticking into my skin; these were to hold my head in place for the operation. The team cut the skin on the top of my head and peeled and folded it forward to expose my skull. At this point they then used some sort of medical saw to cut through my skull and expose my brain (and Gary) The incision started at the front of my skull just after my hairline and travelled backwards right down the centre before turning to the right and coming back to just before my right temple in a large horseshoe shape. They&#39;d basically had to remove the right hand side of my skull due to the sheer size of Gary (the prick). Thank goodness for anaesthetic and anesthetists is what I say!&lt;/p&gt;&lt;p&gt;Once all this was done they reduced the anaesthetic and used other drugs to bring me round. I remember waking up lying on my left hand side, my head was already open and I couldn&#39;t move, there was a large transparent plastic cover in my peripheral vision to the right just above me (like the plastic covers you see on children&#39;s buggies to protect them from the wind and rain). There were 2 people in front of me and a television screen just behind them. One person was the consultant anesthetist just watching proceedings, as I guess he&#39;d done most of his job and was just monitoring the situation. The other person was a lady. She was the Neurophysiologist looking after me, her job was to be the go-between between me as a patient and the Neurosurgeon. She&#39;d ask me questions and update him on how I was reacting to those questions.&lt;/p&gt;&lt;p&gt;Then the last thing in front of me was the TV. It had live pictures of the inside of my head, now they weren&#39;t gory pictures with lots of blood and bone etc. They were the 3D scans of my head taken from my last MRI scan. The crazy thing I remember is they updated in real-time, I could see exactly where the Neurosurgeon was putting his tools. There was no pain while he was doing this but I could feel a distinct &quot;pressure&quot; inside my head that I&#39;d never felt before, so I knew he was there.&lt;/p&gt;&lt;p&gt;I remember finding the real time 3D imaging fascinating. When they gave me the chance to ask them questions I remember asking them how it all worked. I was thinking it was some sort of &lt;a href=&quot;https://en.wikipedia.org/wiki/Haptic_technology&quot;&gt;haptic technology&lt;/a&gt; driven tool but it turns out it was a lot more clever than that. The team had placed 3-4 sensors on the outside of my skull which could detect the &quot;marking tool&quot; the surgeon was using inside my skull. By monitoring the input from these sensors they could get a computer to update the screen in real-time, even for a person who works in technology on a day-to-day basis this blows my mind!&lt;/p&gt;&lt;p&gt;So what exactly were they marking in my head? Well it turns out this is where the &lt;a href=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/#mri--pet-scan-day---wednesday-8th-september&quot;&gt;PET scan&lt;/a&gt; that I&#39;d had on Wednesday comes in. The feasibility study in theory should be able to be used to identify the different cancer grades within the tumour, something that an MRI / CT scan on it&#39;s own can&#39;t show. You can see the results of my PET scan below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;PET Scan of the tumour.&quot; decoding=&quot;async&quot; height=&quot;466&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/4Nr0tziPmC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/4Nr0tziPmC-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/4Nr0tziPmC-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/4Nr0tziPmC-814.webp 814w&quot; width=&quot;814&quot; title=&quot;PET scan results for Gary.&quot;&gt;&lt;/p&gt;&lt;p&gt;Now this is an ongoing study and I&#39;m one of around 30 patients in the trial. The final results of the study won&#39;t be known for many years. The above image is the result of the hour-long scan I had to endure. The radioactive isotope was absorbed by Gary, then depending on the grade, different parts emit radiation at different rates. This Is then picked up by the CT scanner resulting in the image above, hence showing Gary&#39;s internal structure. My assumption is that the red areas are the higher grade (e.g. grade 3), through to green / yellow being lower (e.g. grade 2). But this won&#39;t be known and confirmed until the study concludes in a few years.&lt;/p&gt;&lt;p&gt;The reason I&#39;m telling you this is because when I woke up I heard the team were having a conversation where they were talking about colours and numbers. As it turns out the team were talking about these scans. They&#39;d identified certain areas of Gary that they would like to biopsy for further analysis, before moving onto removing Gary completely. I think because Gary was so big there was a treasure trove of data for the study! I heard from the team post-surgery that they removed 8 areas of Gary for further analysis. They would have liked to have taken more but the Neurosurgeon put his foot down to say no more, as the procedure with only 8 added 1-1.5 hours more to the surgery time. One thing I remember finding amazing was how the whole team was working together. The Neurosurgeon was working inside my head but he obviously couldn&#39;t see the real-time scans with the areas he was supposed to be removing, so the team were speaking to him telling him to move left / right / up down etc. Only the language they used was more medical than that, so using more medical references from the brain. It was incredible to see and hear.&lt;/p&gt;&lt;p&gt;I remember asking a few more questions about what was going on that the Neurophysiologist couldn&#39;t answer and getting the response &quot;that would be a question for the Neurosurgeon, but he&#39;s a little busy at the moment!&quot;. Then I think they put me to sleep again (probably to shut me up to be honest!). I&#39;ve been told there were between 12 - 14 people in the operating theatre at the time but I only remember seeing 2 people and hearing the Neurosurgeon. The other folks must have been supporting the Neurosurgeon &quot;above me&quot;.&lt;/p&gt;&lt;p&gt;The next thing I remember is coming to, my eyes were closed so I couldn&#39;t see anything but I could feel they were stapling my head back together. I felt no pain but I just remember the pressure of the staple gun reverberating through my body. I also remember thinking &quot;that&#39;s a lot of staples!&quot;. If you&#39;ve ever used a staple gun you will know that you have to put quite a lot of pressure on the handle before the staple is released. Well it sounded exactly like that, only into the top and side of my head around 95 times!&lt;/p&gt;&lt;p&gt;Annoyingly that&#39;s all I really remember from the surgery. I estimate maybe 45 mins to an hour of the 7 hour operation. I&#39;ve actually been quite disappointed that I don&#39;t remember more of it and nothing more has come back to me over time. A couple of weeks after surgery I spoke to one of the Neurosurgeons who was also in the operation helping, about this. And she said that this was intentional. They constantly increase and decrease the drugs throughout the surgery as there are certain unpleasant things they don&#39;t want you to remember. So I&#39;m thankful for that, but in another way I will always wonder what happened in those 6+ hours of my life I can&#39;t remember!&lt;/p&gt;&lt;h2 id=&quot;recovery-starts-friday-10th-september-evening-and-night&quot;&gt;Recovery starts - Friday 10th September (evening &amp; night)&lt;/h2&gt;&lt;p&gt;The next thing I remember is waking up on the hospital ward with the absolute worst headache I&#39;ve ever had, not really surprising when they&#39;ve just removed approximately 15% of my brain matter! I could barely move and had to sleep upright due to the pressure lying flat would cause.&lt;/p&gt;&lt;p&gt;I&#39;m so glad they put me to sleep before inserting the Arterial Cannula, as the bruise I had on my right arm was huge! It must have been quite a struggle to get it in there!&lt;/p&gt;&lt;p&gt;Now I have to be open and honest. This was the worst night&#39;s sleep I think I&#39;ve ever had, hence why I was grateful I slept at home the night before. I&#39;m not a fan of hospitals anyway, but this night will stay with me forever.&lt;/p&gt;&lt;p&gt;Due to the risk of blood clots I had my DVT leggings on, but as an extra &quot;brucie bonus&quot;, I also had a pair of leggings that inflated &amp; deflated every 10-30 seconds. The only way I can describe it is when you go to have your blood pressure checked and the strap on your arm inflates then deflates. Well imagine that on the total length of both calves. Inflate / deflate all night long every 10-30 seconds!&lt;/p&gt;&lt;p&gt;I also found I couldn&#39;t really move both because of the leggings and the pain it caused in my head. I found my legs were roasting hot but my top half was cold because I couldn&#39;t pull the blanket up over myself. What made things worse was I couldn&#39;t find the call button to ask for help. I must have spent 20-30 mins looking for it before I eventually found it (it may not have been that long but it felt like it!).&lt;/p&gt;&lt;p&gt;Just to make things worse the patient opposite me who I&#39;d spoken to before surgery was being violently sick all night. I think he must have had a bad reaction to the anaesthetic. So yes, all in all a terrible night&#39;s sleep 0/10 would not recommend!&lt;/p&gt;&lt;h2 id=&quot;lets-get-the-hell-out-of-here-saturday-11th-september&quot;&gt;Let&#39;s get the hell out of here - Saturday 11th September&lt;/h2&gt;&lt;p&gt;After that one night in hospital I was absolutely determined to not have to stay another. So once it was light the nurses brought around food that I couldn&#39;t eat. Everything tasted strange from the anaesthetic. I eventually managed to eat dessert, which was a rhubarb crumble with custard. The inflating leggings were taken off so I could finally move my legs. At that point I realised I had a much bigger scar than I expected so I wanted to go into the toilet to take a look. The toilet was only a few meters away but it felt like miles. I asked for a walking frame so I could get there myself but I had to be escorted for the first few times. Looking in the mirror was a weird experience. I couldn&#39;t see the scar at the moment as it was covered in a blue gause, but it was a hell of a lot bigger than I expected. I&#39;m not sure why I thought it would be smaller looking at the size of Gary. How else were they going to extract the prick?!&lt;/p&gt;&lt;p&gt;At the same time as looking at my head for the first time, I managed to brush my teeth! But no matter how much I tried I couldn&#39;t get the weird taste of anaesthetic out of my mouth. Eventually after what seemed to take an age to do these small things I was escorted back to my bed, mostly under my own steam (with a walker).&lt;/p&gt;&lt;p&gt;Next came something that would be a thorn in my side for the next 3-4 weeks. Lying down or sitting up caused terrible pain in my head. This is because, unlike pretty much everyone but medical folks would know, when you get up / sit up / lie down, the &lt;a href=&quot;https://en.wikipedia.org/wiki/Intracranial_pressure&quot;&gt;intracranial pressure&lt;/a&gt; inside your skull changes. Usually you&#39;d never notice this, but when someone has just removed a huge chunk of skull and stapled it back on, you suddenly feel it!&lt;/p&gt;&lt;p&gt;It seemed to take me ages to lie down on the bed but I eventually managed it and got comfortable. But I was determined not to have to stay another night in the hospital so I decided I needed to get moving. Over the next few hours I kept getting up and down again and used the walker to move around the ward. Once I was steady on my feet the nursing staff were happy to let me walk on my own (it was more of a shuffle really!).&lt;/p&gt;&lt;p&gt;At 12pm the nurse came over and said my wife (Claire) would be visiting in the next couple of hours, this perked me up a bit. It would be good to see a familiar face after what had felt like weeks since the previous morning! Claire was very surprised when the nurse mentioned that I was already up and walking about the ward.&lt;/p&gt;&lt;p&gt;2pm came and my wife brought in some M&amp;S chocolates and she got to see my humongous scar. Thankfully I&#39;d had no memory loss, but I was finding speech and concentration very difficult. I don&#39;t think we said much. I think I was mainly staring off into space or at the ceiling wishing the pain in my head would go away. The only painkillers I could have were paracetamol, none of the good stuff I&#39;m afraid! Thankfully they took the edge off the pain. After an hour it was time for my wife to leave. She said she would be back to see me again tomorrow. But I had other plans.&lt;/p&gt;&lt;p&gt;Claire phoned my parents after leaving the hospital and pretty much said there is no way I&#39;m going to be released from hospital tonight and it will most likely be Monday. So my parents stood down from coming to pick me up since they were a 1.5 hour car journey away. But during this time one of the senior doctors came to see me. He said that as long as I&#39;m up and about moving we could try and get a CT scan sorted to check everything in my head was okay and try to get me discharged. I jumped on that in a flash!&lt;/p&gt;&lt;p&gt;I had to wait an hour or so for the CT scan then the porter came to wheel me across to the other side of the hospital. The CT scan took 10-15 mins or so then they took me back up to the ward. It was around 30-40 mins later that the same doctor came to see me and said everything looked great, so if I wanted to he&#39;d be happy to discharge me. I obviously said yes, and he went off to pull together my discharge paperwork (which would take a couple of hours). I immediately tried to phone Claire but I had a slight weakness in my left hand, and I also found that the phone didn&#39;t make as much sense as it had done the day before. Things hidden behind icons were difficult to find. I found this very worrying considering how easy it was just a single day before. But I eventually managed to work out how to use the phone and rang her. At first she couldn&#39;t believe they were discharging me as I was still slurring my speech down the phone. Somehow I managed to get her to believe me with the help of one of the nurses who overheard via speakerphone. So it was then arranged that my parents would travel down to pick me up from the hospital and take me home. This eventually happened around 10pm. So from admission through surgery to discharge I&#39;d been in hospital for 36 hours! That&#39;s pretty crazy when you think about it. 15% less brain and out in a day and a half! To give you an idea of how busy the hospital was at the time, I heard the nurses discussing the next patient who was going to take up my bed once I&#39;d been discharged! The nurses really are unsung heroes, day in, day out working with sick patients, and it isn&#39;t easy work at all!&lt;/p&gt;&lt;p&gt;I don&#39;t remember any of the journey home, but I do remember that the car was really low and difficult to get into especially given all the staples in my head! I don&#39;t think my parents had a very good night&#39;s sleep that night. Mine wasn&#39;t perfect but it was 1000% better than the night before. I still had to sleep at an angle for another few days which was uncomfortable but atleast I wasn&#39;t in hospital!&lt;/p&gt;&lt;h2 id=&quot;how-does-the-tv-work-sunday-12th-september&quot;&gt;How does the TV work? - Sunday 12th September&lt;/h2&gt;&lt;p&gt;I woke up early as I was in pain and uncomfortable. The F1 Italian Sprint race had happened on Friday during my operation so I wanted to catch-up on what had happened. My Dad had recorded it on Sky+ so before they woke up I decided to go downstairs and try to watch it. Unfortunately, I couldn&#39;t work out how to use the Sky box. I was able to turn it on and find the recordings but when I played them it wasn&#39;t the Formula 1, it was the Formula 2. This totally confused me because the thumbnail preview was showing a Formula 1 car. I tried multiple times to get to the F1, but had no luck. Eventually I gave up and used my phone to watch the highlights on YouTube. This was the start of an ongoing theme over the next few days. Things that had seemed so simple and obvious only 2 days before I was suddenly struggling with. I had the same issue with the Netflix User Interface. I found it too busy and confusing to use. For a once technically proficient person I found this genuinely scary. Thoughts of &quot;how exactly am I going to do my job&quot; if I&#39;m struggling with these simple things! Not fun, and quite a contrast to me only 2 days before. It really did give me some insight into the importance of making things simple and understandable for users that are less technically proficient. If only every web development team could have similar brain surgery. I think we&#39;d have a much more user-friendly internet if so.&lt;/p&gt;&lt;h2 id=&quot;is-my-head-leaking-monday-13th-september&quot;&gt;Is my head leaking? - Monday 13th September&lt;/h2&gt;&lt;p&gt;Odd title I know, but I woke up on Monday and found spots of red transparent liquid on the white pillowcase I&#39;d been sleeping on. Further to that I was certain I could feel &quot;movement&quot; underneath the blue gauze which covered the staples in my head. When I felt something drip onto the bridge of my nose, I immediately got in contact with the Neuro-Oncology team via email to let them know. Within 5 mins they&#39;d got back to me via email and a phone call. They asked me to come back to the hospital as quickly as possible. Of course I was 1.5 hours away at my parents so it wasn&#39;t as simple as that. So I quickly packed a bag of clothes just in case I needed to stop overnight and we headed back along the motorway to the hospital.&lt;/p&gt;&lt;p&gt;Once we got there I waited with Claire who&#39;d met us there for 5-10 mins and one of the Junior doctors took us into a side room and started to remove the staples from my gauze so they could get a better look at my head. She explained that if it was that Cerebrospinal Fluid (CSF) or &quot;brain juice&quot; (as I liked to call it) was leaking from the wound it also means that things can get into the wound / my skull. If this happens it can cause an infection, leading to a whole host of other issues that may even require more surgery. So it&#39;s best to get it checked out. The Junior doctor couldn&#39;t find any obvious leaks so she consulted one of her more senior colleagues. He came to see me and also couldn&#39;t find any obvious leaks. So he decided to do a very scientific test to make sure. He placed a white absorbent pad on my head (like a kitchen towel) and then had me cough and lie down on the bed. This increases the intracranial pressure inside the skull (I know because I could feel it!) and if there&#39;s a leak CSF would come out of the wound and be seen on the absorbent pad. Thankfully nothing was seen on the pad!&lt;/p&gt;&lt;p&gt;The senior doctor was certain the movement and liquid I&#39;d felt / seen was the petroleum jelly (think Vaseline) used to keep the wound moist and clean. My warm head was heating the jelly, turning it to liquid and it felt like water (because I couldn&#39;t see it to verify it wasn&#39;t). He did leave me with a nice vision: Saying that if I do have a CSF leak it would literally be pouring down my face, and if this were to happen phone the emergency services immediately. I&#39;m pretty sure I&#39;d have done that anyway, but it&#39;s always good to know I guess!&lt;/p&gt;&lt;p&gt;With the panic over I headed back to my parents to continue my recovery, with hopefully less drama moving forwards!&lt;/p&gt;&lt;h2 id=&quot;general-struggles-tuesday-14th-september&quot;&gt;General struggles - Tuesday 14th September&lt;/h2&gt;&lt;p&gt;So I&#39;m not going to give a day by day update on what happened as it&#39;s very boring and tedious. It felt like a long time for me because I was obviously living it, so to write about it would be even worse! But here&#39;s a summary of some of my general post-surgery struggles I had.&lt;/p&gt;&lt;p&gt;The DVT leggings were terrible to wear. I hate wearing socks to bed and now I had to wear these really tight leggings over my lower legs for a month! They were a pain to get off and it took help from both of my parents to get them back on. I guess it&#39;s a small price to pay compared to fatal blood clots!&lt;/p&gt;&lt;p&gt;As mentioned before, sitting up and lying down was painful. Basically going from 0 to 90 degrees and vice-versa caused a terrible pain in my head, so I tried to avoid moving, but you&#39;d be surprised how often you make these movements going about your general day-to-day life! This was the same for coughing, laughing and sneezing, all these increase the intracranial pressure in the skull. Everytime i sneezed it felt like the top of my head was about to split open!&lt;/p&gt;&lt;p&gt;Related to the above issue, lying down and getting into bed was a chore! I remember having to get up in the night to go to the toilet, then getting back into the bedroom, and it literally took me 15-20 minutes to pluck up the courage to lie-down on my pillow to go back to sleep! I knew that it would only take 2 seconds to do it, but when I also knew it was going to hurt, my brain and body refused to move. I just ended up rocking backwards and forwards on the bed for minutes at a time, hoping the momentum would carry me forwards and I could then go back to sleep.&lt;/p&gt;&lt;p&gt;A strange one but due to the fact that I&#39;d had open-head awake surgery, when they close your head it&#39;s impossible to remove the air pocket that is leftover. So for 6-8 weeks I had a huge air pocket at the top of my skull. And everytime I moved my head I could feel this air pocket move around my head. It wasn&#39;t uncomfortable, it was just weird. It was a similar sensation to someone lightly brushing the top of your head. There&#39;s nothing that the doctors can do about this, it just takes time for your body to naturally replace and recycle the CSF around the brain every 6 weeks or so. So the air was gradually reabsorbed back into my bloodstream. But this sensation lasted 6-8 weeks after surgery, maybe even more.&lt;/p&gt;&lt;p&gt;Related to the air in my head, I could feel my nose randomly &quot;creaking&quot;. It&#39;s hard to describe but it felt like small air bubbles at the top of my nose releasing. I think it may have been related to the CSF fluid redistributing itself and the pressure inside my head stabilising itself over time. This lasted 2-3 months after surgery.&lt;/p&gt;&lt;p&gt;I had to keep the 95 metal staples in my head for 2 weeks and in this time I kept having phantom movements on the outside of my head. Almost like my body was realising that there was a foreign body (or 95 of them) attached to the outside of my skull. This stopped as soon as the staples were removed and the wound started healing.&lt;/p&gt;&lt;p&gt;And finally I think I may have been suffering from something called &lt;a href=&quot;https://www.healthline.com/health/abulia&quot;&gt;Abulia&lt;/a&gt;. This is described as &quot;a state of diminished motivation&quot;. Now this is a self-diagnosis not from the doctors, but from reading up on the symptoms it certainly seems like what I was suffering with, at least in the first 3-4 weeks anyway.&lt;/p&gt;&lt;p&gt;All in all I think I&#39;ve got off quite lightly considering the size of the scar and the size of Gary. The struggles above weren&#39;t pleasant but they were manageable. I&#39;m very surprised how few headaches I&#39;ve had. If I were to have guessed what the most likely result of major brain surgery would be it&#39;d be headaches. But apparently not. At Least in my case anyway!&lt;/p&gt;&lt;p&gt;It&#39;s also worth noting that the only painkiller advice I was given post-surgery was to take 2 paracetamol! So remember that the next time you have a headache. Paracetamol are so good they even work for folks who&#39;ve had 15% of their brain matter removed!&lt;/p&gt;&lt;h2 id=&quot;biopsy-results-tuesday-21st-september&quot;&gt;Biopsy results - Tuesday 21st September&lt;/h2&gt;&lt;p&gt;As with most things happening in the NHS at the moment teams are extremely busy. So I wasn&#39;t expecting anything from the hospital on this day until I received an email at 9am asking if I was free for a call with my Neurosurgeon + team at 12:30pm. I replied back and accepted, I mean It&#39;s not like I had much else on! I wasn&#39;t quite sure what the call was about so really wasn&#39;t mentally prepared for it at all.&lt;/p&gt;&lt;p&gt;I jumped on the call with the team, my wife and parents were listening in too. My Neurosurgeon, never one to mince his words, immediately spoke and came out with the biopsy results of Gary, the now (mostly) evicted prick! He told me the team had managed to remove a whopping 95% of Gary, but the biopsy results had come back to say that some parts of him were Grade 3, rather than Grade 2. Grade 3 are called &lt;a href=&quot;https://en.wikipedia.org/wiki/Anaplastic_oligodendroglioma&quot;&gt;anaplastic oligodendrioglioma&lt;/a&gt;, these are malignant (cancerous) and can be fast growing. And because of this I&#39;d need to start a 6.5 week course of radiotherapy within the next month, followed almost immediately by a 6 cycle course of &lt;a href=&quot;https://www.cancerresearchuk.org/about-cancer/cancer-in-general/treatment/cancer-drugs/drugs/pcv&quot;&gt;PCV chemotherapy&lt;/a&gt; which will last approximately 7 months. As I&#39;d experienced previously with my Neurosurgeon there was no warning about this news, no &quot;are you sitting down, brace yourself&quot;. Just boom, you&#39;ve got Grade 3 brain cancer! Well you may as well pull off the plaster quickly and get it over with I guess! In hindsight I was expecting it to be a multi-grade tumour even before they told me, I mean why would they have been wanting to biopsy multiple areas in the surgery if it had all been a single grade tumour?&lt;/p&gt;&lt;p&gt;After the initial shock, I managed to compose my thoughts and asked a few questions about the surgery itself. I asked if there&#39;d been any nerve damage at all, as I had a slight weakness in my left hand. He looked slightly bemused by this and said no there&#39;d been no nerve damage and the operation had gone very well. It turns out there&#39;d been a slight miscommunication from what was said to Claire immediately after she got the phone call from the Neurosurgeon to say surgery had gone well and that I was in recovery.&lt;/p&gt;&lt;p&gt;I was led to believe that they stopped surgery because they had caused slight nerve damage. But this wasn&#39;t the case. It was actually that if they went any deeper it &lt;em&gt;could have&lt;/em&gt; caused nerve damage, so they stopped. I later found out that the team were using a technology during the operation called &lt;a href=&quot;https://www.healthcareers.nhs.uk/explore-roles/healthcare-science/roles-healthcare-science/physiological-sciences/neurophysiology&quot;&gt;Neurophysiology&lt;/a&gt;. This involved strategically inserting 5-8 metal pins into the left hand side of my body and monitoring the signals from these pins as they fired electrical impulses into my brain. It was basically a type of automated testing to see when they were getting close to nerves that actually did something, rather than just prodding Gary the useless prick. So once the safest amount of Gary had been removed, the team decided I was getting very tired and I needed to save my strength for recovery. So that&#39;s why the operation &lt;em&gt;only&lt;/em&gt; lasted 7 hours!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;MRI scan of Gary before the operation.&quot; decoding=&quot;async&quot; height=&quot;467&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/IWf716-I-x-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/IWf716-I-x-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/IWf716-I-x-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/IWf716-I-x-814.webp 814w&quot; width=&quot;814&quot; title=&quot;Gary before the operation highlighted in red.&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;MRI scan of Gary after the operation.&quot; decoding=&quot;async&quot; height=&quot;466&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5cyoYNj8Do-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5cyoYNj8Do-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5cyoYNj8Do-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5cyoYNj8Do-814.webp 814w&quot; width=&quot;814&quot; title=&quot;Gary after the operation highlighted in red.&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above images you can see the before at the top then the after scan of my brain with Gary highlighted in red. As you can see he&#39;s mostly gone apart from some left over in the deepest part of my brain. This leftover is what the Radiotherapy and Chemotherapy are going to be targeting as well as any residual cancer cells around the edges of the surgery.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Comparing imaging before / after for my brain&quot; decoding=&quot;async&quot; height=&quot;409&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/1dlJunqBpV-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/1dlJunqBpV-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/1dlJunqBpV-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/1dlJunqBpV-814.webp 814w&quot; width=&quot;814&quot; title=&quot;Image of my brain before (left) and after (right)&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/&quot; title=&quot;Gary from the top.&quot;&gt;//&lt;/a&gt;: # (Image of my brain before (left) and after (right).)&lt;/p&gt;&lt;p&gt;Now a question I&#39;ve been asked many times off the back of the quite frankly incredible image above is &quot;what happens with the hole moving forwards?&quot;. Well this is a question I asked back when I was first diagnosed and they described the operation to me. The hole will now alway be there. Brain cells don&#39;t grow back. Over time it fills with CSF, but it will always be there. And a fascinating observation I&#39;ve noticed since the operation is I can &lt;strong&gt;hear&lt;/strong&gt; the cavity where Gary used to reside in my head. This is especially true when I&#39;m in the shower and I run the water over my head. The right hand side of my head sounds hollow compared to the left! I like to compare it to looking for a stud wall behind some plasterboard. As you tap on the plaster the sound changes from hollow to dense. It actually took me a while to work this out and what was different when I was having a shower in the mornings. It&#39;s a strange feeling to know there&#39;s now just a big hole in my head!&lt;/p&gt;&lt;p&gt;After the appointment I didn&#39;t have long to wait as my initial appointment with the Oncology team to discuss the radiotherapy as it was set for the following day. No time to waste with Grade 3 brain cancer I guess!&lt;/p&gt;&lt;h2 id=&quot;further-treatment-appointment-wednesday-22st-september&quot;&gt;Further treatment appointment - Wednesday 22st September&lt;/h2&gt;&lt;p&gt;The whirlwind that my life had become continued with a meeting at a new hospital and a whole new team of experts. The meeting was to discuss the post-surgery treatment I was signing up for. This meeting was mainly about radiotherapy, with the chemotherapy discussion coming at a later date.&lt;/p&gt;&lt;p&gt;There were 2 oncologists and a number of nurses in the meeting as well as my wife. I&#39;d made the decision that whenever something important was being discussed I should always have company as I knew for a fact I wouldn&#39;t remember it all, since it had only been 12 days since my surgery! There was no chance of me remembering much!&lt;/p&gt;&lt;p&gt;The meeting seemed to last ages and there was so much information being discussed I found it hard to concentrate on what was being said, so I&#39;m very glad Claire was there with me to take notes and remember everything.&lt;/p&gt;&lt;p&gt;As well as introducing the timeline for the radiotherapy all the wonderful side-effects were discussed too. And they always seem to save the best (and rarest) to last, which includes the fact that radiotherapy in some cases can lead to other forms of cancer. What&#39;s better than having 1 type of brain cancer? Well 2 forms of brain cancer I guess!&lt;/p&gt;&lt;p&gt;Once all the difficult conversations were discussed, I signed on the dotted line to say I agreed with the treatment. I&#39;d already decided this was the direction I wanted my treatment to go even before hearing all the side-effects so it wasn&#39;t a hard decision to make.&lt;/p&gt;&lt;p&gt;With a room full of doctors and nurses I decided to ask the question if it would be possible to take the staples out of my head. It was coming up to 2 weeks since surgery and this was close to the date they had said, so I may as well have it done while I was there anyway.&lt;/p&gt;&lt;p&gt;I felt quite sorry for the nurse when she saw the sheer number of staples in my head. I wouldn&#39;t even know where to start. The technique involves taking every other staple out first, just in case the wound hadn&#39;t healed. I guess they don&#39;t want your head to suddenly open up again, and every other staple allows for some support while assessing the wound. Removing the staples wasn&#39;t as uncomfortable as you&#39;d expect. There were a few minor sharp pains with a few of the staples, but most of them were pain free. Because there were 95 of them it seemed to take ages!&lt;/p&gt;&lt;p&gt;Once the staples were out I was done for the day and I was travelling back home to be with my wife and children. I also managed to wash my hair / head for the first time in just under 2 weeks. It was the most amazing shower and I actually felt a little more human after it!&lt;/p&gt;&lt;p&gt;My kids were at school when I got back, and I needed to sleep for a few hours. This was the first time they&#39;d seen me since before the surgery so it was going to be interesting to see how they would react to the huge scar on my head. My youngest son wasn&#39;t phased at all but as soon as my eldest saw me I could tell he wasn&#39;t comfortable with my new Frankenstein&#39;s monster look. We tried not to make a big deal of it but I ended up having to wear a hat around the house for a few weeks while my head healed to stop him freaking out.&lt;/p&gt;&lt;h3 id=&quot;prognosis&quot;&gt;Prognosis&lt;/h3&gt;&lt;p&gt;So jumping back to the meeting with the team they went over the scans and some of how well the surgery had gone and then got down to the nerve-racking part, which is the future prognosis. They could only give me timescales for the &quot;average&quot; patient in my position. It was mentioned that this isn&#39;t a cure and it&#39;s likely to always be there.&lt;/p&gt;&lt;p&gt;The timescales they gave me are below:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Do nothing - the average patient can expect to live &lt;strong&gt;7 years&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Radiotherapy followed closely by chemotherapy - the average patient can expect to live &lt;strong&gt;14 years&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So here&#39;s hoping I&#39;m the exception to the rule or not the &quot;average&quot; patient. Else I certainly won&#39;t need to worry about retirement plans (that&#39;s probably a blessing actually)! Given the retirement age for men is 68 in the UK, I&#39;ll fall some 15 years short. But a lot can happen in 14 years, you never know with further funding and research more options could be available by then!&lt;/p&gt;&lt;h2 id=&quot;making-my-mask-friday-1st-october&quot;&gt;Making my mask - Friday 1st October&lt;/h2&gt;&lt;p&gt;Because Radiotherapy is a very targeted treatment, they need to make sure the treatment area is accurate for every session, and this involves me not moving my head at all for around 6-8 minutes while the treatment happens. This of course is impossible without aid, so they make a fitted mask that you wear throughout the treatment, that you can then take home should you wish to at the end!&lt;/p&gt;&lt;p&gt;The mask is made by heating up a rubber mesh to around 70 degrees centigrade in what looks like a pizza oven. While you are lying on the &quot;couch&quot; they quickly pull the heated mesh over your face / head and manipulate it a little to make sure it fits snugly. Then you have to wait 20 minutes underneath it while it cools, before it can then be taken off.&lt;/p&gt;&lt;p&gt;It&#39;s a very strange sensation when they first pull it over your face. It&#39;s like someone is pulling a plastic bag over your head and for the first second or so there&#39;s initial panic in that you think you can&#39;t breathe. But once you breathe through your nose it&#39;s actually quite pleasant and warm. You also need to close your eyes so as to not pull out your eyelashes when they remove it. A side effect of this means that during the treatment itself you have no idea what is happening around you.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Picture of my finished mask!&quot; decoding=&quot;async&quot; height=&quot;384&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5uSLLs9QXm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5uSLLs9QXm-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5uSLLs9QXm-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/5uSLLs9QXm-814.webp 814w&quot; width=&quot;814&quot; title=&quot;A picture of the finished mask!&quot;&gt;&lt;/p&gt;&lt;p&gt;It&#39;s also worth noting that when I say you lie on a &quot;couch&#39;&#39; it&#39;s not like something you sit on to watch TV. It&#39;s actually a flat carbon fiber board that isn&#39;t built for comfort. It&#39;s built for accuracy.&lt;/p&gt;&lt;h2 id=&quot;radiotherapy-starts-tuesday-19th-october&quot;&gt;Radiotherapy Starts - Tuesday 19th October&lt;/h2&gt;&lt;p&gt;The radiotherapy happened over 6.5 weeks, with 33 sessions in total. I needed to go into the hospital everyday apart from weekends. The actual treatment only took 8-9 minutes once I was in the room. It was the travelling there and back (~45 mins each way), plus there was sometimes a wait for the machine to be ready, as there were sometimes delays with other patients. The worst day I had was one particular Friday where I left the house at 8am and used the NHS hospital transport. My treatment was delayed by 1.5 hours, so I missed my transport slot to go home. I eventually got back at 14:30 and was absolutely shattered.&lt;/p&gt;&lt;p&gt;I&#39;d got into the routine of having the treatment, coming home, eating, then going to bed for 2-3 hours. Because of this I was so tired, there was no way I could do anything really for 6.5 weeks. The effect of the treatment was cumulative, so as the weeks went on I got more and more tired.&lt;/p&gt;&lt;p&gt;I&#39;m not going to cover each day, but will give a week by week blow of the whole experience.&lt;/p&gt;&lt;h3 id=&quot;week-1-19th-22nd-october&quot;&gt;Week 1: 19th - 22nd October&lt;/h3&gt;&lt;p&gt;This week was mainly getting used to the schedule of how it all worked, signing in everyday, waiting in a particular area, hoping that there wasn&#39;t a delay on my particular machine etc. It&#39;s incredible the cognitive load that the &quot;unknown&quot; puts on you. But once into the routine it was fine. The only real side effects I had in the first week was tiredness, and getting used to staying perfectly still when I had the mask on. It was also trying to get used to the machines and what exactly they were doing. As mentioned earlier, My eyes were shut for the whole process so I had no idea what exactly was happening around me for 8-9 minutes, I only had the sound of the machine to rely on. For my first couple of sessions it felt like the whole machine was rocking backwards and forwards because I had no point of reference. With my eyes shut and touching nothing but the &quot;couch&quot;, as soon as your brain thinks you are moving it is very hard to stop it from thinking it! Trust me I tried multiple times! I&#39;m just very glad I don&#39;t suffer from claustrophobia because if I did the whole 6.5 weeks would be a nightmare as that mask was very tight! Sometimes so tight it left an mesh imprint on my nose and forehead.&lt;/p&gt;&lt;h3 id=&quot;week-2-25th-29th-october&quot;&gt;Week 2: 25th - 29th October&lt;/h3&gt;&lt;p&gt;Week 2 was mainly travelling in and out via hospital transport so the days were long and it very much started to feel like &quot;groundhog day&quot;. The funny thing about Hospital transport is the drivers are all volunteers, some were very chatty, others wouldn&#39;t say a word for the whole journey. I often found the drivers were chatty on the journey back, when I was tired after hours of being at the hospital. Small talk is difficult at the best of times, but 6 weeks after major surgery and radiotherapy it is a struggle! The main side effect from this week was I noticed I&#39;d started to get a distinctive tan on my forehead from the radiation. It was like going on holiday without all the fun.&lt;/p&gt;&lt;h3 id=&quot;week-3-1st-5th-november&quot;&gt;Week 3: 1st - 5th November&lt;/h3&gt;&lt;p&gt;Week 3 was a tough week. The whole process was getting very repetitive and I was only just getting to the half-way point! This is also the point at which I noticed my hair was starting to fall out.&lt;/p&gt;&lt;p&gt;I was having a shower one morning and realised there was hair in my mouth. I quickly spat it out then a few seconds later realised there was hair in my mouth again. It was only when I washed my hair I noticed whole clumps of hair were coming out from the inside of my scar. I&#39;d been expecting this as they&#39;d warned me about it, but in the back of my mind I thought it may not happen to me. So it&#39;s still quite a shock when it does happen!&lt;/p&gt;&lt;h3 id=&quot;week-4-8th-12th-november&quot;&gt;Week 4: 8th - 12th November&lt;/h3&gt;&lt;p&gt;Groundhog day continues and I notice more of my hair is falling out, this time from the other side of my head. I guess because of the geometry of how they are targeting Gary, they are hitting it from multiple angles. So I ended up having hair at the back and sides of my head and very little in the middle, with a small patch of hair right at the front of my head. It looked ridiculous so I decided to shave my head completely. This completed my full on &quot;cancer look&quot;, and it certainly shows off my scar a lot more!&lt;/p&gt;&lt;p&gt;The one thing I notice is how much colder my head actually is! You wouldn&#39;t have thought such little hair would make a difference, but it really does! Thankfully I have a few hats that I wore both inside and outside the house.&lt;/p&gt;&lt;h3 id=&quot;week-5-15th-19th-november&quot;&gt;Week 5: 15th - 19th November&lt;/h3&gt;&lt;p&gt;At this point I&#39;m well and truly over the whole process. The journey in and the appointment takes up the whole morning, then I&#39;m sleeping in the afternoon. So by the time I wake up it is already getting dark. So the day is gone and I&#39;ve got nothing to show for it. So incredibly frustrating!&lt;/p&gt;&lt;h3 id=&quot;week-6-22nd-26th-november&quot;&gt;Week 6: 22nd - 26th November&lt;/h3&gt;&lt;p&gt;Thankfully getting closer to the end of the whole process now, but I&#39;m starting to feel incredibly drained. It got so bad at the end of the week that on Friday Claire had to phone the emergency number as I felt like I was about to have a seizure. This is something they had warned us about but I hadn&#39;t had any seizure-like symptoms since before surgery. After lying down in a dark room for an hour or so and taking some extra medication that the oncall oncologist had recommended I felt a little better. But it felt depressing, as it was almost like I&#39;d regressed weeks rather than moved forwards.&lt;/p&gt;&lt;h3 id=&quot;week-7-29th-november-2nd-december&quot;&gt;Week 7: 29th November - 2nd December&lt;/h3&gt;&lt;p&gt;The final week! In the weekly Tuesday consultation with the consultant / senior nurse, it was decided that I was going to be kept on stronger seizure medication in the evenings for the foreseeable future. Thankfully I haven&#39;t had any huge side effects on this drug so I didn&#39;t mind. As long as it keeps the seizures away!&lt;/p&gt;&lt;h2 id=&quot;radiotherapy-ends-thursday-2nd-december&quot;&gt;Radiotherapy Ends - Thursday 2nd December&lt;/h2&gt;&lt;p&gt;In my final session on the Thursday I took a whole bag full of chocolates in for the nurses and reception staff as they&#39;d all been so helpful and friendly for the past 6 weeks. They seemed very happy with them. I guess they must get it quite a lot. Since it was my final session I asked if I could film my radiotherapy session as I was curious as to what exactly the machine was doing, having had no idea for the past 32 sessions. Thankfully they allowed it.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Picture of me lying on the machine.&quot; decoding=&quot;async&quot; height=&quot;385&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/Ar4S2PlWKp-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/Ar4S2PlWKp-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/Ar4S2PlWKp-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/Ar4S2PlWKp-814.webp 814w&quot; width=&quot;814&quot; title=&quot;A picture of of me on the machine for my final session!&quot;&gt;&lt;/p&gt;&lt;p&gt;Once the machine passed over me for the final time I remember clenching both my fists and saying a little &quot;yes!&quot; to myself, 6.5 weeks and 33 sessions over! And the final time I&#39;d need to put on the uncomfortable mask! You get the option at the end about if you want to keep the mask and take it home. I decided I wanted to keep it. I&#39;m not sure why. Maybe I can pass it onto my kids and grandkids in the future? Maybe it will become a family heirloom that they can one day take onto the Antiques Roadshow! But for now it sits up in the loft so as not to scare the hell out of them!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Picture of the mask with a party hat on&quot; decoding=&quot;async&quot; height=&quot;610&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NlNe8A76Sx-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NlNe8A76Sx-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NlNe8A76Sx-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NlNe8A76Sx-814.webp 814w&quot; width=&quot;814&quot; title=&quot;A picture of the mask with a party hat on to celebrate the final session.&quot;&gt;&lt;/p&gt;&lt;p&gt;We had a little celebration as a family, a mini post-radiotherapy party with party plates, hats and balloons too! No jelly and ice cream though. Probably a good idea as I&#39;m trying to cut down on sugar!&lt;/p&gt;&lt;h2 id=&quot;whats-next-friday-3rd-december&quot;&gt;What&#39;s next? - Friday 3rd December&lt;/h2&gt;&lt;p&gt;After 6.5 weeks of going to the hospital everyday, Friday was unusual. I didn&#39;t need to be anywhere or subject my head to both an internal and external suntan! I think I spent most the day sleeping trying to recover from the cumulative effects of the radiotherapy then had a takeaway (Indian) in the evening. All in all the treatment had been long and repetitive, but the side effects I had were all manageable.&lt;/p&gt;&lt;p&gt;The one thing I would say about the whole experience is it&#39;s quite lonely. That&#39;s not to say I&#39;m not surrounded by friends,family and colleagues (past and present) but at the end of the day it&#39;s me who is waiting around and then me lying in the machine(s) for the treatment and you can only ever do that alone. It certainly doesn&#39;t help at the moment that Covid restrictions don&#39;t allow &lt;em&gt;anyone&lt;/em&gt; to be with you in the hospital. So if you are waiting 1.5 hours for a delayed machine. You are waiting alone.&lt;/p&gt;&lt;p&gt;But I&#39;d like to say a huge thank you to all my friends / family and the wonderful NHS Transport service for getting me too and from each of my sessions over the 6.5 weeks.&lt;/p&gt;&lt;p&gt;So what next? Well I have approximately 6 weeks off over Christmas then should start 4-6 cycles of PCV chemotherapy some time mid-January. All being well this will last approximately 7 months. Well that&#39;s a lot of 2022 planned for then! I&#39;m hoping that around March they will be able to do another MRI scan of my brain and tell me what difference the radiotherapy made to Gary&#39;s leftovers. Apparently it takes around 3 months for the brain to recover and repair after radiotherapy, and until it does they can&#39;t see on the scans if it has made a difference.&lt;/p&gt;&lt;p&gt;So watch this space for more updates in 2022, where the remaining 5% of Gary is hopefully evicted from my head!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Picture of me walking out of the radiotherapy ward for the final time!&quot; decoding=&quot;async&quot; height=&quot;673&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NWbsLK2fWi-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NWbsLK2fWi-300.webp 300w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NWbsLK2fWi-600.webp 600w, https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/NWbsLK2fWi-814.webp 814w&quot; width=&quot;814&quot; title=&quot;A picture of me walking out of the radiotherapy ward for the final time!&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;oncology-meeting-wednesday-29th-december&quot;&gt;Oncology Meeting - Wednesday 29th December&lt;/h2&gt;&lt;p&gt;So since my last update at the start of December, not much has really happened. I&#39;ve had 5-6 weeks off, had a good Christmas with family and tried not to think too much about 2022 (although I must admit it&#39;s almost impossible). As mentioned before, the plan moving forwards was radiotherapy, followed almost immediately by chemotherapy. At my initial follow-up meeting &lt;a href=&quot;https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan/#further-treatment-appointment---wednesday-22st-september&quot;&gt;back in September&lt;/a&gt; very little was mentioned about the chemotherapy. This was intentional by the team, since getting my head around radiotherapy was enough for me at that point! So I&#39;ve only had a few basic details about it all. This meeting was to tell me a lot more.&lt;/p&gt;&lt;p&gt;As with most of my meetings, it was a video call, it included two oncologists and a senior nurse. They went through a few of the details about the type of chemo I&#39;m going to be having (it is called &lt;a href=&quot;https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv&quot;&gt;PCV&lt;/a&gt;), PCV stands for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;P – Procarbazine&lt;/li&gt;&lt;li&gt;C – Lomustine (CCNU)&lt;/li&gt;&lt;li&gt;V – Vincristine&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These are the three types of drugs I&#39;m going to be taking for the next few months. Vincristine is taken intravenously, the other two drugs are prescribed to me and taken in pill form at home.&lt;/p&gt;&lt;p&gt;I&#39;m going to be taking these drugs in what are called cycles. Each cycle is 42 days long, and I will have up to 6 cycles (assuming my body allows for it). A cycle consists of:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Day 1 - Go to the hospital and sit while they give me Vincristine via a drip.&lt;/li&gt;&lt;li&gt;Day 1 to 10 - take the Lomustine and Procarbazine every day at the same time each day&lt;/li&gt;&lt;li&gt;Day 11 to 42 - This is a rest period for my body to recover from the chemotherapy drugs.&lt;/li&gt;&lt;li&gt;Repeat this process up to 6 times if my body allows for it.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now, the difference between the radiotherapy and chemotherapy is that radio is very targeted (hence my ‘sunburnt&#39; head!). But chemotherapy isn&#39;t targeted at all, it is given to me and can have an impact on all the cells in my body. If you take a &lt;a href=&quot;https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv&quot;&gt;look at the potential side effects&lt;/a&gt; this is the reason why there can be so many!&lt;/p&gt;&lt;p&gt;A big negative of chemotherapy is it can have quite a big impact on my immune system. It reduces the white blood cell count in my body. These cells are used to combat infections. So the less white blood cells you have, the more susceptible to other illness&#39;. This is what I mean when I mention &quot;if my body allows for it&quot;. At the start of each cycle, the hospital looks at my white blood cell count to determine if I am healthy and able to continue with the next cycle. If not then they may lower the drug dosages or if it&#39;s terrible, pause the chemotherapy completely. Each and every person&#39;s body reacts differently to the drugs, so I won&#39;t know until I&#39;m into the first couple of cycles as to how my body reacts. So it&#39;s another journey into the unknown!&lt;/p&gt;&lt;p&gt;Six cycles at 42 days each, so I&#39;m looking at around 7 months of chemotherapy! That&#39;s quite a long time to try and not get sick! Especially during a global pandemic! Speaking of which, I asked about Covid and If I do end up catching it the NHS are able to give me &lt;a href=&quot;https://www.theguardian.com/world/2021/sep/18/covid-antibody-drug-ronapreve-to-be-given-to-vulnerable-nhs-patients&quot;&gt;special drugs&lt;/a&gt; to help my body combat the virus.&lt;/p&gt;&lt;p&gt;So it&#39;s people in my position across the world who are in danger when a huge portion of a population refuse to take the vaccine. As if life isn&#39;t bad enough when you have cancer, only to find going out people around you could make you even sicker! So yes, for the next 7 months I&#39;m generally going to be avoiding people I don&#39;t know, and asking friends and family I see to take a test before meeting with them. Not a great position to be in! I&#39;ll get off my soapbox now…&lt;/p&gt;&lt;p&gt;The only disappointing part of the meeting was they were unable to give me specific dates for my treatment or the hospital it would be at. This isn&#39;t the fault of the doctors, it&#39;s just a scheduling and administration bottleneck given the pressure the NHS is under at the moment. They said I&#39;d be receiving a few phone calls in the coming days to confirm dates etc. Until then, we wait!&lt;/p&gt;&lt;h2 id=&quot;houston-we-have-dates-thursday-30th-december-2021&quot;&gt;Houston, we have dates! - Thursday 30th December 2021&lt;/h2&gt;&lt;p&gt;Well, thankfully I didn&#39;t need to wait long for the dates. I received a few phone calls the day after the meeting:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Monday 10th January - chemotherapy pre-admission&lt;/li&gt;&lt;li&gt;Tuesday 11th January - start of cycle 1&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In the previous meeting, they also mentioned that I&#39;d need to have another MRI scan to see what effect the radio has had on the remaining 5% of Gary, but they said this could take a while to schedule, and I may have started chemo before it actually happens.&lt;/p&gt;&lt;h2 id=&quot;death-to-2021-january-1st-2022&quot;&gt;Death to 2021 - January 1st 2022&lt;/h2&gt;&lt;p&gt;As I said at the start of this very long blog post. Just when I thought 2020 was bad, 2021 comes along and proves me wrong! It has undoubtedly been one of the worst years of my life, but that being said I&#39;ve achieved a lot (well up until August anyway!). I worked up until 2 weeks before surgery, so I managed to pack quite a bit into the first 8 months. My year in review:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;January: Spoke at &lt;a href=&quot;https://archive.fosdem.org/2021/&quot;&gt;FOSDEM21&lt;/a&gt;&lt;/li&gt;&lt;li&gt;February: Spoke at &lt;a href=&quot;https://ldnwebperf.org/sessions/how-to-read-a-webpagetest-waterfall-chart/&quot;&gt;London Web Performance&lt;/a&gt;&lt;/li&gt;&lt;li&gt;March: &lt;a href=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/&quot;&gt;Cloudflare Worker experimentation blog posts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;April: Diagnosed with a Grade 3 Brain tumour&lt;/li&gt;&lt;li&gt;May: Spoke at &lt;a href=&quot;https://conferences.css-tricks.com/conferences/2021-lazyload-webperf-2021/&quot;&gt;Web Directions Lazy Load&lt;/a&gt;&lt;/li&gt;&lt;li&gt;June: Lots of Hospital appointments&lt;/li&gt;&lt;li&gt;July: Missed the F1 at Silverstone due to the Covid risk before my surgery date.&lt;/li&gt;&lt;li&gt;August: Stopped work before surgery&lt;/li&gt;&lt;li&gt;September: Had awake open-head brain surgery&lt;/li&gt;&lt;li&gt;October: Recovery + Radiotherapy&lt;/li&gt;&lt;li&gt;November: Lots of radiotherapy&lt;/li&gt;&lt;li&gt;December: Turned 40 and got a Puppy&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Not bad, all things considered! Here&#39;s hoping for a better 2022! Fingers crossed the final 5% of Gary gets evicted from my head!&lt;/p&gt;&lt;h2 id=&quot;mri-scan-scheduled-thursday-6th-january-2022&quot;&gt;MRI Scan scheduled - Thursday 6th January 2022&lt;/h2&gt;&lt;p&gt;So much for it taking a while to schedule the MRI! I received a phone call asking me to come in to hospital the next day (Friday) for my MRI scan. Not much notice, but the good thing (I hope) about this date is they should have the scans back for my pre-admission meeting, so you never know they may actually be able to give me an update on how the rest of Gary the prick is looking!&lt;/p&gt;&lt;h2 id=&quot;mri-scan-friday-7th-january&quot;&gt;MRI Scan - Friday 7th January&lt;/h2&gt;&lt;p&gt;You&#39;d have thought by now that the thought of lying still in a tube doesn&#39;t phase me, but it still does. There&#39;s nothing particularly scary about it. It&#39;s very loud and a little uncomfortable (and very boring). But other than the cannula for the contrast they inject, it&#39;s not that unpleasant. Even so, I still get a little nervous before it happens. I think it&#39;s because when you are in the machine alone with your thoughts, there&#39;s no way to ignore the fact that you have cancer. It isn&#39;t a bad dream that you are going to suddenly wake up from. It&#39;s actually that you are lying in this machine (again) for 25 minutes!&lt;/p&gt;&lt;p&gt;Anyway, the scan was uneventful. Just another hurdle to jump over in the whole process!&lt;/p&gt;&lt;h2 id=&quot;chemotherapy-pre-admission-monday-10th-january&quot;&gt;Chemotherapy pre-admission - Monday 10th January&lt;/h2&gt;&lt;p&gt;The meeting was a bit of a disappointment. I was expecting there to be a bigger team to discuss what was going to be happening, but in the end it was &quot;only a nurse&quot; (her words, not mine!). I managed to get a few questions answered, but I have many more for the consultants / doctors.&lt;/p&gt;&lt;p&gt;Unfortunately, due to it being a specialist cancer centre, Claire was unable to be at the meeting with me. She had to sit in the car and listen in while I had her on speakerphone. This is due to the current Omicron covid variant restrictions. The team mentioned that if I&#39;d started just 2 weeks earlier, it wouldn&#39;t have been an issue. But they&#39;ve had quite a few patients partners walk into the centre who knowingly (or unknowingly) were covid positive, which obviously puts all patients at risk. So now they &lt;em&gt;only&lt;/em&gt; allow patients in for meetings and treatment. And as a patient, you have to bring a negative lateral flow test from that day with you before you are allowed to be admitted into the centre.&lt;/p&gt;&lt;p&gt;On a more positive note, I&#39;ve got all the dates for my cycles. So if all goes to plan, and I manage all 6, my ‘last day&#39; will be on the 20th September. Although I have no idea how long chemotherapy drugs stay in your system, so they may linger for a little while after that??&lt;/p&gt;&lt;h2 id=&quot;chemotherapy-cycle-1-start-tuesday-11th-january&quot;&gt;Chemotherapy cycle 1 start - Tuesday 11th January&lt;/h2&gt;&lt;p&gt;I was incredibly nervous about the chemotherapy, I&#39;m not 100% sure why, as the doctors had mentioned that it would likely not be as bad as the radiotherapy in my instance. I think it&#39;s probably due to the longevity of the treatment. The surgery was scary but in all honesty if something went wrong I doubt I&#39;d have known very much about it, I think it would have all just ended. Radiotherapy was localised long and repetitive, but once in the room it only took 5 minutes for the treatment, then you were done for the day. But with chemotherapy, I&#39;d be taking the treatment &quot;home with me&quot;. With a &lt;a href=&quot;https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv&quot;&gt;tonne of possible side effects&lt;/a&gt;, and a total of up to 6 cycles lasting up to 7 months, I&#39;m actually going to feel the effects of this for a long time into 2022. There&#39;s pretty much no avoiding getting ill in some way once started.&lt;/p&gt;&lt;p&gt;Thankfully, the journey to the cancer centre is nowhere near as tedious as the hospital trips for radiotherapy, and I only need to go once every 42 days, the rest of the cycle is at home. Once I&#39;d chosen a chair to sit in, the nurse came over and checked I was who I said I was, then explained what was going to happen (since it was my first cycle).&lt;/p&gt;&lt;p&gt;I was given the &lt;a href=&quot;https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/vincristine&quot;&gt;vincristine&lt;/a&gt; intravenously into my arm, then while that was happening I&#39;d speak to the pharmacy team about the rest of my drugs. Unfortunately, the drip has to go into the back of the hand or wrist for this type of drug. This is because vincristine can cause damage to skin when outside of veins. The inside of my elbow (where&#39;d I&#39;d prefer it to be given) has a void behind the vein, so if there were a leak then the nurses wouldn&#39;t know about it! Not ideal, but I can see why it is done! So in the end we managed to come to a compromise where I was cannulated on the side of my wrist behind my thumb.&lt;/p&gt;&lt;p&gt;Once the cannula was in my wrist, the drip only took 5 minutes to give me all the medicine it needed to. In this time, I spoke to the pharmacy team with Claire on the speakerphone, so she could listen in (she was sitting in the car due to covid restrictions). It was only then that I realised the sheer number of pills I was going to have to take at the start of each cycle. A minimum of 69 tablets over the first 10 days at different times of day, on top of the seizure tablets I already take! I&#39;m very glad I don&#39;t have problems taking tablets!&lt;/p&gt;&lt;p&gt;Once the drip had finished, that was it. I was sent home with a small green bag full of all the medication I&#39;d need for the next 41 days. Before the process starts all over again!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Side effects&lt;/strong&gt; It&#39;s been just under a week into my first cycle as I&#39;m writing this, and I have a pretty good idea as to how my body is reacting to it all. The first two days were pretty awful, very tired and lethargic and just generally not feeling 100%.&lt;/p&gt;&lt;p&gt;But It was into the third day that I started to feel the main side effect I&#39;ve noticed. Eating my breakfast on the third day, the milk tasted metallic and my tongue / mouth were extremely sore. I&#39;d love to say that this has improved, but it has actually got worse over the past days. Nothing I eat or drink tastes the same, and it has put me totally off food. It feels like I have a mouth full of ulcers. Not pleasant! Apparently this is a side effect of the vincristine so should hopefully diminish over time.&lt;/p&gt;&lt;p&gt;The only thing that has alleviated the pain is taking regular dosages of painkillers (more pills!), &lt;a href=&quot;https://www.corsodyl.co.uk/products/corsodyl/mouthwash/&quot;&gt;Corsodyl mouthwash&lt;/a&gt;, and &lt;a href=&quot;https://www.medicines.org.uk/emc/product/9257/smpc&quot;&gt;Difflam mouthwash&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If anyone is in the same position, here&#39;s a list of the foods I&#39;ve had so far that still taste like anything palatable:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Rice Pudding&lt;/li&gt;&lt;li&gt;Custard&lt;/li&gt;&lt;li&gt;Baked Beans&lt;/li&gt;&lt;li&gt;French fries (no salt)&lt;/li&gt;&lt;li&gt;Steak pie&lt;/li&gt;&lt;li&gt;Boiled (or mashed) potatoes&lt;/li&gt;&lt;li&gt;Sausages&lt;/li&gt;&lt;li&gt;Gravy&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;ll keep this list updated as I find more foods that taste okay, I hope there are more, else I&#39;m in for a very boring 7 months of food!&lt;/p&gt;&lt;p&gt;On a positive note, I haven&#39;t experienced any nausea, which is what I thought may happen. But then again, I &lt;em&gt;am&lt;/em&gt; taking 3 types of anti-sickness tablets! So maybe they are doing their job at the moment and I have that to look forward to later in the cycle? I hope not!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;1p/19q Codeleted gene update&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I mentioned &lt;a href=&quot;https://oncologypro.esmo.org/education-library/factsheets-on-biomarkers/1p-19q-co-deletion-in-glioma&quot;&gt;1p/19q Codeleted gene&lt;/a&gt;) before which will effect how effective the chemotherapy is against Gary, Thankfully it turns out that I &lt;em&gt;do&lt;/em&gt; have this gene mutation, this is a good thing as it should mean that the chemotherapy should be more effective in my case! Yay for genetics!&lt;/p&gt;&lt;h2 id=&quot;oncology-update-wednesday-19th-january&quot;&gt;Oncology Update - Wednesday 19th January&lt;/h2&gt;&lt;p&gt;I was told this meeting was to discuss the results of my MRI scan, so I was quite nervous in the days leading up to it. But in the end it turned out to mainly be a catchup about how cycle 1 was going. I updated the team on the side effects I was experiencing and asked them a number of questions, one of these was how my brain was looking on the scan. Due to the fact that my brain is still healing from Decembers radiotherapy, they couldn&#39;t tell me too much. But they did say that the tumour was looking &quot;stable&quot;, which I guess is good! They said that my next scan (date unknown) should tell us a lot more, as it will be past the 10-12 week cut-off for my brain healing from radiotherapy.&lt;/p&gt;&lt;h2 id=&quot;chemotherapy-cycle-1-end-friday-21st-january&quot;&gt;Chemotherapy cycle 1 end - Friday 21st January&lt;/h2&gt;&lt;p&gt;So technically this isn&#39;t the end of the cycle, but it is the end of taking all the chemo drugs for cycle 1! 10 days and 70+ tablets later, It&#39;s been a busy week, and I&#39;ve learnt a lot that I can take into the next 5 cycles.&lt;/p&gt;&lt;p&gt;So what have I learned in cycle 1?&lt;/p&gt;&lt;p&gt;Well, first thing is to make sure you are taking the correct anti-sickness pills! I&#39;d mistakenly been taking the wrong ones for the first few days. When you are feeling like crap and half-asleep at 6 in the morning, this was an easy mistake to make! They both look the same in colour and shape and come with some wonderful side effects! So yes, I won&#39;t be making that mistake again!&lt;/p&gt;&lt;p&gt;Another thing is to start using both sets of mouthwash and dosing up on constant painkillers from day 1. The mistake I made was to wait for the side effects to become almost unbearable, before contacting the cancer triage number. Hitting it hard from day 1 will hopefully make the first 4-5 days (the worst ones), more manageable!&lt;/p&gt;&lt;p&gt;I&#39;ve also learnt that only 1 in 10 people on PCV chemotherapy get the jaw / mouth side effects like I have! I mean, I wouldn&#39;t expect it any other way given the rarity of the type of tumour I have, where&#39;s the fun in that, huh, body? What a stupid bag of meat.&lt;/p&gt;&lt;p&gt;Lastly and probably most important is the types of foods I can and can&#39;t eat. After day 2 normal breakfast is out the window and replaced with food with an actual taste. By Day 5 my mouth starts to feel slightly better, at day 12 I was almost back to normal.&lt;/p&gt;&lt;p&gt;Now of course this is assuming the other 5 cycles are exactly the same as the first, but the Oncology team did say there would likely be a slight cumulative effect as the cycles progress, so prepare myself for it to get worse over time!&lt;/p&gt;&lt;p&gt;All in all, though, given the number of &lt;a href=&quot;https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv&quot;&gt;possible side effects&lt;/a&gt;, I feel I&#39;ve got off lightly for my first cycle. I&#39;m just hoping it stays that way for the following 5! But that&#39;s totally out of my control. Here&#39;s hoping my white blood count stays high enough to continue, and I don&#39;t catch any illness&#39; over the next 8 months! This may be a big ask given I&#39;m currently starting a phased return to work!&lt;/p&gt;&lt;h2 id=&quot;ongoing-work-months-of-march-april-2022&quot;&gt;Ongoing work - Months of March / April 2022&lt;/h2&gt;&lt;p&gt;Now, considering I&#39;ve only been working a limited number of hours during my &quot;phased return to work&quot; period, I&#39;ve felt It&#39;s been very productive. Obviously not 100%, but I set myself a daily set of tasks I&#39;d like to complete, then anything that doesn&#39;t get done gets &quot;bumped&quot; onto the next day or later in the week. Obvious really when you think about it, but I&#39;m really having to be that strict with myself, else I know I&#39;ll end up doing full days again and burning myself out! Not good for me (or work either!). I&#39;ve been logging all my working hours and what I&#39;ve been doing to see what pattern emerged over my &quot;recovery periods&quot;. Interestingly, you can see a pattern. As time passes and the drugs work out of my system, I&#39;m able to work slightly longer and achieve more each day. Some highlights at work for the past months include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Recruitment&lt;/strong&gt;: The community and I are very much recruiting FE developers. I&#39;ve been involved in all stages of this (CV sift, phone interview and face-to-face interviews).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Joiners&lt;/strong&gt;: We&#39;ve had a number of new joiners in the community. So I&#39;ve been involved in welcoming them to the department and making sure our guidance around the communities new starter &quot;buddy system&quot; is set out and well-documented.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;But the absolute highlight for me has to be the GOV.UK team has managed to remove &lt;a href=&quot;https://jquery.com/&quot;&gt;jQuery&lt;/a&gt; from all Frontend apps across GOV.UK.&lt;/p&gt;&lt;p&gt;Sorry to go into &quot;work mode&quot;, but this basically means 32 KB of compressed JavaScript has been removed that our users no longer need to download when they visit all GOV.UK pages. Now I know what you may be thinking, 32 KB that&#39;s nothing! So that&#39;s true for a single user, but GOV.UK see&#39;s millions of users every month, so it multiplies to a huge data saving for all our users! I tweeted about the results we saw in our performance monitoring (&lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1509487050122276864&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;RUM&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1511995957483048960&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Synthetic&lt;/a&gt;) and the response was quite crazy!&lt;/p&gt;&lt;p&gt;The performance data speaks for itself, really. I highlighted the fact that this has improved the web performance of GOV.UK for the users who really need it (our P95 users), These are users on limited data plans and low-spec devices. By improving performance for these users actually improves performance for everyone!&lt;/p&gt;&lt;p&gt;So a massive pat on the back to the GOV.UK team for getting this change out the door it&#39;s been a slog but if you don&#39;t remove tech-debt it will eventually drown you and stop you from moving forwards.&lt;/p&gt;&lt;p&gt;We also had some sad news in the community, Alex J a fantastic Lead Frontend Developer and great friend and colleague decided to leave the department to return home to Romania. I can&#39;t tell you how genuinely gutted I was when I heard the news he was leaving, his dedication and knowledge on GOV.UK and his leadership in the Frontend community will be surely missed! I wish Alex all the best and hope our paths cross again in the future!&lt;/p&gt;&lt;p&gt;Sorry for that digression into work. I&#39;m also happy that I&#39;ve proved that I&#39;m still able to work sans-Gary (the prick!). So now back to the &quot;fun part&quot;, my cancer treatment (what an odd statement!)&lt;/p&gt;&lt;h2 id=&quot;oncology-update-wednesday-6th-april-morning&quot;&gt;Oncology Update - Wednesday 6th April (morning)&lt;/h2&gt;&lt;p&gt;As always, a busy start to the next cycle. Oncology review in the morning, chemotherapy treatment in the afternoon.&lt;/p&gt;&lt;p&gt;I was genuinely nervous about this meeting, as 2 weeks before I&#39;d had yet another MRI scan. This was the first scan that would be able to &quot;show anything&quot;. Since it takes 3 months for the brain to &quot;repair&quot; after radiotherapy. In the last MRI scan, nothing could be said for certain, as damaged brain matter looks the same as a brain tumour on an MRI scan (talk about ironic!).&lt;/p&gt;&lt;p&gt;But anyway, the meeting went really well. My bloods looked great, and the scan was described as &quot;stable&quot; or in other words what is left of Gary hadn&#39;t grown. The Oncologist at one point also said that the tumour was very difficult to see! I think that&#39;s the best news I could have had! It certainly makes up for the nights leading up to the meeting where I would wake up thinking about it!&lt;/p&gt;&lt;p&gt;The chemotherapy would be going ahead as planned on the same dosage of &lt;a href=&quot;https://www.cancerresearchuk.org/about-cancer/cancer-in-general/treatment/cancer-drugs/drugs/vincristine&quot;&gt;Vincristine&lt;/a&gt; I&#39;d had before.&lt;/p&gt;&lt;h2 id=&quot;chemotherapy-cycle-3-start-wednesday-6th-april-afternoon&quot;&gt;Chemotherapy cycle 3 start - Wednesday 6th April (afternoon)&lt;/h2&gt;&lt;p&gt;Nothing really to report here, only we turned up an hour early for my appointment! I think my frazzled brain got the time wrong, thankfully the cancer centre were able to fit me in any way. I had the infusion, but then had to wait awhile for my other medication (it takes time to prepare). I think my punctuality caused that issue, not the nursing team!&lt;/p&gt;&lt;p&gt;But once completed, I set off on my merry way with a body full of vincristine and a bag of 90+ tablets.&lt;/p&gt;&lt;h2 id=&quot;chemotherapy-cycle-3-end-and-recovery-saturday-16th-april&quot;&gt;Chemotherapy cycle 3 end and recovery - Saturday 16th April&lt;/h2&gt;&lt;p&gt;As I&#39;ve found with the previous cycles, it starts off slowly in the first few days, then ramps up around day 3 or 4. This is while the body adjusts to the concoction of drugs sent in to kill cells (mainly the bad ones, but there is friendly fire with chemotherapy as it isn&#39;t targetted like radiotherapy).&lt;/p&gt;&lt;p&gt;I was able to work a couple of hours on the Thursday, which got me thinking &quot;oh, maybe this cycle won&#39;t be so bad&quot;. Boy was I wrong about that! I think I jinxed it, so I won&#39;t be making that mistake again in the future!&lt;/p&gt;&lt;p&gt;I won&#39;t go into a day-by-day account, but let&#39;s just say the cycle turned out to be worse than cycle 1. I&#39;ve never felt so bad for so many days with no end in sight! The fact that I knew I needed to keep taking tablets every day that were only going to make me feel even worse, is a horrible feeling. It&#39;s a real struggle to make yourself take them at some points in the 10-days! Some wonderful side effects from this cycle (in no particular order) have been:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Nausea (no vomiting, thankfully)&lt;/li&gt;&lt;li&gt;Weakness (as in, can&#39;t get out of bed or even move)&lt;/li&gt;&lt;li&gt;Headaches (thank you paracetamol!)&lt;/li&gt;&lt;li&gt;Light-headed and dizziness (even when sitting on the sofa)&lt;/li&gt;&lt;li&gt;Spiked blood pressure&lt;/li&gt;&lt;li&gt;Lack of appetite&lt;/li&gt;&lt;li&gt;Random pains all over my body&lt;/li&gt;&lt;li&gt;Insomnia&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And to top it all off, I now seem to have developed &lt;a href=&quot;https://www.nhs.uk/conditions/tinnitus/&quot;&gt;Tinnitus&lt;/a&gt; in both ears, this is a known side effect of the vincristine and can be temporary or permanent (only time will tell).&lt;/p&gt;&lt;p&gt;So, yes, if you are reading this and are new to chemotherapy it probably isn&#39;t a fun read, but as they say &quot;no pain, no gain&quot; (FYI: don&#39;t say this to a person going through chemo unless you want a stern look or a strongly-worded letter!).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;So what have I learned in cycle 3?&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Don&#39;t go into any new cycle expecting the same side effects from the previous cycles, you are setting yourself for a fall!&lt;/li&gt;&lt;li&gt;Only take the anti-sickness pills if you are actually feeling sick (see my comments below on this)&lt;/li&gt;&lt;li&gt;Make sure you are open and honest with the team managing your treatment, keep them updated regularly! They are there to help you!&lt;/li&gt;&lt;li&gt;Try to focus on the recovery period, it takes a while for the drugs to leave your system. It can take up to 6 weeks for some people (it&#39;s likely you won&#39;t feel &quot;normal&quot; again until this happens).&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Just to jump back to point 2, there&#39;s conflicting information here. You are told to take them only when you feel sick, but also that it is easier to cure the &quot;feeling of sickness&quot; than the sickness itself! So yes, get in a time machine and see if you are likely to feel sick or be sick, then choose the correct path!&lt;/p&gt;&lt;p&gt;The reason why this point is so important is that each drug you take has its own unique set of side effects for your body. I found the anti-sickness pills were making me extremely weak and tired, so I stopped taking them midway through the cycle. Only then to find that the last few days of the cycle I&#39;d suffer from terrible Insomnia, because I no longer had the weakness and tiredness from the anti-sickness pills! It&#39;s a minefield! I&#39;ll be mentioning all this at my Oncology review meeting at the start of Cycle 4 (May 18th 2022), maybe they can adjust my drugs in some way?&lt;/p&gt;&lt;p&gt;To end on a more positive note, I&#39;m now over half-way through! 3 cycles down, 3 to go! Roll on October, and at some point back to &quot;normality&quot;.&lt;/p&gt;&lt;p&gt;So as before I likely won&#39;t be updating the post again until after my next cycle ends (end of May), unless something drastic happens!&lt;/p&gt;&lt;div id=&quot;postBottom&quot;&gt; &lt;/div&gt;&lt;h2 id=&quot;diagnosis-anniversary-thursday-21st-april&quot;&gt;Diagnosis Anniversary - Thursday 21st April&lt;/h2&gt;&lt;p&gt;So here&#39;s an anniversary that I never thought I&#39;d be ‘celebrating&#39; in a million years, but here we are. Celebrating is totally the wrong word, but it is a significant date, as 1-year ago today it felt like the whole ground fell from under me. My life, what was once a path paved and walked day by day, suddenly became a fog of unknown obstacles and fear. So many questions that my whole family and I had no idea about! I mean, why would you read into brain cancer if you didn&#39;t need to?!&lt;/p&gt;&lt;p&gt;It has been both the longest and shortest year of my life, when you live it day-to-day everything drags! Radiotherapy is relentless, a 6-week-long tidal wave that never seems to end. Avoiding everyone during chemotherapy is depressing because you are scared to even catch a common-cold! It&#39;s been a real rollercoaster of emotions!&lt;/p&gt;&lt;p&gt;Some particular points that stand out to me:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Scariest moments&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Seeing the size of Gary for the first time over the video call with the surgeon!&lt;/li&gt;&lt;li&gt;Closely followed by heading (hah!) into the surgery for my &lt;a href=&quot;https://www.hey.nhs.uk/patient-leaflet/undergoing-awake-craniotomy/&quot;&gt;awake craniotomy&lt;/a&gt;, Not knowing if I was going to wake up parallelised, partially-parallelised, or even wake up at all!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Low points&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Obviously being diagnosed with brain cancer is up there at the top of the list, but also telling my wife, and parents and sister of the diagnosis.&lt;/li&gt;&lt;li&gt;The week between initial diagnosis and actually having more detailed information from the hospital (that was a long week!)&lt;/li&gt;&lt;li&gt;Seeing the look on my mom&#39;s face when we were initially shown the size of Gary.&lt;/li&gt;&lt;li&gt;Having those horrible conversations with my wife about what to do if the &quot;worst should happen&quot;&lt;/li&gt;&lt;li&gt;This list could go on for a while, so I&#39;ll spare you and stop at 4!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;High points&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Realising there were lots of options available, and I shouldn&#39;t see this as a &quot;death sentence&quot;.&lt;/li&gt;&lt;li&gt;Seeing the outpouring of support both personally (family &amp; friends), at work, and across the whole web community when I first announced my diagnosis.&lt;/li&gt;&lt;li&gt;Being told they&#39;d been able to remove 95% of the tumour with little to no-damage to any of the good parts of my brain.&lt;/li&gt;&lt;li&gt;Finishing radiotherapy after a lot off back-and-forth to the hospital every week day for 6 weeks!&lt;/li&gt;&lt;li&gt;Receiving the &lt;a href=&quot;https://www.lego.com/en-gb/product/millennium-falcon-75192&quot;&gt;UCS Lego Millennium Falcon&lt;/a&gt; from some very kind friends in the Web Performance community (build in progress at the moment!)&lt;/li&gt;&lt;li&gt;Realising I can still think (and work) in the post-Gary world (&quot;full recovery&quot; may take a while, but I can do it!)&lt;/li&gt;&lt;li&gt;My wife raising over £2,700 for &lt;a href=&quot;https://www.thebraintumourcharity.org/&quot;&gt;The brain Tumour Charity&lt;/a&gt; for her entry into the &lt;a href=&quot;https://www.tcslondonmarathon.com/&quot;&gt;2022 London Marathon&lt;/a&gt;. Thanks to everyone who donated, you rock!&lt;/li&gt;&lt;li&gt;Seeing how we have come together as a family to cope with the scary and ever-changing world of the cancer diagnosis and treatment process.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To jump on the last point a more. I can&#39;t thank my close family and friends enough over what has been an incredibly tough year! I certainly couldn&#39;t have made it this far without the daily support of my wife, kids, parents, sister and her husband, and my mother-in-law.&lt;/p&gt;&lt;p&gt;Admittedly, my kids are the absolute perfect age to drive me to distraction, but that&#39;s exactly what is needed. Life would be so boring without their daily arguments and tantrums!&lt;/p&gt;&lt;p&gt;And last (but certainly not least!), a huge thank you to the help, support and dedication of the whole of the NHS workers I&#39;ve interacted with over the past year (so so many individuals!). Without them and modern medicine, I hate to think where I would be now, 1-year into my diagnosis!&lt;/p&gt;&lt;h2 id=&quot;treatment-tally-table&quot;&gt;Treatment Tally Table&lt;/h2&gt;&lt;p&gt;I thought I&#39;d pull together a tally of all the treatment I&#39;ve had along the way in my quest to evict Gary from my skull, which I will keep updated below.&lt;/p&gt;&lt;p&gt;As I&#39;ve said before, thank you again to the NHS. All this would have cost a fortune in &lt;em&gt;certain&lt;/em&gt; countries without universal healthcare (sorry American readers)!&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Treatment&lt;/th&gt;&lt;th&gt;Number&lt;/th&gt;&lt;th&gt;Finished?&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Radiotherapy&lt;/td&gt;&lt;td&gt;33&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Chemotherapy Cycles&lt;/td&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MRI Scans&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CT Scans&lt;/td&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PET Scans&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Major Surgery&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;Yes?&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;21/04/21: Tumor first diagnosed.&lt;/li&gt;&lt;li&gt;12/05/21: Initial post published.&lt;/li&gt;&lt;li&gt;15/05/21: Posted 3 new updates from the week. Next appointment on the 20th May.&lt;/li&gt;&lt;li&gt;19/05/21: Added information about another seizure.&lt;/li&gt;&lt;li&gt;21/05/21: Added information about the pre-operative assessment appointment.&lt;/li&gt;&lt;li&gt;25/05/21: Added update about the weekend and another seizure I&#39;ve had.&lt;/li&gt;&lt;li&gt;29/05/21: Added information about obtaining my scan data and a different type of seizure.&lt;/li&gt;&lt;li&gt;03/06/21: Added update on date for memory test and overall symptoms.&lt;/li&gt;&lt;li&gt;07/06/21: Added steroid and reduced seizures update.&lt;/li&gt;&lt;li&gt;10/06/21: Added update from latest Neurologist meeting.&lt;/li&gt;&lt;li&gt;11/06/21: Update on first &#39;core feature&#39; seizure I&#39;ve had in 2 weeks.&lt;/li&gt;&lt;li&gt;19/06/21: Added an update about the sleepy seizure and an epiphany.&lt;/li&gt;&lt;li&gt;18/07/21: Added an updates about the month of July and &quot;Between Life and Death&quot;.&lt;/li&gt;&lt;li&gt;23/07/21: Update on the latest timings and the surgery info I&#39;ve been given.&lt;/li&gt;&lt;li&gt;26/07/21: Pre-surgery MRI scan update.&lt;/li&gt;&lt;li&gt;01/08/21: Scan results and swelling symptoms update.&lt;/li&gt;&lt;li&gt;06/08/21: Back on the &#39;roids and finally a productive day!&lt;/li&gt;&lt;li&gt;12/08/21: The highs and lows of steroids and finally some information on an operation date!&lt;/li&gt;&lt;li&gt;23/08/21: Added info about a surgery date and the fact I had another seizure.&lt;/li&gt;&lt;li&gt;27/08/21: Surgery cancelled, and an update on the very long week.&lt;/li&gt;&lt;li&gt;04/09/21: New surgery date is looking promising update, fingers crossed it will be next week!&lt;/li&gt;&lt;li&gt;08/09/21: Added update on the scans today and my final post for a little while.&lt;/li&gt;&lt;li&gt;21/12/21: Added a huge post-surgey update on the recover, radiotherapy and future plans.&lt;/li&gt;&lt;li&gt;17/01/22: Added an update about my chemo meetings and first cycle. Also added a tally table for all the treatment I have recieved so far.&lt;/li&gt;&lt;li&gt;17/01/22: Added an update to my 1p/19q Codeleted gene result, thanks to &lt;a href=&quot;https://twitter.com/milesm999&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Miles Mandelson&lt;/a&gt; for reminding me about this result.&lt;/li&gt;&lt;li&gt;24/01/22: Added update on Cycle 1 and 2 of chemo and the oncology meetings&lt;/li&gt;&lt;li&gt;19/04/22: Added update on Cycle 3 recovery and return to work.&lt;/li&gt;&lt;li&gt;21/04/22: Added update on the 1 year diagnosis anniversary.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>How to use WebPageTest&#39;s Graph Page Data view</title>
    <link href="https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/" />
    <updated>2021-04-13T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/</id>
    <content type="html">&lt;p&gt;WebPageTest is a mysterious beast. You can use it almost every day for years and every so often a link catches your eye that you&#39;ve never noticed before. It then leads you to a whole new set of functionality that you didn&#39;t know it had. It&#39;s happened so often I&#39;ve actually &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#hidden-gems&quot;&gt;written a whole section about the &#39;hidden gems&#39;&lt;/a&gt; in my &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;&#39;How to read a WebPageTest waterfall chart&lt;/a&gt; blog post. In this blog post I&#39;m going to focus on one of those &#39;hidden gems&#39;: the &#39;Plot Full Results&#39; link that sits under the performance results on the &#39;Summary&#39; tab. Unsure what I&#39;m talking about? See the image below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;On the summary tab, just under the performance table you will see a link to &#39;Plot Full Results&#39;.&quot; decoding=&quot;async&quot; height=&quot;287&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/RMamZhv1x2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/RMamZhv1x2-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/RMamZhv1x2-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/RMamZhv1x2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Clicking on this link will bring you to a page with a number of options and graphs available to you. It may be a little confusing as to what you are seeing at first, but let&#39;s go through it together.&lt;/p&gt;&lt;h2 id=&quot;context&quot;&gt;Context&lt;/h2&gt;&lt;p&gt;First let&#39;s cover what this page is doing, as there isn&#39;t much on the page to really explain it. This page view exists to give you an overview of all the test runs that WebPageTest ran for the site you are testing. WebPageTest will pick the results from the &lt;strong&gt;median&lt;/strong&gt; test run for either the &#39;SpeedIndex&#39; or &#39;Load Time&#39; metrics. By default it will use &#39;SpeedIndex&#39; to choose the median run.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Annotated view of the summary run data.&quot; decoding=&quot;async&quot; height=&quot;239&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Gc5n7GY1_m-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Gc5n7GY1_m-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Gc5n7GY1_m-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Gc5n7GY1_m-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the image above the data being shown in the table is from &#39;Run 2&#39; of 3. The reason for multiple runs is because loading the same page under what seems like the same conditions, often returns wildly different results. This is the non-deterministic nature of the web. Variations in network traffic between the test agent and the website being tested (as well as many other factors) creates these variations in the end result. In order to reach a result that accurately represents the &lt;em&gt;actual&lt;/em&gt; performance of a website at the given time, WebPageTest runs multiple test runs. It then picks the test run slap bang in the middle for the chosen median metric as the representative run.&lt;/p&gt;&lt;p&gt;This is the reason why you should run as many test runs as possible when using WebPageTest. On the public instance of WebPageTest you are limited to a maximum of 9 test runs. But if you have a private instance you can run as many test runs as you like. Just remember to always pick an odd number of runs, since you are wanting WebPageTest to return the median run. An even number of tests has no median test run, since dividing the total number of runs (minus 1 for the median) by 2 will return a non-integer number. e.g. (8 runs - 1) / 2 = 3.5. How do you pick the 3.5th test run?&lt;/p&gt;&lt;p&gt;But what about if you &lt;em&gt;are&lt;/em&gt; interested in the results from the other runs in the test, not just the one WebPageTest has chosen? Well this is where the &lt;code&gt;graph_page_data.php&lt;/code&gt; page helps. It gives you information about important metrics reported across &lt;em&gt;all&lt;/em&gt; test runs that completed.&lt;/p&gt;&lt;h2 id=&quot;the-graphs&quot;&gt;The graphs&lt;/h2&gt;&lt;p&gt;So I&#39;m going to describe this in reverse compared to the actual page layout, as I think it&#39;s easier to explain once you have an idea of what metrics are being plotted in the graphs. The complete list can be seen below:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Web Vitals - First Contentful Paint (FCP)&lt;/li&gt;&lt;li&gt;Web Vitals - Largest Contentful Paint (LCP)&lt;/li&gt;&lt;li&gt;Web Vitals - Cumulative Layout Shift (CLS)&lt;/li&gt;&lt;li&gt;Web Vitals - Total Blocking Time (TBT)&lt;/li&gt;&lt;li&gt;Load Time (onload)&lt;/li&gt;&lt;li&gt;Browser-reported Load Time (Navigation Timing onload)&lt;/li&gt;&lt;li&gt;DOM Content Loaded (Navigation Timing)&lt;/li&gt;&lt;li&gt;SpeedIndex&lt;/li&gt;&lt;li&gt;Time to First Byte (TTFB)&lt;/li&gt;&lt;li&gt;Base Page SSL Time&lt;/li&gt;&lt;li&gt;Time to Start Render&lt;/li&gt;&lt;li&gt;Time to Interactive&lt;/li&gt;&lt;li&gt;Time to Visually Complete&lt;/li&gt;&lt;li&gt;Last Visual Change&lt;/li&gt;&lt;li&gt;Time to Title&lt;/li&gt;&lt;li&gt;Fully Loaded&lt;/li&gt;&lt;li&gt;Estimated RTT to Server&lt;/li&gt;&lt;li&gt;Number of DOM Elements&lt;/li&gt;&lt;li&gt;Connections&lt;/li&gt;&lt;li&gt;Requests (Fully Loaded)&lt;/li&gt;&lt;li&gt;Requests (onload)&lt;/li&gt;&lt;li&gt;Bytes In (onload)&lt;/li&gt;&lt;li&gt;Bytes In (Fully Loaded)&lt;/li&gt;&lt;li&gt;Custom Metrics included below.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There are a total of 24 metrics tracked in total (at the time of writing).&lt;/p&gt;&lt;h3 id=&quot;basic-graph&quot;&gt;Basic graph&lt;/h3&gt;&lt;p&gt;Let&#39;s focus on a basic graph and examine it:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Graph for the last visual change metric.&quot; decoding=&quot;async&quot; height=&quot;464&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/jrJT2B9ynU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/jrJT2B9ynU-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/jrJT2B9ynU-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;The graph is for the last visual change metric. Along the horizontal (x-axis) we see each of the 9 runs from our test. Up the vertical (y-axis) we see the time measured in milliseconds. The value from each test run for this metric is represented by a red dot on the graph. As you can see, there&#39;s quite a variance for this metric over the 9 runs. The lowest being run number 2 at 9,043 ms. The highest, run number 5 at 10,920 ms.&lt;/p&gt;&lt;p&gt;But each graph comes with an associated table that extracts important statistical information about the metric across all runs.&lt;/p&gt;&lt;h3 id=&quot;basic-table&quot;&gt;Basic table&lt;/h3&gt;&lt;p&gt;Below we see the table associated with the graph seen above. Last visual change metric over 9 runs.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Table associated with the last visual change graph from 9 test runs.&quot; decoding=&quot;async&quot; height=&quot;81&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/9vRk6WXA0M-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/9vRk6WXA0M-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/9vRk6WXA0M-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/9vRk6WXA0M-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Let&#39;s go over each of the columns one by one:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the arithmetic (population) mean, or also commonly referred to as the average value of all the test runs for a selected metric. For this you add all the metric values together and divide by the total number of runs. It&#39;s worth mentioning that the mean can be easily distorted if you have extreme variations higher or lower than the expected value (i.e. a high standard deviation).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Median&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I touched on the median value a little earlier in the post. The median (or p50) is the value of the metric that falls right in the middle of the set of results. Each set is the total number of test run by WebPageTest.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;p25&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;p25 stands for the 25th percentile. In web performance terms this is basically saying: &quot;25% of users will have a score for this metric that is better than this value&quot;. Users in this region are likely receiving a good overall experience.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;p75&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;p75 stands for the 75th percentile. The same sentence as used with p25 works here: &quot;75% of users will have a result for this metric that is better than this value&quot;. By improving the performance of a metric at p75, you will be improving it for all users sitting at, or beyond it in the distribution curve (e.g. the low performing 25% long-tail users).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;p75 - p25&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In statistics this is known as the interquartile range, or IQR. This is giving us an idea of the difference between the 75th percentile and the 25th percentile. If this value is small we know the values for the metric are all fairly close to the median.&lt;/p&gt;&lt;p&gt;In our table we can see that if the last visual change metric value increases by 807 ms, it slips from the 25th percentile to the 75th percentile. IQR is a good measure of variation because it doesn&#39;t take outliers into account. They are removed from the calculation since it is only focussing on the inner 50% of the data where most results will be clustered.&lt;/p&gt;&lt;p&gt;In terms of web performance we want to minimise this value. We want to pull p75 users up closer to p25 users so everyone receives a better experience.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;StdDev&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This stands for &#39;standard deviation&#39;, which is represented with the Greek letter σ (sigma) . It tells us how spread out our data set is from the mean. The standard deviation can be used to tell us if a result is statistically significant, or part of the expected variation. See the &lt;a href=&quot;https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule&quot;&gt;&quot;68–95–99.7 rule&quot;&lt;/a&gt; for more information on how standard deviation can be used to see if a result is expected, or an outlier.&lt;/p&gt;&lt;p&gt;A low standard deviation means the data is clustered close to the mean value. The data is much more predictable and the effect of variance is lower. We will see fewer outliers in the interquartile range. A high standard deviation tells us the data is dispersed over a wider range from the mean.&lt;/p&gt;&lt;p&gt;A lower standard deviation in a web performance context means the specific metric is more stable and predictable.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CV&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;CV stands for the Coefficient of Variation. This is the measurement of relative variability. It is simply the standard deviation divided by the mean, which produces a ratio. It is presented in the table as a percentage which is an optional, but sometimes more understandable step.&lt;/p&gt;&lt;p&gt;CV is essentially a normalisation process. It is useful when you want to directly compare sets of results that have different measures. E.g. If test A has a CV of 8%, and test B a CV of 15%, you would say test B has more variation than test A, relative to its mean.&lt;/p&gt;&lt;p&gt;In summary, all of the above values are giving us some insights into the spread and variance of the data from the test runs for a specific metric. For web performance, high variation is bad as it shows instability and less predictability for a metric.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Repeat view row&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The &#39;Repeat View&#39; row isn&#39;t seen in the above image but there is an option to run and view the repeat view data. &#39;First view&#39; data comes from a browser under &#39;cold cache&#39; conditions. By that I mean all assets need to be downloaded, nothing is cached.&lt;/p&gt;&lt;p&gt;If you have &#39;Repeat View&#39; selected there&#39;d be a second row below with another set of results for when the browser loads with a &#39;warm cache&#39; scenario. Comparing these two rows of data gives you some insight into the effect caching has on the metric being examined.&lt;/p&gt;&lt;h3 id=&quot;configuration-options&quot;&gt;Configuration options&lt;/h3&gt;&lt;p&gt;Now that we&#39;ve gone over the basic graph and table for a specific metric, it&#39;s a good time to look at the configuration options available to us at the top of the page:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Options available to us at the top of the page.&quot; decoding=&quot;async&quot; height=&quot;284&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/QBotGNaWQo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/QBotGNaWQo-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/QBotGNaWQo-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/QBotGNaWQo-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Let&#39;s examine these one by one.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;View (First/Repeat)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Pretty self-explanatory really. Which results do you want to be plotted to the graphs? I&#39;d say most of the time you always want the first view results plotted to see the metrics under &#39;cold cache&#39; conditions. If you have run a repeat view, then you can enable this to see how the same metric changes under &#39;warm cache&#39; conditions.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Median (Of plotted metric)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;When selected, WebPageTest will draw a line on the graph through the run that has been selected as the median.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Median run highlighted on the graph in red.&quot; decoding=&quot;async&quot; height=&quot;555&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/cwONwY2oht-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/cwONwY2oht-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/cwONwY2oht-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/cwONwY2oht-801.webp 801w&quot; width=&quot;801&quot;&gt;&lt;/p&gt;&lt;p&gt;In the graph above for Speed Index we can see the median run that has been selected is from run number 3, and it has a value of approximately 7,750.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Median (Run with median metric SpeedIndex)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;By default, the median run for the graphs is selected by examining the Load Time metric. The run that sits in the middle of all the runs is selected as the representative run for this test. But you can use another metric. Instead of looking at Load Time, we can tell WebPageTest to look at the SpeedIndex metric for the representative run.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Both median run metrics highlighted on the same graph.&quot; decoding=&quot;async&quot; height=&quot;484&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/piAk8KaOOE-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/piAk8KaOOE-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/piAk8KaOOE-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/piAk8KaOOE-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the graph above we can see the difference that changing the median metric has on each metric. Two lines are now shown, one for when Load Time is used, the other for when SpeedIndex is used:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Load Time: run 9 is selected which has a value of 12,558 ms&lt;/li&gt;&lt;li&gt;SpeedIndex: run 3 is selected which has a value of 11,706 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This functionality is useful as it gives you a quick way to examine what the difference would be to all the metrics if you change the metric used to select the median run.&lt;/p&gt;&lt;h2 id=&quot;statistical-comparison-against&quot;&gt;Statistical Comparison Against&lt;/h2&gt;&lt;p&gt;So this is where it starts to get really interesting. It isn&#39;t at all clear, but you can pass in multiple test ID&#39;s into this page view, as well as associated human readable labels to make the graph keys easier to understand. This is done using the following URL pattern:&lt;/p&gt;&lt;p&gt;&lt;code&gt;https://www.webpagetest.org/graph_page_data.php?tests=[TEST-ID-1]-l:[TEST-LABEL-1],[TEST-ID-2]-l:[TEST-LABEL-2],[TEST-ID-3]...&lt;/code&gt;&lt;/p&gt;&lt;p&gt;But don&#39;t worry if that sounds too laborious I have a tool that &lt;a href=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/#webpagetest-compare-tool&quot;&gt;I&#39;ll show you later&lt;/a&gt; to make it easier.&lt;/p&gt;&lt;p&gt;What this functionality allows you to do is pass in multiple test ID&#39;s and compare statistics against each other. So for example, say you are testing out a performance improvement and you are interested in seeing how this change has affected each metric at a statistical level. It allows you to do this.&lt;/p&gt;&lt;p&gt;In the dropdown you can select which test run you want to statistically compare all the other runs against. In the example below I&#39;m testing loading a page with 0% packet loss against one with 3% packet loss.&lt;/p&gt;&lt;h3 id=&quot;basic-graph-multiple-test-ids&quot;&gt;Basic graph - Multiple test ID&#39;s&lt;/h3&gt;&lt;p&gt;Now that we&#39;ve added another test ID, the graphs have changed:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Two sets of points are now seen on the graph, along with to median lines.&quot; decoding=&quot;async&quot; height=&quot;495&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/3rnBLS2i1V-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/3rnBLS2i1V-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/3rnBLS2i1V-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/3rnBLS2i1V-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Each graph now has two sets of points in different colours. We can now compare each set of test runs against each other. On the graph for the SpeedIndex metric we can see each median line has also been drawn:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Zero % PL median - run 3 selected with a SpeedIndex of approximately 7,800 ms&lt;/li&gt;&lt;li&gt;Three % PL median - run 4 selected with a SpeedIndex of approximately 11,000 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now considering these two sets of tests are comparing the same page load under different packet loss conditions, it makes sense that the 3% packet loss has a slower SpeedIndex. But we can find more data to back this up.&lt;/p&gt;&lt;h3 id=&quot;advanced-table&quot;&gt;Advanced Table&lt;/h3&gt;&lt;p&gt;Once the &#39;Statistical Comparison Against&#39; dropdown has been populated, a new table appears above each metrics graph. This table gives you a set of statistics that compares multiple sets of data for each metric:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The advanced table gives a set of statistics comparing the sets of results for the chosen metric.&quot; decoding=&quot;async&quot; height=&quot;120&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Ry6Ui1fq6M-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Ry6Ui1fq6M-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Ry6Ui1fq6M-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;You will notice that the bottom row has a set of blank cells. This is because it is the variant we are comparing all others against. First let&#39;s set some context: We are comparing two populations of data, one with no packet loss, one with 3% packet loss. The question is:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Does this packet loss have an effect on the SpeedIndex metric, and if so, how likely is it that it just happened at random?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Let&#39;s go through what each of the columns mean and see if there&#39;s enough evidence to answer the question:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Variant&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Pretty self explanatory, which set of data are we looking at in this row. By default the label you provide is used, if not then the test ID is used. Due to the length of a WPT test ID, it breaks the whole table UI and makes the table harder to read if you don&#39;t add your own human readable label (-l).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Count&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The number of test runs that were conducted during each test.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Mean +/- 95% Conf. Int&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is a confusing cell when you first look at it, but it actually contains 2 numbers. The first is the mean for the metric, the second is the value above and below the mean that covers the 95% &lt;a href=&quot;https://en.wikipedia.org/wiki/Confidence_interval&quot;&gt;confidence interval (CI)&lt;/a&gt;. This is known as a normal based confidence interval. The 95% is saying that: &#39;95% of the data for the test will sit between this range&#39;. The true value for the mean if we had a larger sample size (by having more test runs), has a very high probability of sitting within this range. The probability of observing a value outside of this range is less than 0.05 (or 5%).&lt;/p&gt;&lt;p&gt;Translating this directly to the table we see above:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;0% packet loss has a mean SpeedIndex of 7,583 ms, and the 95% confidence interval ranges from 6,744 ms to 8,422 ms&lt;/li&gt;&lt;li&gt;3% packet loss has a mean SpeedIndex of 11,041 ms, and the 95% confidence interval ranges from 8,894 ms to 13,188 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Diff of mean from [selected test]&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;What&#39;s the difference between the means for each set of tests. In the table above we see that &#39;Three PL&#39; has a mean value that is 3,457 ms higher than that of the 0% packet loss test.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;p-value (2-tailed)&lt;/strong&gt; P-value stands for percentage value. The p-value is the probability of obtaining test results at least as extreme as the results actually observed. 2-tailed means we consider values on both ends of the distribution curve (extremely high, or extremely low). The lower the p-value, the more meaningful the result because it is less likely to be caused by noise.&lt;/p&gt;&lt;p&gt;In the table above we see a probability of 0.006 (or 0.6%) is extremely low. So the likelihood of this result randomly occurring is very low.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Significant?&lt;/strong&gt; Whether a result is considered significant depends on the p-value we set for significance &lt;em&gt;before&lt;/em&gt; we start testing. In the case of WebPageTest, it uses the 95% confidence interval, this p-value is set at 0.05, or 5%. This corresponds to the 5% that sits &quot;outside&quot; our confidence interval. So there&#39;s a 5% chance the results beyond the 95% confidence interval occurred at random.&lt;/p&gt;&lt;p&gt;In the above table the data has a p-value of 0.006, which means the results &lt;em&gt;are&lt;/em&gt; significant, as the p-value is below 0.05 we set for significance. This is why the value in the table is &#39;TRUE&#39; rather than &#39;FALSE&#39;.&lt;/p&gt;&lt;p&gt;Or to summarise all the above: the fact that the Speed Index metric for the 3% packet loss test has a much higher mean than the 0% packet loss data isn&#39;t a random occurrence. It&#39;s a significant result that we should pay attention too, and we can feel confident that it&#39;s real. It isn&#39;t a result that is likely to occur just from random variance.&lt;/p&gt;&lt;h2 id=&quot;webpagetest-compare-tool&quot;&gt;WebPageTest Compare Tool&lt;/h2&gt;&lt;p&gt;So I mentioned a tool to simplify URL generation. Well back in April 2020 I created a very basic tool for generating comparison URLs for WebPageTest which I &lt;a href=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/&quot;&gt;blogged about here&lt;/a&gt;. The tool is available at &lt;a href=&quot;https://wpt-compare.app/&quot;&gt;https://wpt-compare.app/&lt;/a&gt;. Since writing this post you are reading now, I&#39;ve added a new bit of functionality that will generate both the filmstrip URL (&lt;code&gt;/video/compare.php&lt;/code&gt;) and the graph comparison page (&lt;code&gt;/graph_page_data.php&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The wpt-compare tool can be used to generate a filmstrip URL and now the test graph URLs.&quot; decoding=&quot;async&quot; height=&quot;323&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/uzYCYk5aEG-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/uzYCYk5aEG-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/uzYCYk5aEG-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/uzYCYk5aEG-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;To use it simply enter the test URLs and then give each test a more readable label. Click &#39;Generate URLs&#39; and follow the links provided.&lt;/p&gt;&lt;h2 id=&quot;real-world-example&quot;&gt;Real-world example&lt;/h2&gt;&lt;p&gt;So let&#39;s take a look at a real world example and see what the data from the &lt;code&gt;/graph_page_data.php&lt;/code&gt; page shows us. Here&#39;s the test setup:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;URL: https://www.lenovo.com/gb/en/&lt;/li&gt;&lt;li&gt;Comparing: Gzip vs Brotli compression&lt;/li&gt;&lt;li&gt;Browser: Chrome Desktop&lt;/li&gt;&lt;li&gt;Connection: 3G (1.6 Mbps/768 Kbps 300ms RTT)&lt;/li&gt;&lt;li&gt;Test runs: 201&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The reason for choosing this site is because it has a high number of assets compressed using either Gzip or Brotli compression. I counted 40 in total. I used the list that &lt;a href=&quot;https://twitter.com/paulcalvano&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Paul Calvano&lt;/a&gt; posted on &lt;a href=&quot;https://twitter.com/paulcalvano/status/1252216874810716162&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter here&lt;/a&gt; to choose an appropriate website. 201 runs is a lot, so this was run on my own private WebPageTest instance (where there&#39;s no limit on the number of runs you can complete). If you try the same, just be prepared to leave it running overnight!&lt;/p&gt;&lt;p&gt;Testing Brotli vs Gzip compression with WebPageTest is very easy. Just add the following script in the &#39;Script&#39; tab when running the gzip test to force gzip compression:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;addHeader accept-encoding: gzip,deflate
navigate %URL%
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This adds a request header to all requests that overrides the browsers default. In doing so we are now essentially telling the server we &lt;em&gt;only&lt;/em&gt; support gzip compression, so it doesn&#39;t serve us brotli compressed files.&lt;/p&gt;&lt;h3 id=&quot;update-removing-dns-lookup-noise&quot;&gt;Update: Removing DNS lookup noise&lt;/h3&gt;&lt;p&gt;After posting this blog post to Twitter I had a really useful conversation with &lt;a href=&quot;https://twitter.com/TimVereecke&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tim Vereecke&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;. Tim raised an excellent point that depending on DNS performance between the runs, this could be adding noise to the results. After all, the DNS performance is out of our control and really has no part to play in the results we are trying to examine. So if we could remove it then we&#39;ll have a cleaner set of results. Thankfully this is entirely possible using WebPageTest scripting:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;setDnsName www.lenovo.com www.lenovo.com
navigate %URL%
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So what is this magic that is happening here? Thankfully &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Patrick Meenan&lt;/a&gt; was on hand to explain how this works:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;setDnsName does lookups for the second param then stores them as overrides in the hosts file, effectively caching the DNS lookups locally.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;But we can do better than this. That&#39;s when Andy suggested a more generic (and flexible) version of the script:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;setDnsName %host% %host%
navigate %url%
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This does the same as the first script, only you are no longer hard-coding the URL&#39;s. &lt;code&gt;navigate %url%&lt;/code&gt; will take whatever URL you have added to the test URL input box and use it within the script.&lt;/p&gt;&lt;p&gt;I reran all 402 test runs to see what difference it made. And it really has made quite a difference to the results. So I&#39;ve added a second version of the graphs to the metrics that were affected by this change.&lt;/p&gt;&lt;h3 id=&quot;a-stable-metric-ssl-time-with-dns-lookup&quot;&gt;A stable metric: SSL Time - with DNS lookup&lt;/h3&gt;&lt;p&gt;First let&#39;s take a look at a graph from a stable metric.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;SSL time is a stable metric, and it can be seen in the resulting graph.&quot; decoding=&quot;async&quot; height=&quot;630&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/N6yNSzy-Hg-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/N6yNSzy-Hg-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/N6yNSzy-Hg-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/N6yNSzy-Hg-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;First thing to notice about the graph above is how grouped together the results are. Both the green and red median lines are pretty much on top of each other. And this is confirmed with the values for the means and the CI&#39;s. Gzip&#39;s CI is plus or minus 1.14 ms. So over 201 runs, 95% of the data is going to sit between 330.4 ms and 332.7 ms. That&#39;s an incredibly small range. Brotli has slightly more variation, from 331.96 ms and 347.85 ms. With the p-value of 0.042, there&#39;s a 4.2% chance of one of the run values sitting outside the 95% confidence interval. This is a significant result, that we can feel confident is real.&lt;/p&gt;&lt;p&gt;Overall we can see that Brotli vs Gzip compression has no performance impact on SSL time. Which of course when you think about it, it can&#39;t. So I&#39;m glad we now have graph data to back this up!&lt;/p&gt;&lt;h3 id=&quot;a-stable-metric-ssl-time-without-dns-lookup&quot;&gt;A stable metric: SSL Time - without DNS lookup&lt;/h3&gt;&lt;p&gt;Since this test is examining only the SSL time in isolation, the DNS lookup time has no impact on this metric. The resulting graph was the same as above.&lt;/p&gt;&lt;h3 id=&quot;an-unstable-metric-ttfb-with-dns-lookup&quot;&gt;An unstable metric: TTFB - with DNS lookup&lt;/h3&gt;&lt;p&gt;Let us now turn our attention to what looks like a much more unstable metric: the Time to First Byte results:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;TTFB graph is very interesting with a clear distinction between the 2 sets of data.&quot; decoding=&quot;async&quot; height=&quot;635&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/npzklZNuWC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/npzklZNuWC-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/npzklZNuWC-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/npzklZNuWC-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I&#39;ll be completely upfront about this graph, I&#39;m very surprised by it. From my understanding, I didn&#39;t &lt;em&gt;think&lt;/em&gt; compression would have an impact on TTFB at all, since I&#39;m assuming all compressed asset versions were being cached by the CDN. But the graph shows a distinction between the two compression algorithms, with gzip having both a lower mean and median across 201 test runs. The p-value comes out as 0 (which may be an error due to rounding of significant figures, I&#39;m unsure). Either way it is low compared to our 0.05 significance level set by the 95% confidence interval. So yes, this is a significant result that isn&#39;t just from random variation.&lt;/p&gt;&lt;p&gt;I have a theory about why this could be. Brotli compression at levels 10 &amp; 11 is &lt;a href=&quot;https://quixdb.github.io/squash-benchmark/#results-table&quot;&gt;incredibly resource intensive&lt;/a&gt;. So if for some reason this is happening &quot;on-the-fly&quot; each time, rather than being compressed and then cached, this could be causing the difference. It&#39;s worth noting that Lenovo use Akamai&#39;s Resource Optimizer to do this compression, so I&#39;d guess this isn&#39;t the case. But it&#39;s the only explanation I have at the moment. Any other explanations or ideas please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;, I&#39;d love to find out what is going on.&lt;/p&gt;&lt;p&gt;Here&#39;s another point about the &#39;graph page data&#39; view now worth mentioning: If it hadn&#39;t of been for the statistical analysis seen here, this would have been missed. Now ultimately it may turn out to be a false positive that means nothing at all, but at least it is now visible and can be investigated further.&lt;/p&gt;&lt;h3 id=&quot;an-unstable-metric-ttfb-without-dns-lookup&quot;&gt;An unstable metric: TTFB - without DNS lookup&lt;/h3&gt;&lt;p&gt;Let&#39;s examine the difference DNS lookup time has on TTFB. &lt;strong&gt;Note&lt;/strong&gt;: the y-axis scales are different so you can&#39;t directly visually compare.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;TTFB for the tests where DNS lookup time has been removed.&quot; decoding=&quot;async&quot; height=&quot;624&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/dddJ4CJbUY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/dddJ4CJbUY-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/dddJ4CJbUY-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/dddJ4CJbUY-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;We are looking at a mean value of 214 ms reduction (1,632-1,417) in the mean time for gzip, and a huge 441 ms reduction (1,754-1,313) in the mean TTFB for brotli. This has given us a significant result now saying that TTFB is improved by using brotli in this instance, with a 104 ms improvement in the mean over gzip.&lt;/p&gt;&lt;h3 id=&quot;obvious-winners-bytes-in-onload-and-fully-loaded-with-dns-lookup&quot;&gt;Obvious winners: Bytes in (onload and fully loaded) - with DNS lookup&lt;/h3&gt;&lt;p&gt;Well as we are examining compression it&#39;s probably a good idea to look at a metric where we&#39;re pretty sure we&#39;ll see improvements. The total number of bytes in:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Bytes In (onload) graph shows the number of bytes download up to the onload event firing.&quot; decoding=&quot;async&quot; height=&quot;634&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Hr_zdRzRn--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Hr_zdRzRn--300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Hr_zdRzRn--600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Hr_zdRzRn--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above graph we see the number of bytes downloaded by the browser up to the onload event firing. As you can see, Brotli compression has saved 60 KB up to this point in the page load. The p-value is low, and the result is real since significant is set to &#39;TRUE&#39;.&lt;/p&gt;&lt;p&gt;Moving onto the fully loaded graph:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Bytes In (fully loaded) graph shows the number of bytes download up to the page is fully loaded.&quot; decoding=&quot;async&quot; height=&quot;640&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/kjXCP1xeCE-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/kjXCP1xeCE-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/kjXCP1xeCE-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/kjXCP1xeCE-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we see the total number of bytes downloaded by the browser during the page load. The graph shows 2 distinct strips of green and red, clearly showing the two compression methods in action. The stats show that 44 KB was saved with Brotli compression being used. P-value is 0 meaning it is therefore significant, so is a reliable result.&lt;/p&gt;&lt;p&gt;In both graphs above, what surprises me is that these graphs don&#39;t just show a strip of green and red blobs across the graph. Some variance is actually shown. I have a couple of theories as to why this could be:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The CDN is under CPU load so a different version of files is served to ease the bottleneck (e.g. a gzip version is served instead of brotli)&lt;/li&gt;&lt;li&gt;Lenovo could have A/B testing in action on the homepage. Each bucket is slightly different meaning different assets are loaded&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Again, it&#39;s good that this graph view has exposed this peculiarity. If this were happening on my website I&#39;d like to get to the bottom of why it is happening. With this data it&#39;s now possible to investigate further.&lt;/p&gt;&lt;h3 id=&quot;obvious-winners-bytes-in-onload-and-fully-loaded-without-dns-lookup&quot;&gt;Obvious winners: Bytes in (onload and fully loaded) - without DNS lookup&lt;/h3&gt;&lt;p&gt;Since these graphs refer to the number of bytes downloaded by the browser, there aren&#39;t affected by the DNS lookup time. They are the same as seen in the graphs seen above.&lt;/p&gt;&lt;h3 id=&quot;the-unexpected-fcp-and-start-render-with-dns-lookup&quot;&gt;The unexpected: FCP and Start Render - with DNS lookup&lt;/h3&gt;&lt;p&gt;And finally let&#39;s look at a set of graphs that (for me) were quite unexpected, those related to First Contentful Paint (FCP) and start render:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;First Contentful Paint graph shows Brotli to be slower than Gzip.&quot; decoding=&quot;async&quot; height=&quot;636&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Qg0HJR4f-h-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Qg0HJR4f-h-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Qg0HJR4f-h-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/Qg0HJR4f-h-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above FCP graph we see that the mean and the median for gzip compression is actually lower than brotli, meaning the gzip page is rendering slightly quicker than brotli. Again the p-value is 0, so the results look to be trustworthy.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Start render graph for gzip looks to be quicker than brotli&quot; decoding=&quot;async&quot; height=&quot;611&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/-7Jtnwpe7R-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/-7Jtnwpe7R-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/-7Jtnwpe7R-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/-7Jtnwpe7R-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The same observation can be seen with the &#39;Start Render&#39; which is actually totally expected given &lt;a href=&quot;https://nooshu.github.io/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#start-render---green-line-&quot;&gt;how start render is measured&lt;/a&gt;. So this just solidifies the FCP observation above. Brotli start render time is 330 ms slower than gzip at the mean. Again the p-value is 0, so the results look to be trustworthy.&lt;/p&gt;&lt;p&gt;So why could this be happening? Well it&#39;s worth remembering that web performance is a problem space with many dimensions. Fewer bytes down the wire doesn&#39;t &lt;em&gt;always&lt;/em&gt; mean quicker. In this case it could be that brotli is slower to decompress on the device, so painting pixels to the screen is slower than gzip. Again this is another theory, but the data and analysis seem to suggest there&#39;s a significant result here that shouldn&#39;t be ignored.&lt;/p&gt;&lt;h3 id=&quot;the-unexpected-fcp-and-start-render-without-dns-lookup&quot;&gt;The unexpected: FCP and Start Render - without DNS lookup&lt;/h3&gt;&lt;p&gt;Let&#39;s re-examine the FCP and start render metrics with DNS lookup time removed. &lt;strong&gt;Note&lt;/strong&gt;: the y-axis scales are different so you can&#39;t directly visually compare.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;First Contentful Paint graph with DNS lookup time removed.&quot; decoding=&quot;async&quot; height=&quot;640&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/ACjX6iuCGn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/ACjX6iuCGn-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/ACjX6iuCGn-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/ACjX6iuCGn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There&#39;s been a huge change in the mean where brotli comes out a huge 632 ms quicker than gzip for FCP at the mean. Again another significant result with an incredibly low p-value of 0.001!&lt;/p&gt;&lt;p&gt;Moving onto start render:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Start render graph with DNS removed paints a very different paints quite a different picture.&quot; decoding=&quot;async&quot; height=&quot;633&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/sJrUvSd8PW-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/sJrUvSd8PW-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/sJrUvSd8PW-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/sJrUvSd8PW-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Where we really see the difference is in the mean values. In this analysis brotli is 512 ms quicker for start render than with gzip, compared to 331 ms slower from the first set of results! Again the p-value is 0 meaning this is a significant result. DNS lookup time really does have an impact on performance.&lt;/p&gt;&lt;h2 id=&quot;user-interface-updates&quot;&gt;User interface updates&lt;/h2&gt;&lt;p&gt;While writing this blog post I raised a couple of issues and including feature requests for this page view:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/pull/1482&quot;&gt;Fix the layout of the Statistical Comparison table.&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/issues/1484&quot;&gt;Allow multiple tables of data for graph_page_data.php when &gt; 1 test ID&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;compare-metrics-across-multiple-test-ids&quot;&gt;Compare metrics across multiple test IDs&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://twitter.com/tkadlec&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tim Kadlec&lt;/a&gt; kindly looked over these requests and added the ability to compare metrics like Mean, Median, StdDev etc across multiple test runs when more than 1 test ID is provided. So the new statistics tables look like below when passing in multiple test ID&#39;s:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The new layout of the statistics view showing multiple test ID&#39;s in the tables.&quot; decoding=&quot;async&quot; height=&quot;386&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/CuYgslELAG-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/CuYgslELAG-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/CuYgslELAG-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/CuYgslELAG-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example above we are comparing the FCP times as various percentages of packet loss are introduced to the connection.&lt;/p&gt;&lt;h3 id=&quot;force-graphs-y-axis-to-start-at-zero&quot;&gt;Force graphs y-axis to start at zero&lt;/h3&gt;&lt;p&gt;In some cases where comparing across test runs the graph would start at a y-axis value that wasn&#39;t 0. This can sometimes give the impression that the results are better than they actually are, because it amplifies the gains or losses seen. By forcing the y-axis back to 0 gives a more representative view of the data and the results. It is now possible to do this in the config options at the top of the page:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A new config option allows you to force the y-axis to zero on the graphs.&quot; decoding=&quot;async&quot; height=&quot;891&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/gcA2eK7DnY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/gcA2eK7DnY-300.webp 300w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/gcA2eK7DnY-600.webp 600w, https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/gcA2eK7DnY-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Many thanks to Tim for fixing the layout and adding these features.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;In this blog post we&#39;ve examined the seemingly invisible &#39;Plot Full Results&#39; link that sits under the summary table on every WebPageTest result summary page. A link that you may have never even clicked on! We&#39;ve examined what all the graphs and tables are and tried our hand at interpreting the results. On examining some real-world data we&#39;ve pulled out a few unusual results that may have been hard to spot without this page view. We&#39;ve also examined the difference that DNS performance can have on your test results. So consider removing this noise for your own testing in the future.&lt;/p&gt;&lt;p&gt;So next time you run a test using WebPageTest, why not click the &#39;Plot Full Results&#39; link and see what it tells you. Thanks for reading.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: It&#39;s been far too many years than I care to admit since I studied statistics, so there&#39;s been a fair amount of &#39;on-the-job&#39; learning happening while writing this post. So if you happen to spot anything that is totally wrong, or just poorly explained please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt; and I&#39;ll correct it and give you credit in the &#39;post changelog&#39; below.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;13/04/21: Initial post published. Big thanks to &lt;a href=&quot;https://twitter.com/tomnatt&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tom Natt&lt;/a&gt; for checking and feeding back on parts of this post during the draft stage.&lt;/li&gt;&lt;li&gt;16/04/21: Added new graphs to show the difference DNS performance can have on the results. Thanks to &lt;a href=&quot;https://twitter.com/TimVereecke&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tim Vereecke&lt;/a&gt; for the tip, and to &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for the script optimisation.&lt;/li&gt;&lt;li&gt;22/05/21: Added information about the UI updates from &lt;a href=&quot;https://twitter.com/tkadlec&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tim Kadlec&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Setting up Cloudflare Workers for web performance optimisation and testing</title>
    <link href="https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/" />
    <updated>2021-03-14T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/</id>
    <content type="html">&lt;p&gt;I recently published a blog post about &lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/&quot;&gt;Cloudflare Worker recipes&lt;/a&gt;. It occurred to me after publishing that I&#39;d offered no guidance at all to the reader as to how to get started using them. You need to do that before you can start using the recipes I&#39;d listed. So I aim to rectify that in this follow up blog post where I go through the process step by step, including images.&lt;/p&gt;&lt;p&gt;The aim of this blog post is to get you all setup with a Worker, then write some code to allow us to rewrite the CSS rules on the BBC News homepage so that all the fonts render using &lt;a href=&quot;https://en.wikipedia.org/wiki/Comic_Sans&quot;&gt;&#39;Comic Sans&#39;&lt;/a&gt;. A very silly objective but one that should teach you the basics.&lt;/p&gt;&lt;p&gt;The famous quote from Ian Malcome (Jeff Goldblum) in Jurassic Park feels very relevant right now:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Your scientists were so preoccupied with whether they could, they didn&#39;t stop to think if they should.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So let us proceed... If you already have a Cloudflare account you can skip step 1.&lt;/p&gt;&lt;h2 id=&quot;step-1-sign-up&quot;&gt;Step 1 - Sign up&lt;/h2&gt;&lt;p&gt;This is dead simple, visit &lt;a href=&quot;https://dash.cloudflare.com/sign-up/workers&quot;&gt;this URL&lt;/a&gt; and create yourself an account.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the login page for Cloudflare Workers.&quot; decoding=&quot;async&quot; height=&quot;784&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/0gbV450DpZ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/0gbV450DpZ-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/0gbV450DpZ-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/0gbV450DpZ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;step-1a-choose-a-subdomain&quot;&gt;Step 1a - Choose a subdomain&lt;/h3&gt;&lt;p&gt;Your worker will require a unique subdomain. You will then create your own workers under this unique subdomain. For example in the instance below a Worker named &quot;my-worker&quot; would be deployed to: &lt;code&gt;https://my-worker.nooshu-test-worker.workers.dev&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Choosing a subdomain to deploy your workers.&quot; decoding=&quot;async&quot; height=&quot;691&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/B0TLMtTqgE-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/B0TLMtTqgE-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/B0TLMtTqgE-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/B0TLMtTqgE-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;step-1b-choose-a-plan&quot;&gt;Step 1b - Choose a plan&lt;/h3&gt;&lt;p&gt;Next up is time to choose your plan. As you are just starting out you could use the free plan for testing. But for only $5 per month you get a whole bunch of benefits. This post isn&#39;t sponsored by Cloudflare, I promise!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Choose your Worker plan, either free or bundled for $5 per month.&quot; decoding=&quot;async&quot; height=&quot;885&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/9fZjPdLQVk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/9fZjPdLQVk-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/9fZjPdLQVk-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/9fZjPdLQVk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;step-1c-verify-your-email&quot;&gt;Step 1c - Verify your email&lt;/h3&gt;&lt;p&gt;Final part of signing up, check your email to verify your new account!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Verify your email account to complete signup.&quot; decoding=&quot;async&quot; height=&quot;576&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/2drESIOoIT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/2drESIOoIT-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/2drESIOoIT-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/2drESIOoIT-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I then had to complete a Google reCAPTCHA after clicking the email link to identify all the boats in the image before being redirected to the login page for the final step in verification:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Complete the final verification by logging in.&quot; decoding=&quot;async&quot; height=&quot;676&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/TwkX5M6CTj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/TwkX5M6CTj-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/TwkX5M6CTj-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/TwkX5M6CTj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;step-2-create-your-worker&quot;&gt;Step 2 - Create your Worker&lt;/h2&gt;&lt;p&gt;After login you will be presented with the screen below. Here we are given 2 routes. Either we create a new Worker using by clicking the big blue &#39;Create a Worker&#39; button to the right of the page, or we can use the &lt;code&gt;wrangler&lt;/code&gt; CLI tool used to wrangle your workers. I&#39;ll show both routes below. &lt;strong&gt;Note&lt;/strong&gt;: you only need to use one route, so choose your preferred method.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;On the screen we get to create our first worker.&quot; decoding=&quot;async&quot; height=&quot;607&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/qTo-vhdG7N-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/qTo-vhdG7N-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/qTo-vhdG7N-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/qTo-vhdG7N-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;step-2a-using-the-user-interface-ui&quot;&gt;Step 2a - Using the user interface (UI)&lt;/h3&gt;&lt;p&gt;This is the quickest method: setup a new Worker is via the UI as it does it all for you. Once clicking on the &#39;Create a Worker&#39; you will be presented with this screen. I&#39;ve annotated it to highlight key areas:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Annotated screen for the developer tools for the Worker.&quot; decoding=&quot;async&quot; height=&quot;538&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/sRMfevGq1G-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/sRMfevGq1G-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/sRMfevGq1G-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/sRMfevGq1G-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The screen seen above gives you everything you need to develop your first Worker. The name of the worker has been automatically generated by the system, but it can be renamed as needed. The Worker script goes in the left panel. It comes pre-populated with a JavaScript hello world application. You can send a request through the Worker and preview the response in the right panel. A preview of the actual webpage can be seen in the &#39;Preview&#39; tab. At the bottom right of the page you may notice a section that looks very familiar. It&#39;s a cut-down version of the Chrome DevTools panel. It allows you to view &lt;code&gt;console&lt;/code&gt; messages, network requests through the worker and also a sources panel for any local sources (&lt;code&gt;worker.js&lt;/code&gt; is seen by default). Saving and deploying the above hello world app makes it viewable at &lt;code&gt;https://summer-bread-11b8.nooshu-test-worker.workers.dev&lt;/code&gt; (this link isn&#39;t live).&lt;/p&gt;&lt;p&gt;The above method works well, but it&#39;s a little restrictive. I&#39;m sure ideally you are going to want to use your own code editor on your machine for most Worker development. This is where &lt;code&gt;wrangler&lt;/code&gt; comes in.&lt;/p&gt;&lt;h3 id=&quot;step-2b-using-wrangler&quot;&gt;Step 2b - Using &lt;code&gt;wrangler&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;Wrangler is the &lt;a href=&quot;https://github.com/cloudflare/wrangler&quot;&gt;open source&lt;/a&gt; CLI tool you can use to manage and deploy your Workers from your local machine. It comes with great documentation on how to set it up, but I&#39;ll cover it here too for completeness. First, I&#39;d recommend creating a new directory for your Worker code.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Install&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;You can use npm to install &lt;code&gt;wrangler&lt;/code&gt; by using the following command:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;npm install -g @cloudflare/wrangler&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Authenticate with your Cloudflare account&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Running the following command will start the authentication process:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;wrangler config&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will then ask you to enter an API Token, I go through this process below.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Create an API key process&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Via the CLI Quick Start you will find a &#39;Get your API token&#39; button, or you can go to it directly via &lt;a href=&quot;https://dash.cloudflare.com/profile/api-tokens&quot;&gt;this link&lt;/a&gt;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;On the CLI Quick start there is a blue &#39;Get your API token&#39; button.&quot; decoding=&quot;async&quot; height=&quot;550&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/4ql9Ht7XpQ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/4ql9Ht7XpQ-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/4ql9Ht7XpQ-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;This then brings you to a screen to start the creation process. Click the &#39;Create Token&#39; button:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the page where we can start the creation of the token.&quot; decoding=&quot;async&quot; height=&quot;501&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/LCYdlwDx_2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/LCYdlwDx_2-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/LCYdlwDx_2-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/LCYdlwDx_2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;We then get the option to create a Token from a set of templates. Click the &#39;Use template&#39; button on the &#39;Edit Cloudflare Workers&#39; row:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;We get the option to create a token from a template.&quot; decoding=&quot;async&quot; height=&quot;555&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/lpAO2WfkL4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/lpAO2WfkL4-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/lpAO2WfkL4-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;The &#39;Edit Cloudflare Workers&#39; template will show. Most of it can be left with the defaults. Just populate the dropdowns highlighted in red and click the &#39;Continue to summary&#39; button:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The template page can be left as default, apart from the zone resources and account resources dropdowns which need to be populated.&quot; decoding=&quot;async&quot; height=&quot;983&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/lcPQVpYLUs-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/lcPQVpYLUs-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/lcPQVpYLUs-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/lcPQVpYLUs-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Last step in the process is to review the summary page and click the &#39;Create Token&#39; button:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Review the summary page and click Create Token.&quot; decoding=&quot;async&quot; height=&quot;268&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZHPPIz2Dgn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZHPPIz2Dgn-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZHPPIz2Dgn-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZHPPIz2Dgn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The final page is the one that gives you the actual token you need for authorisation with Cloudflare. Make sure to copy this down as it is only displayed once for security reasons. If you don&#39;t you will need to repeat the whole process again!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Page with the created token which you will need to copy to clipboard for authorisation.&quot; decoding=&quot;async&quot; height=&quot;363&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/3sV-dmpHjU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/3sV-dmpHjU-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/3sV-dmpHjU-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/3sV-dmpHjU-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;When prompted in your terminal, you should paste in the API token you&#39;ve just created above. &lt;code&gt;wrangler&lt;/code&gt; will then run through the configuration process. Once complete you are now authenticated to make changes to your worker file locally.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Start a project from an existing template&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The quickest way to get up and running is to use a pre built template. You can do this by running the following command:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;wrangler generate my-project https://github.com/cloudflare/worker-template
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will create a &lt;code&gt;my-project&lt;/code&gt; directory with the required worker files you need. &lt;code&gt;index.js&lt;/code&gt; is where your Worker code exists. The &lt;code&gt;wrangler.toml&lt;/code&gt; file is the configuration for this Worker. You should edit this file and add in your &lt;code&gt;account_id&lt;/code&gt;. This can be found on the Workers overview page:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;On the worker overview page you will find the account ID you need for the wrangler.toml file.&quot; decoding=&quot;async&quot; height=&quot;450&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RTn6RNHWa5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RTn6RNHWa5-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RTn6RNHWa5-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RTn6RNHWa5-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once done, save the edited &lt;code&gt;wrangler.toml&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Preview the worker&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Last step is to preview your new worker. You can do this by running the command below (make sure you are in the Worker directory we created with above):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;wrangler preview
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will output a URL to the command line and automatically open your default browser with your new Worker visible:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Very simple sample worker has now been created&quot; decoding=&quot;async&quot; height=&quot;466&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/UwNpL2_KYq-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/UwNpL2_KYq-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/UwNpL2_KYq-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/UwNpL2_KYq-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now admittedly this isn&#39;t the most exciting example, but we now have a solid base to work from. When it comes to editing and redeploying this code simply edit the &lt;code&gt;index.js&lt;/code&gt; file with your changes then run:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;wrangler publish
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This command will push your changes up to the Worker that you can then view on the Worker subdomain you chose earlier. For this example it was &lt;code&gt;https://my-project.nooshu-test-worker.workers.dev&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;That now completes the setup of the Worker, so we can finally move onto the more interesting part: Making it actually do something useful!&lt;/p&gt;&lt;h2 id=&quot;step-3-write-some-code&quot;&gt;Step 3 - Write some code&lt;/h2&gt;&lt;p&gt;So my basis for the Worker code is the boilerplate code I listed in my &lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/&quot;&gt;&#39;Cloudflare Worker recipes for frontend performance testing&#39;&lt;/a&gt;, which you can &lt;a href=&quot;https://gist.github.com/Nooshu/8b018e39ed6b35c4640e88f5eee71d91&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;find here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;. We are going to take this code and use it as a template to modify the BBC News homepage.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What do you want to change?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;So the next decision for you to make is what exactly do you want to change on the site you are testing? I&#39;ve compiled some common web performance tasks in the &lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/&quot;&gt;recipes blog post&lt;/a&gt;, but it&#39;s completely up to you as to what you want to change. The question to ask yourself is: &quot;What changes can I make to this page to improve performance?&quot;. Now the experiments themselves may actually make no difference at all, or even make performance worse! But at least you&#39;ve found that out early in the process, allowing you to iterate quickly and try again.&lt;/p&gt;&lt;p&gt;So for this silly example I&#39;m simply going to change the BBC News font from the standard font to:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Comic Sans MS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Comic Sans&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cursive&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s a completely pointless example but it illustrates how you can use a Worker to manipulate a page.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Working locally in the browser&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;So before diving into the Worker code you may wish to experiment locally first to work out exactly what changes you would like to make on the page, and persist them across page reloads. For this I&#39;d recommend the incredibly useful Chrome feature called &lt;a href=&quot;https://developers.google.com/web/updates/2018/01/devtools#overrides&quot;&gt;&#39;Local Overrides&#39;&lt;/a&gt;. Local overrides allow you to make modifications to a page asset on your local machine, that will then persist across page reloads. So the browser will choose to use your local copy, rather than the network version from the website.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Working in the &#39;cloud&#39;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Another option you have is to work within the Cloudflare Worker UI I described in &lt;a href=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/#step-2a---using-the-user-interface-ui&quot;&gt;Step 2a - Using the user interface (UI)&lt;/a&gt;. The advantage with this method is you get instant feedback. You can tweak the code on the left hand side and instantly see the result reflected in the response and/or preview window. In the image below you can see that I have:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Made a request for the exact CSS file that I want to change &lt;code&gt;https://m.files.bbci.co.uk/modules/bbc-morph-news-page-styles/2.4.1/enhanced.css&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Set the &lt;code&gt;x-host: www.bbc.co.uk&lt;/code&gt; header.&lt;/li&gt;&lt;li&gt;Set the &lt;code&gt;Accept: text/css,*/*;q=0.1&lt;/code&gt; header (since a direct request to the resource doesn&#39;t set it automatically).&lt;/li&gt;&lt;li&gt;Modified the body &lt;code&gt;font-family&lt;/code&gt; from &lt;code&gt;Helvetica,Arial,freesans,sans-serif&lt;/code&gt; to &lt;code&gt;&#39;&quot;Comic Sans MS&quot;, &quot;Comic Sans&quot;, cursive;&#39;&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;Cloudflare UI with the example code and modified CSS.&quot; decoding=&quot;async&quot; height=&quot;371&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/1mCEfT_qXN-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/1mCEfT_qXN-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/1mCEfT_qXN-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/1mCEfT_qXN-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;We can see the modified response body in the built in DevTools console window, so we know that the changes we&#39;re expecting are reflected in the resulting response. These changes once deployed to the Worker will modify all CSS files that pass through it., but there&#39;s a recipe where I restrict that to a single CSS file if needed.&lt;/p&gt;&lt;p&gt;One annoying downside to working in the UI is it&#39;s quite restrictive in terms of space for editing. So if you want to use your local code editor the complete code from the above image can be found in &lt;a href=&quot;https://gist.github.com/Nooshu/d45a48e8f5aa6ed758889c7953fee286&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;. If you prefer a version without the comments, &lt;a href=&quot;https://gist.github.com/Nooshu/a546aaba23b6f64eb4812d39387c87c1&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;see here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Working in your text editor&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The last option, and the one I find most convenient is working from my standard code editor, since this fits in with my usual workflow. Once set up, it is dead simple to make changes. Simply edit the &lt;code&gt;index.js&lt;/code&gt; file in the worker directory we created earlier with the changes you want, then publish the changes to Cloudflare using &lt;code&gt;wrangler publish&lt;/code&gt;. These changes will be deployed instantly and also automatically reflected in the Worker UI we used earlier.&lt;/p&gt;&lt;h2 id=&quot;step-4-web-performance-testing&quot;&gt;Step 4 - Web performance testing&lt;/h2&gt;&lt;p&gt;So what&#39;s the point in making web performance changes if you can&#39;t measure the difference between results? This is where WebPageTest comes into play. Once configured we can easily test our website performance before and after the modifications and compare the results to see if we have improved performance (or made it worse!)&lt;/p&gt;&lt;p&gt;For this we are going to use some basic &lt;a href=&quot;https://docs.webpagetest.org/scripting/&quot;&gt;WebPageTest scripting&lt;/a&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;overrideHost	www.bbc.co.uk	my-project.nooshu-test-worker.workers.dev
overrideHost	m.files.bbci.co.uk	my-project.nooshu-test-worker.workers.dev
navigate	https://www.bbc.co.uk/news
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img alt=&quot;Basic setup of how to run a scripted test in WebPageTest.&quot; decoding=&quot;async&quot; height=&quot;490&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/prA05TA9ap-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/prA05TA9ap-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/prA05TA9ap-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/prA05TA9ap-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;What the code is doing above is redirecting requests the WebPageTest browser is making to specific domains through the Worker so they can be modified as required. WebPageTest will automatically add a different &lt;code&gt;x-host&lt;/code&gt; request header to the requests coming from each domain when it does this.&lt;/p&gt;&lt;p&gt;Simply edit the above script as needed and then run a test. If you are unsure how to run a test, I have a &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/&quot;&gt;blog post here&lt;/a&gt; all about it.&lt;/p&gt;&lt;h3 id=&quot;note-about-the-x-bypass-transform-header&quot;&gt;Note about the &lt;code&gt;x-bypass-transform&lt;/code&gt; header&lt;/h3&gt;&lt;p&gt;So in many of the recipes written with WPT and CFW&#39;s there&#39;s sometimes a custom header (&lt;code&gt;x-bypass-transform&lt;/code&gt;) you can add to bypass any page transformations. Using this allows for a more accurate web performance measurement since you are running the &#39;baseline&#39; page (as in no changes), as well as your modifications through your Worker. This makes them more directly comparable. Unfortunately in the example detailed above CORS reared it&#39;s ugly head and broke all the font loading when &lt;em&gt;any&lt;/em&gt; custom headers were added to the test using the &lt;code&gt;addHeader&lt;/code&gt; WebPageTest script. So I stripped out the bypass code from the example gist for this particular test.&lt;/p&gt;&lt;p&gt;What I believe is happening is the custom header is being added to the font request like so: &lt;code&gt;access-control-request-headers: x-custom-header-123&lt;/code&gt;. This is triggering a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request&quot;&gt;preflight request&lt;/a&gt; (&lt;code&gt;:method: OPTIONS&lt;/code&gt;) to check to see if this header is allowed. The server responds back with &lt;code&gt;access-control-allow-headers: *&lt;/code&gt; and then the font fails with a 403. This makes absolutely no sense to me at all. I assume the preflight is failing authorisation for reasons... then causing the 403. So yes, I still hate CORS! If anyone has an explanation as to why adding &lt;em&gt;any&lt;/em&gt; header to a request ends up killing the web fonts responses, please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;h2 id=&quot;step-5-result&quot;&gt;Step 5 - Result&lt;/h2&gt;&lt;p&gt;That&#39;s it, you&#39;re done. If all has gone to plan the results of your modifications should be visible within the WebPageTest filmstrip. The requests and responses have been routed through our Worker, with the response being modified on the fly before being received by the WebPageTest browser agent:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The route BBC domain and the CSS subdomain being routed through our worker.&quot; decoding=&quot;async&quot; height=&quot;109&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/e1yEZjKUAi-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/e1yEZjKUAi-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/e1yEZjKUAi-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/e1yEZjKUAi-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The results of my silly change can be seen below:&lt;/p&gt;&lt;h3 id=&quot;before&quot;&gt;Before&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Image of the BBC News Homepage before my CFW change. STandard fonts are shown.&quot; decoding=&quot;async&quot; height=&quot;553&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZrHIEh9-t3-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZrHIEh9-t3-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZrHIEh9-t3-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/ZrHIEh9-t3-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;after&quot;&gt;After&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Image of the BBC News Homepage after my CFW change. The body font has been changed to Comic Sans.&quot; decoding=&quot;async&quot; height=&quot;553&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RY3XIhMWZs-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RY3XIhMWZs-300.webp 300w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RY3XIhMWZs-600.webp 600w, https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/RY3XIhMWZs-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The before image shows the standard page being run through our Worker with no changes made. In the after image I&#39;ve swapped out the font for &lt;code&gt;&#39;Comic Sans&#39;&lt;/code&gt;. It&#39;s a simple visual example that has no performance effect on the page as all the fonts are still loaded anyway, but using the same methods listed above you can &lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/&quot;&gt;make many different changes&lt;/a&gt; to a webpage and test the difference it makes to the page performance. And as a bonus no production code was harmed during this experiment!&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;We&#39;ve covered a lot in this post. Setting up a Cloudflare account all the way through to writing Worker code to modify a CSS file on the fly. Hopefully this post will have introduced you to the basics of using Cloudflare Workers for web performance testing. If you have any feedback, or recipes of your own please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;! P.S. this worker used in this blog post has now been deleted, so &lt;code&gt;my-project.nooshu-test-worker.workers.dev&lt;/code&gt; will give you an error if you try it.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;14/03/21: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Cloudflare Worker recipes for frontend performance testing</title>
    <link href="https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/" />
    <updated>2021-03-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Table of contents&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#what-are-cfws&quot;&gt;What are CFW&#39;s?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#why-only-frontend-web-performance&quot;&gt;Why only frontend web performance?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#practical-use-case&quot;&gt;Practical use case&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#boilerplate-code&quot;&gt;Boilerplate code&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#code-quality&quot;&gt;Code quality&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#recipes&quot;&gt;Recipes&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#checking-all-requests-flow-through-the-worker&quot;&gt;Checking all requests flow through the worker&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#adding-resource-hints-to-the-page-head&quot;&gt;Adding resource hints to the page &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#adding-resource-hints-using-the-link-header&quot;&gt;Adding resource hints using the link header&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#removing-resource-hints-from-the-page-head&quot;&gt;Removing resource hints from the page &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#removing-resource-hints-headers&quot;&gt;Removing resource hints headers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#modifying-css-and-javascript-response-bodies&quot;&gt;Modifying CSS and JavaScript response bodies&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#removing-elements&quot;&gt;Removing elements&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#clearing-and-adding-inline-scripts&quot;&gt;Clearing and adding inline scripts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#adding-inline-css&quot;&gt;Adding inline CSS&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#adding-attributes&quot;&gt;Adding attributes&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#quickly-adding-scripts-to-the-head-and-closing-body-tags&quot;&gt;Quickly adding scripts to the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; and closing &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt; tags&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#testing-performance&quot;&gt;Testing performance&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#in-the-browser&quot;&gt;In the browser&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#using-webpagetest&quot;&gt;Using WebPageTest&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#summary&quot;&gt;Summary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/#further-reading&quot;&gt;Further Reading&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers (CFW)&lt;/a&gt; (or Workers) have very much become part of my workflow when it comes to examining and ultimately improving frontend web performance (hopefully!). I use them both for work and personal projects. This post I&#39;ve mainly written for myself, as I&#39;ve found I&#39;ve been repeating certain recipes over and over again. So it feels like a good idea to have a place to log them for reference in the future. I&#39;d love to get feedback on what could be improved, or any other examples you may have. If you do, please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Let me know&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you are unsure where to start with these recipes I&#39;ve written a &lt;a href=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/&quot;&gt;step by step guide on how to get started with Cloudflare Workers here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;what-are-cfw-s&quot;&gt;What are CFW&#39;s?&lt;/h2&gt;&lt;p&gt;Workers are a serverless offering from Cloudflare built upon their Content Delivery Network (CDN). They provide a lightweight JavaScript execution environment built on the &lt;a href=&quot;https://v8.dev/&quot;&gt;v8 JavaScript engine&lt;/a&gt;. There are many uses for Workers. Many examples are listed &lt;a href=&quot;https://developers.cloudflare.com/workers/tutorials&quot;&gt;on the Worker documentation page&lt;/a&gt;, including &lt;a href=&quot;https://developers.cloudflare.com/workers/tutorials/build-a-slackbot&quot;&gt;How to build a Slackbot&lt;/a&gt;, and &lt;a href=&quot;https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator&quot;&gt;How to build a QR code generator&lt;/a&gt;. But I&#39;m going to focus on a particular area that I find them useful for: frontend web performance.&lt;/p&gt;&lt;h2 id=&quot;why-only-frontend-web-performance&quot;&gt;Why only frontend web performance?&lt;/h2&gt;&lt;p&gt;Back in February 2012, &lt;a href=&quot;https://twitter.com/souders&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Steve Souders&lt;/a&gt; published his now pretty famous blog post &lt;a href=&quot;https://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/&quot;&gt;&#39;the Performance Golden Rule&#39;&lt;/a&gt;. To quote from the post:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;80-90% of the end-user response time is spent on the frontend. [So] Start there.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I highly recommend reading the post, but a quick TL;DR; is essentially: once the server has received a request, done all the database lookups, generated the HTML, and delivered the response to the browser, the rest of the page performance now depends on how it is handled on the frontend. So if you want to make large web performance gains, then focusing on the frontend is a way to do it. I&#39;m sure there are backend optimisations you can explore with Workers, but I won&#39;t be focussing on them in this blog post (although I&#39;d love to hear all about them).&lt;/p&gt;&lt;h2 id=&quot;practical-use-case&quot;&gt;Practical use case&lt;/h2&gt;&lt;p&gt;To give you a practical example of where Workers come in useful I&#39;ll look at some of the work I do at &lt;a href=&quot;https://www.gov.uk/government/organisations/government-digital-service&quot;&gt;Government Digital Service (GDS)&lt;/a&gt;. The department has built and maintains &lt;a href=&quot;https://www.gov.uk/&quot;&gt;GOV.UK&lt;/a&gt;. It is the website for the UK government. It&#39;s the best place to find policy, announcements, information about the government, and guidance for citizens. Since 2012 it has replaced 1,884 government websites with just one, to become the home of all central government&#39;s online content and services.&lt;/p&gt;&lt;p&gt;What I&#39;m trying to tell you is that it&#39;s a big and complicated bit of software, both on the backend and frontend. The frontend that users see is made up of approximately 13 separate applications, and that&#39;s not including any of the publishing applications. So what would be seen as a &quot;small change&quot; for a small static site, can actually turn out to be a huge change for a large complex site of its size. This is an issue pretty much every organisation and developer will run up against at some point. I&#39;m sure many readers have been in the position where they&#39;ve said: &quot;If &lt;em&gt;only&lt;/em&gt; we could change this small piece of code, and examine the difference it makes&quot;, knowing full well that the change is potentially day&#39;s worth of work (if not more).&lt;/p&gt;&lt;p&gt;What we need is a way to experiment with changing production code, without &lt;em&gt;actually&lt;/em&gt; making any changes to production code. It doesn&#39;t sound possible does it? But that&#39;s exactly what we can do with Workers. The Worker can sit between the production server and our browser and make changes to server responses on the fly. We can then observe the difference these response changes make to performance. Here&#39;s a tweet about &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1364005363381637122&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;an example I tried with GOV.UK&lt;/a&gt;, examining what difference the position of the JavaScript in the HTML source and the loading mechanism used (e.g. &lt;code&gt;async&lt;/code&gt; / &lt;code&gt;defer&lt;/code&gt;) has on performance. As I mention in the tweet thread, this set of changes would have taken much longer than the hour it took to create the experiments!&lt;/p&gt;&lt;h2 id=&quot;boilerplate-code&quot;&gt;Boilerplate code&lt;/h2&gt;&lt;p&gt;This code is all based off what &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; has written in his excellent blog post all about the subject: &lt;a href=&quot;https://andydavies.me/blog/2020/09/22/exploring-site-speed-optimisations-with-webpagetest-and-cloudflare-workers/&quot;&gt;&#39;Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers&#39;&lt;/a&gt;, which itself was based off the work &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Patrick Meenan&lt;/a&gt; mentions in his &lt;a href=&quot;https://www.slideshare.net/patrickmeenan/getting-the-most-out-of-webpagetest&quot;&gt;&#39;Getting the most out of WebPageTest&#39;&lt;/a&gt; talk (slide 60). So with this post I really am kneeling on the shoulders of giants!&lt;/p&gt;&lt;p&gt;The heavily commented code that all these recipes are based off can be &lt;a href=&quot;https://gist.github.com/Nooshu/8b018e39ed6b35c4640e88f5eee71d91&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;code-quality&quot;&gt;Code quality&lt;/h3&gt;&lt;p&gt;At this point it&#39;s probably worth mentioning that the code used in the examples is pretty rough and ready &quot;throwaway-code&quot;. It&#39;s simply there to transform certain server responses, rather than being production ready code. That being said, you should always try to &lt;a href=&quot;https://dash.cloudflare.com/login&quot;&gt;login to the Cloudflare admin area&lt;/a&gt; and observe the CPU time that your worker is taking during execution. The free worker plan allows for up to 10ms CPU time per request. If you are going over these 10ms I&#39;d imagine throttling will kick in. This is also a sure sign there are potential inefficiencies in the code which may lead to unrealistic final results (due to increased latency).&lt;/p&gt;&lt;p&gt;For example, I was recently looking to see what a really large HTML page can have on the CPU time in a Worker. I picked the &lt;a href=&quot;https://apod.nasa.gov/apod/archivepixFull.html&quot;&gt;NASA Astronomy Picture of the Day full Archive&lt;/a&gt; which returns 710 KB of uncompressed HTML. For certain DOM manipulation operations I managed to get it to spike to 35ms. And really it&#39;s not a complicated DOM at all, it&#39;s just large:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the graph we see the spike in CPU time hitting 35ms for a single request.&quot; decoding=&quot;async&quot; height=&quot;438&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/Xk80XcadwF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/Xk80XcadwF-300.webp 300w, https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/Xk80XcadwF-600.webp 600w, https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/Xk80XcadwF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So it&#39;s worth checking the CPU graph if you notice latency issues during testing.&lt;/p&gt;&lt;h2 id=&quot;recipes&quot;&gt;Recipes&lt;/h2&gt;&lt;p&gt;Below you will find a set of recipes I&#39;ve found useful when performance testing.&lt;/p&gt;&lt;h3 id=&quot;checking-all-requests-flow-through-the-worker&quot;&gt;Checking all requests flow through the worker&lt;/h3&gt;&lt;p&gt;The first thing that&#39;s worth checking is that all the expected responses for your test site are actually flowing through your worker. There are cases when this may not be the case. For example, if some of your requests use relative URLs, and others use absolute URLs. Your absolute URLs will likely be pointing to the original site. This could lead to less accurate results since your browser will be creating a TCP connection to both the worker URL and the original server. Thankfully this pretty simple to fix using the &lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/html-rewriter&quot;&gt;HTMLRewriter API&lt;/a&gt; and the &lt;a href=&quot;https://developers.cloudflare.com/workers/examples/rewrite-links&quot;&gt;&#39;Rewrite links&#39;&lt;/a&gt; example.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// domain rewriter vars&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;OLD_URL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://www.example.com/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;NEW_URL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular HTML response for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// rewrite the links from the following page elements&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AttributeRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;href&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AttributeRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;href&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;img&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AttributeRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;script&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AttributeRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;image&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AttributeRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;meta&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AttributeRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// transform the old response&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// return the modified page&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// https://developers.cloudflare.com/workers/examples/rewrite-links&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AttributeRewriter&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;attributeName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;attributeName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; attributeName
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; attribute &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;attributeName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;attribute&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;attributeName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                attribute&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;OLD_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;NEW_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the above example we are picking out selected elements in the page and making sure they all use relative URLs. Note: you should only do this for assets that actually exist on your sites server defined in the &lt;code&gt;site&lt;/code&gt; variable, else you&#39;re just going to get a lot of 404&#39;s!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/3de82f5aa8cb38e367e6ff5cf730d356&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;adding-resource-hints-to-the-page-head&quot;&gt;Adding resource hints to the page &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;In this example we&#39;re going to look at how we can modify our page &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; to add a few &lt;a href=&quot;https://www.w3.org/TR/resource-hints/&quot;&gt;resource hints&lt;/a&gt;, and give the browser information about the page before it is fully parsed. There are &lt;a href=&quot;https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/&quot;&gt;many articles available&lt;/a&gt; to read about how these tags can help improve the performance of a website.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; resourceHints &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
&amp;lt;link rel=&quot;preload&quot; href=&quot;/assets/font/font1.woff2&quot; as=&quot;font&quot; type=&quot;font/woff2&quot; crossorigin=&quot;anonymous&quot;&gt;
&amp;lt;link rel=&quot;dns-prefetch&quot; href=&quot;https://fonts.gstatic.com/&quot;&gt;
&amp;lt;link href=&quot;https://cdn.domain.com&quot; rel=&quot;preconnect&quot; crossorigin&gt;
&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular HTML response for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// add listed resource hints to the page head&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;head&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addResourceHints&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// transform the old response&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addResourceHints&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// notice how we are prepending the hints, right after the opening head tag&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// can be changed to append if you want them right before the closing tag&lt;/span&gt;
        element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;prepend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resourceHints&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Notice how we have different resource hints being added. In the example I&#39;ve added a &lt;code&gt;preload&lt;/code&gt;, &lt;code&gt;dns-prefetch&lt;/code&gt;, and a &lt;code&gt;preconnect&lt;/code&gt;. You can add as many or as few as you need. Also note how these tags are being added directly after the opening &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag. You can change this too if you so wish (e.g. using &lt;code&gt;.append()&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/aa0c994fdcaf4b4024798e4253a6bb0d&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;adding-resource-hints-using-the-link-header&quot;&gt;Adding resource hints using the link header&lt;/h3&gt;&lt;p&gt;So we&#39;ve added resource hints by modifying the HTML in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;, but it is also possible to &lt;a href=&quot;https://www.w3.org/TR/resource-hints/#fetching-the-resource-hint-link&quot;&gt;use a response header&lt;/a&gt; instead. This is quite simple to do with a Worker, but there are a couple of caveats:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Be careful of absolute / relative URLs with your preloaded assets. Make sure the URL in the &lt;code&gt;preload&lt;/code&gt; matches how the asset would usually load in the page. If they don&#39;t match you will end up preloading &lt;em&gt;and&lt;/em&gt; loading the asset twice (e.g. a font is preloaded and then also loaded through the usual page load).&lt;/li&gt;&lt;li&gt;If using &lt;code&gt;preload&lt;/code&gt; make sure to include the &#39;nopush&#39; string. Cloudflare converts a link preload to a &lt;a href=&quot;https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/&quot;&gt;HTTP/2 server push&lt;/a&gt; by default. This may (or may not) be the functionality you actually want.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In the below example I&#39;ve also allowed for both HTML changes as well as modification of the response headers.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/**
    * Insert any HTML modifications here
    */&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// apply above modifications to the response&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Make the headers mutable by re-constructing the already modified response.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// add our Link headers&lt;/span&gt;
  response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Link&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;/assets/fonts/font-file-1.woff2&gt;;rel=&quot;preload&quot;;as=&quot;font&quot;;crossorigin; nopush, &amp;lt;/assets/js/js-file-1.js&gt;;rel=&quot;preload&quot;;as=&quot;script&quot;; nopush&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/18de65027b3249e407dc97b3aaf7fe61&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;removing-resource-hints-from-the-page-head&quot;&gt;Removing resource hints from the page &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;I&#39;ve given you an example of how to add resource hints to the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; so I may as well give you an example of how to remove them too. Remember you can be very specific as to which ones you remove by making use of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors&quot;&gt;Attribute selectors&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// blanket example of removing all resource hints&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link[rel=&#39;preload&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link[rel=&#39;prefetch&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link[rel=&#39;dns-prefetch&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link[rel=&#39;prerender&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link[rel=&#39;preconnect&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// example were we only remove a selected preload hint for a font&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link[rel=&#39;preload&#39;][href*=&#39;our-woff2-font.woff2&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/6bf735bc9ab3d065658725b78d04fca8&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;removing-resource-hints-headers&quot;&gt;Removing resource hints headers&lt;/h3&gt;&lt;p&gt;So above we added the &lt;code&gt;Link&lt;/code&gt; header for our resource hints, but what about if our server is already serving them and you want to test what difference &lt;em&gt;removing&lt;/em&gt; them makes? Well it&#39;s pretty much identical to the above code only we use the &lt;code&gt;delete()&lt;/code&gt; method instead of &lt;code&gt;set()&lt;/code&gt;:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// remove all external scripts from the page&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/**
      * Make your HTML changes here
      */&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Make the headers mutable by re-constructing the already modified response.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// delete our Link header(s)&lt;/span&gt;
    response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Link&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/52fdb9c91607a5920c5c093668fe300e&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;modifying-css-and-javascript-response-bodies&quot;&gt;Modifying CSS and JavaScript response bodies&lt;/h3&gt;&lt;p&gt;The great thing about CFWs is you can easily make changes to responses for other types of responses too. For example, in your Worker you could look for CSS and JavaScript files and make changes to their response bodies. You could quite easily remove whole sections of JavaScript code, or add in brand new selectors to the CSS and observe the results in the page. In the simple example below we are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;replacing a common CSS string (the &lt;code&gt;font-family&lt;/code&gt; property value)&lt;/li&gt;&lt;li&gt;adding a simple &lt;code&gt;console.log()&lt;/code&gt; to the end of a JavaScript file&lt;/li&gt;&lt;/ul&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/**
      * Make your HTML changes here
      */&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;// Change CSS here&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// grab the CSS response&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// extract the body of the request&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// modify the CSS response body&lt;/span&gt;
    body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;Arial, Helvetica Neue, Helvetica, sans-serif;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;gi&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Georgia, Times, Times New Roman, serif;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;Arial,Helvetica Neue,Helvetica,sans-serif;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;gi&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Georgia, Times, Times New Roman, serif;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// return the modified response&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;*/*&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;// Change JavaScript here (uses the generic Accept directive)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// being granular we only modify a single response for a specific JavaScript file&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;our-specific-js-filename.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// grab the JS response&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// extract the JS body of the request&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// using template literals we add a console.log to the end&lt;/span&gt;
    body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;body&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; console.log(&#39;String added last&#39;);&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// return the modified response&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&#39;s a caveat to the above code. The &lt;code&gt;Accept&lt;/code&gt; header for scripts in &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values#values_for_scripts&quot;&gt;all modern browsers&lt;/a&gt; is &lt;code&gt;*/*&lt;/code&gt;, so I&#39;ve specifically targeted a single JavaScript file. You can easily do the same with the CSS using the same method.&lt;/p&gt;&lt;p&gt;Also, when you get to the point of &lt;code&gt;var body = await response.text();&lt;/code&gt;, what you do next basically comes down to a JavaScript string manipulation exercise. You could easily add / remove / insert code into the files using the basic &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#instance_methods&quot;&gt;string manipulation methods&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Be sure to check to see if your &lt;code&gt;&amp;lt;script&gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;link&gt;&lt;/code&gt; tags don&#39;t have an &lt;code&gt;integrity&lt;/code&gt; attribute. If they do you have &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity&quot;&gt;Subresource Integrity&lt;/a&gt; enabled on your JavaScript and CSS assets. If you modify these files on the fly like in the example above, the hash of the file will have changed and the browser will refuse to load it. If that&#39;s the case I&#39;d remove the &lt;code&gt;integrity&lt;/code&gt; attribute using the HTMLRewriter at the same time.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/b7110cdae45b4dfe4aaac3e2ab48e08e&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;removing-elements&quot;&gt;Removing elements&lt;/h3&gt;&lt;p&gt;This is an incredibly simple example, as the HTMLRewriter API does all the heavy lifting for us.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// remove a specific script&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;script[src*=&#39;name-of-our-script.js&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// remove the third meta tag in the head&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;head &gt; meta:nth-of-type(3)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// remove all div elements that start with &#39;prefix&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;div[class^=&#39;prefix&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// remove all link elements that start with &#39;/assets/&#39; and end with &#39;.css&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;link[href^=&#39;/assets/&#39;][href$=&#39;.css&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can be really creative with your &lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/html-rewriter#selectors&quot;&gt;CSS selectors&lt;/a&gt; if you need to be, after all the Worker is powered by Chrome&#39;s v8 engine, so it understands all the modern CSS selectors.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/f4e5890419b807555f2590a2676864c8&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;clearing-and-adding-inline-scripts&quot;&gt;Clearing and adding inline scripts&lt;/h3&gt;&lt;p&gt;The problem with inline scripts is that most of the time they are just a simple &lt;code&gt;&amp;lt;script&gt;&lt;/code&gt; tag. There are no attributes to use to select them, and their position in the page may not always be consistent in the DOM. So a method I&#39;ve found useful is to remove all inline scripts from a page then rebuild and add them back in where you need them. This gives you fine grain control over the HTML markup coming from the Worker:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// remove inline scripts from the page&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;body &gt; script:not([src])&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// reinsert some inline JavaScript back into the page&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;reinsertInlineScript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// transform the page&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;reinsertInlineScript&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; inlineScript &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;document.body.className = ((document.body.className) ? document.body.className + &#39; js-enabled&#39; : &#39;js-enabled&#39;);&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;prepend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;script&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;inlineScript&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;/script&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Using this method you could completely rebuild where inline scripts sit in the page source, and experiment with what they contain (since they are blocking after all).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/b9d6a0c14946197d38dacf238a3b1bf0&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;adding-inline-css&quot;&gt;Adding inline CSS&lt;/h3&gt;&lt;p&gt;Using pretty much the same method as above you can add inline CSS into the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; of your page. In doing so we are adding bytes to the browsers critical path, but it can be useful for experimentation. One particular use case could be you are wanted to explore is what difference adding &lt;a href=&quot;https://www.smashingmagazine.com/2015/08/understanding-critical-css/&quot;&gt;critical CSS&lt;/a&gt; to a page does for rendering performance:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// add the inline CSS to the head&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;head&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addNewCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// transform the page&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addNewCSS&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newInlineCSS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
      body {
        border: 10px solid red;
      }
    &lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;style&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;newInlineCSS&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;/style&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/af80e8974f73903b1bbb62aed057ab2a&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;adding-attributes&quot;&gt;Adding attributes&lt;/h3&gt;&lt;p&gt;There may be times where you want to add certain attributes to elements. A great use case for this in web performance is adding &lt;code&gt;defer&lt;/code&gt; and &lt;code&gt;async&lt;/code&gt; attributes to &lt;code&gt;&amp;lt;script&gt;&lt;/code&gt; tags, and seeing how this changes the loading performance of a page. In the example below we create classes to add these attributes to scripts. These can be adapted to add any attributes you might want to add to an element:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// add defer to this script&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;script[src*=&#39;script-to-be-deferred.js&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addDeferAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// add async to this script&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;script[src*=&#39;script-to-be-asynced.js&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addAsyncAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// transform the page&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addDeferAttribute&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;defer&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;defer&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;addAsyncAttribute&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;async&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;async&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/7f66cbfaa832e765e2321212819132f9&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;quickly-adding-scripts-to-the-head-and-closing-body-tags&quot;&gt;Quickly adding scripts to the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; and closing &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt; tags&lt;/h3&gt;&lt;p&gt;The position of scripts in the page source has become less of an issue since modern browsers have started to support the &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;defer&lt;/code&gt; attributes, but there still may be cases where you want to position them very specifically in the source for experimentation. This is the method I used when I was investigating reducing the Cumulative Layout Shift (CLS) on GOV.UK &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1364005363381637122&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;in this tweet&lt;/a&gt;. I cleared all scripts from the page source and rebuilt it with the scripts repositioned as required:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acceptHeader &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; acceptHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// store this particular request for modification&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create a new response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HTMLRewriter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// remove all external scripts from the page&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;body &gt; script[src]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;removeElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// insert scripts back before the closing body tag&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;reinsertBodyScripts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// insert scripts back before the closing head tag&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;head&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;reinsertHeadScripts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldResponse&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// return the modified page along with custom headers&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;reinsertBodyScripts&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; srcArray &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&#39;/assets/js/body-script-1.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&#39;/assets/js/body-script-2.js&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

    srcArray&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;script src=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;val&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;&amp;lt;/script&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;reinsertHeadScripts&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; srcArray &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&#39;/assets/js/deferred-head-script-1.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&#39;/assets/js/deferred-head-script-2.js&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

    srcArray&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;script src=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;${&lt;/span&gt;val&lt;span class=&quot;token punctuation interpolation-punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot; defer&gt;&amp;lt;/script&gt;&lt;/span&gt;&lt;span class=&quot;token string template-punctuation&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There are many combinations you may want to experiment with related to &lt;code&gt;async&lt;/code&gt;, &lt;code&gt;defer&lt;/code&gt; and standard blocking scripts. I can&#39;t cover them all here. But hopefully this example gives you an idea of how you&#39;d do it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: Complete code for this example can be &lt;a href=&quot;https://gist.github.com/Nooshu/2de919f6eb8b69c13a3741700bf8742c&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;seen in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;testing-performance&quot;&gt;Testing performance&lt;/h2&gt;&lt;p&gt;So, once you&#39;ve made changes to your page you&#39;re going to want to check what difference they have made to the performance of the page. There are a couple of ways to do this:&lt;/p&gt;&lt;h3 id=&quot;in-the-browser&quot;&gt;In the browser&lt;/h3&gt;&lt;p&gt;The browser is a great way to check to see if the page is being modified in the way you expect (e.g. view the page source), so I&#39;d recommend setting up this method anyway. Using the &lt;a href=&quot;https://bewisse.com/modheader/&quot;&gt;ModHeader&lt;/a&gt; extension you can easily add request headers to your requests. In our examples above we&#39;d set &lt;code&gt;x-host&lt;/code&gt; to the site we want to change (e.g. &lt;code&gt;x-host: www.gov.uk&lt;/code&gt;), and &lt;code&gt;x-bypass-transform&lt;/code&gt; to either &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; depending on if we want to toggle the Worker changes on or off. You could then use &lt;a href=&quot;https://developers.google.com/web/tools/lighthouse&quot;&gt;Chrome Lighthouse&lt;/a&gt; and run a performance audit with the changes both on and off. Using a tool like &lt;a href=&quot;https://googlechrome.github.io/lighthouse-ci/viewer/&quot;&gt;Lighthouse CI Diff&lt;/a&gt; you could then compare the difference between the two sets of results.&lt;/p&gt;&lt;h3 id=&quot;using-webpagetest&quot;&gt;Using WebPageTest&lt;/h3&gt;&lt;p&gt;I use the browser method above for verification and debugging of the changes, but my preferred method of comparing results is using &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;. It gives you incredibly detailed performance information that you can compare across multiple experiments. The test setup uses the &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/main/src/scripting.md#overridehost&quot;&gt;overrideHost&lt;/a&gt; scripting functionality to reroute any requests to the original domain through the Worker for modification. An example script I use for testing with GOV.UK can be found below:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;setCookie https://www.gov.uk/ cookies_policy={&quot;essential&quot;:true,&quot;settings&quot;:true,&quot;usage&quot;:true,&quot;campaigns&quot;:true}
setCookie https://www.gov.uk/ cookies_preferences_set=true
setCookie https://www.gov.uk/ global_bar_seen={&quot;count&quot;:0,&quot;version&quot;:8}
addHeader x-bypass-transform:false
overrideHost www.gov.uk govuk-worker.nooshu.workers.dev
navigate https://www.gov.uk/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Enter the above script into the &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#script-tab&quot;&gt;&#39;Script&#39; tab&lt;/a&gt; of WebPageTest, &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#advanced-testing-tab&quot;&gt;configure all your test settings&lt;/a&gt; then run a test. Once completed you can use the &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-the-filmstrip-view-and-waterfall-chart-are-related&quot;&gt;compare view functionality&lt;/a&gt; to examine the difference between the different sets of results.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;In this post I&#39;ve covered what Cloudflare Workers are, and how they can be used for various frontend performance tweaks and testing. Moving onto how you can easily check to see you are seeing the changes you expect, and then performance difference they make.&lt;/p&gt;&lt;p&gt;If you&#39;ve watched &lt;a href=&quot;https://twitter.com/csswizardry&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Harry Roberts&lt;/a&gt;&#39; talk &lt;a href=&quot;https://www.youtube.com/watch?v=SVt7bjTwCMM&quot;&gt;&#39;From Milliseconds to Millions&#39;&lt;/a&gt; where he talks about the importance of a pages &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag for page performance, it&#39;s now possible to easily try this. Using a Cloudflare Worker you have a way of testing changes quickly and easily against your production environment, without having to touch a single line of production code! Imagine being able to sell in performance work internally (or externally) with actual metrics you could see if production code &lt;em&gt;is&lt;/em&gt; changed. Note: The results may not be 100% accurate compared to your production environment, but they are certainly a good way to give you an indication as to what is possible.&lt;/p&gt;&lt;p&gt;I personally think that&#39;s a pretty great tool to have in your web performance toolkit!&lt;/p&gt;&lt;p&gt;I&#39;d love to hear any feedback you have on the blog post above, or if there are any other recipes you use, please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;h2 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h2&gt;&lt;p&gt;There&#39;s been more written about using Cloudflare Workers for web performance optimisation here:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://andydavies.me/blog/2020/09/22/exploring-site-speed-optimisations-with-webpagetest-and-cloudflare-workers/&quot;&gt;Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://calendar.perfplanet.com/2019/prototyping-optimizations-with-cloudflare-workers-and-webpagetest/&quot;&gt;Prototyping optimizations with Cloudflare Workers and WebPageTest&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/dot_js&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andrew Galloni&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/pmeenan/cf-workers&quot;&gt;A collection of performance optimization scripts using Cloudflare Workers&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Patrick Meenan&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;02/03/21: Initial post published.&lt;/li&gt;&lt;li&gt;03/03/21: Added recipes for removing resource hints in the headers and HTML. Added table of contents.&lt;/li&gt;&lt;li&gt;14/03/21: Added link to my new blog post &lt;a href=&quot;https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/&quot;&gt;&#39;Setting up Cloudflare Workers for web performance optimisation and testing&#39;&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Testing priority hints with WebPageTest</title>
    <link href="https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/" />
    <updated>2021-02-20T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/</id>
    <content type="html">&lt;p&gt;Back in January 2019 the Chrome team published an &lt;a href=&quot;https://groups.google.com/a/chromium.org/g/blink-dev/c/jpeSdM897Xw&quot;&gt;Intent to Experiment: Priority Hints&lt;/a&gt; message to the blink developers Google group. This set out a plan to start an &lt;a href=&quot;https://web.dev/origin-trials/&quot;&gt;origin trial&lt;/a&gt; to allow developers to experiment with this new web platform feature and give feedback before eventual rollout across all origins.&lt;/p&gt;&lt;p&gt;Unfortunately Priority Hints are currently &lt;a href=&quot;https://www.chromestatus.com/feature/5273474901737472&quot;&gt;on hold&lt;/a&gt; as the Chrome team weren&#39;t able to to see statistically significant performance wins with the &lt;a href=&quot;https://wicg.github.io/priority-hints/&quot;&gt;current design&lt;/a&gt;. So does that mean you can&#39;t play with this experimental web platform feature now? No, not at all! Read on to find out more how you can still experiment with it using both your browser, and more interestingly, &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt; with &lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers&lt;/a&gt;!&lt;/p&gt;&lt;h2 id=&quot;what-are-priority-hints&quot;&gt;What are priority hints?&lt;/h2&gt;&lt;p&gt;Lets very briefly look at what priority hints are and their expected usage. So priority hints are a way for developers to tell a browser the relative priority of resources on a page. A developer can then increase or decrease the priority of an asset depending on how &lt;em&gt;they&lt;/em&gt; judge it&#39;s priority in the page load process. So for example:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- The main image is in the viewport,
so considered important by the browser, but we disagree --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/images/hero-image.png&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;importance&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;low&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Main hero image, but considered unimportant&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- we want the browser to know to fetch this script,
but don&#39;t prioritise over other important assets --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/js/required-later.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;importance&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;low&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;  &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;script&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- script set to async so it won&#39;t block the thread while JS is fetched
but we consider this script to be important so bump its priority for download --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/js/async-script.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;importance&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;high&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now of course you need to be careful when changing priorities. If you bump one asset&#39;s priority level, you are lowering the priority of another. Or if you bump all assets to &lt;code&gt;high&lt;/code&gt;, then really you have no prioritisation at all! There&#39;s a very important note to be made at this point. These are &lt;em&gt;hints&lt;/em&gt; to the browser. They aren&#39;t instructions. Prioritisation in the browser is very complex and it will use heuristics to determine how best to load a page. These hints are a developer&#39;s way to give it more information for consideration. But ultimately it is up to the browser if it actually decides to use the hint for network prioritisation. It may simply choose to ignore them and continue as is.&lt;/p&gt;&lt;p&gt;But one of the interesting use cases for priority hints is not boosting an assets priority with the &lt;code&gt;high&lt;/code&gt; value, it&#39;s actually lowering an assets priority using &lt;code&gt;low&lt;/code&gt;. By doing so you are giving other assets on a page network priority. So say for example you had an image at the top of your page that you know will be in the main viewport. Browser heuristics state that this will automatically be a high priority asset. But what if that image doesn&#39;t actually add value to a user&#39;s journey. It&#39;s a &#39;nice to have&#39; (eventually), but concentrate on the rest of the page before loading it. That&#39;s just a very simple example of where this functionality could be useful.&lt;/p&gt;&lt;p&gt;There&#39;s a really informative blog post: &lt;a href=&quot;https://developers.google.com/web/updates/2019/02/priority-hints&quot;&gt;&quot;Get Ready for Priority Hints&quot;&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/malchata&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Jeremy Wagner&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/yoavweiss&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Yoav Weiss&lt;/a&gt; which goes into priority hints in much more detail.&lt;/p&gt;&lt;h2 id=&quot;so-how-do-i-use-them&quot;&gt;So how do I use them?&lt;/h2&gt;&lt;p&gt;Okay so enough waffle already, how do you try them today? It&#39;s actually really simple, as they are sitting behind the &#39;Experimental Web Platform features&#39; in Chrome flags (chrome://flags/#enable-experimental-web-platform-features).&lt;/p&gt;&lt;h3 id=&quot;browser&quot;&gt;Browser&lt;/h3&gt;&lt;p&gt;In Chrome you can either enable the &#39;Experimental Web Platform features&#39; flag manually, or use the command line interface to fire up a Chrome window with it already enabled. On OSX you simply enter this command into the terminal:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/Applications/Google&#92; Chrome.app/Contents/MacOS/Google&#92; Chrome --enable-experimental-web-platform-features&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;webpagetest&quot;&gt;WebPageTest&lt;/h3&gt;&lt;p&gt;Doing this via WebPageTest is just as simple as above. Just make sure you are running a test using a Chromium browser and enter &lt;code&gt;--enable-experimental-web-platform-features&lt;/code&gt; into the &#39;command line custom options&#39; under the &#39;Chromium&#39; tab (see below):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;WebPageTest &#39;Chromium&#39; tab allows you to enter CLI commands to be passed through to the test, as displayed in the image.&quot; decoding=&quot;async&quot; height=&quot;596&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Z1b0LD3BFm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Z1b0LD3BFm-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Z1b0LD3BFm-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Z1b0LD3BFm-794.webp 794w&quot; width=&quot;794&quot;&gt;&lt;/p&gt;&lt;p&gt;The Chromium browser running the test will now have priority hints enabled (assuming it&#39;s based on a build after M73).&lt;/p&gt;&lt;h3 id=&quot;are-priority-hints-enabled&quot;&gt;Are priority hints enabled?&lt;/h3&gt;&lt;p&gt;So how do you know if priority hints are enabled? Well I&#39;ve created some &lt;a href=&quot;https://condescending-edison-a6f5b2.netlify.app/&quot;&gt;incredibly simple demo pages&lt;/a&gt; that will allow you to test to see if they are. The pages contain a selection of images with their priorities modified using the &lt;code&gt;importance&lt;/code&gt; attribute. You will quickly be able to spot if priority hints are enabled by looking at the network tab in DevTools and making sure the &#39;Priority&#39; tab is visible.&lt;/p&gt;&lt;p&gt;In the image below, priority hints aren&#39;t enabled. All images get a &lt;code&gt;high&lt;/code&gt; priority and the preloaded image gets a &lt;code&gt;low&lt;/code&gt; priority:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image shows the priorities that the Chrome browser heuristics have determined is best to load the page, all images display high.&quot; decoding=&quot;async&quot; height=&quot;794&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/cAhrbWfZL4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/cAhrbWfZL4-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/cAhrbWfZL4-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;Now lets see what happens when the experimental flag (and client hints) and enabled:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image shows the priorities that I have managed to update using the importance attribute.&quot; decoding=&quot;async&quot; height=&quot;807&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/gOF8hMfpKz-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/gOF8hMfpKz-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/gOF8hMfpKz-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;The above image looks very different. Each of the images has a priority set depending on the image name (mirrored with the &lt;code&gt;importance&lt;/code&gt; attribute) and the preload has been forced to be &lt;code&gt;high&lt;/code&gt; priority. Note that images with &lt;code&gt;importance=&quot;auto&quot;&lt;/code&gt; return the default priority determined by the browser heuristics. A very unscientific and simple demo, but it allows you to test to see if client hints have been enabled.&lt;/p&gt;&lt;h2 id=&quot;using-it-in-the-wild&quot;&gt;Using it in the wild&lt;/h2&gt;&lt;p&gt;So you may be asking: &quot;Why would I be adding the &lt;code&gt;importance&lt;/code&gt; attribute to my website when no browsers actually support them!&quot;. And for that I completely agree, that&#39;s not a good idea. But that doesn&#39;t mean we can&#39;t test in &quot;production environments&quot;. It&#39;s time to roll out our favorite new friend, &lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers (CFW)&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I&#39;ve blogged before about how I&#39;ve started to &lt;a href=&quot;https://nooshu.github.io/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/#cloudflare-workers&quot;&gt;use CFW&lt;/a&gt; to experiment with web performance changes. And since priority hints are easily applied using the &lt;code&gt;importance&lt;/code&gt; attribute they seem like the perfect candidate for the &lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/html-rewriter&quot;&gt;HTMLRewriter&lt;/a&gt; runtime API. But what&#39;s even better is we can easily combine a CFW with WebPageTest. By doing so, we can experiment with changing our production environment and measure the difference in performance the &lt;code&gt;importance&lt;/code&gt; attributes would make to page loading.&lt;/p&gt;&lt;h2 id=&quot;cloudflare-workers-setup&quot;&gt;Cloudflare Workers setup&lt;/h2&gt;&lt;p&gt;With this setup I&#39;m piggybacking off the code Andy Davies &lt;a href=&quot;https://andydavies.me/blog/2020/09/22/exploring-site-speed-optimisations-with-webpagetest-and-cloudflare-workers/&quot;&gt;lists on his blog post&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;preloading-print-css-files&quot;&gt;Preloading Print CSS files&lt;/h3&gt;&lt;p&gt;In this very simple example we take &#39;IDLE&#39; priority CSS files (print stylesheets) and bump them up the priority order by preloading them. It&#39;s then possible to tweak the priority of the preload using the &lt;code&gt;importance&lt;/code&gt; attribute. &lt;strong&gt;Note&lt;/strong&gt;: This is a completely contrived example to show the attributes in action. I wouldn&#39;t recommend doing this in practice!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Default waterfall&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Without preloading our print style sheets have an &#39;IDLE&#39; priority and are seen at requests 11 and 12:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In this waterfall we see the print styles in their usual place with an IDLE priority attached.&quot; decoding=&quot;async&quot; height=&quot;365&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/ISARPxEOcZ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/ISARPxEOcZ-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/ISARPxEOcZ-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/ISARPxEOcZ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Preloaded CSS&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Now let&#39;s use a Cloudflare Worker to inject a &lt;code&gt;&amp;lt;link rel=&quot;preload&quot;&gt;&lt;/code&gt; right after the opening &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In this waterfall we have preloaded the print CSS and it can be seen at requests 1 and 2.&quot; decoding=&quot;async&quot; height=&quot;233&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/tFvD68wtyq-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/tFvD68wtyq-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/tFvD68wtyq-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/tFvD68wtyq-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example what is seen is very predictable. Our print stylesheets have been bumped up the network request order, now sitting at request 2 and 3. Also the priority for these CSS files is now set to &lt;code&gt;HIGHEST&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Preloaded CSS with priority hints&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Now let&#39;s enable priority hints in Chrome and modify the preloads to add the &lt;code&gt;importance=&quot;low&quot;&lt;/code&gt; attribute:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the priority of the preload has been changed from HIGHEST to LOWEST.&quot; decoding=&quot;async&quot; height=&quot;233&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/joMvKCCmK4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/joMvKCCmK4-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/joMvKCCmK4-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/joMvKCCmK4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example above we still have the print stylesheets sitting at requests 2 &amp; 3, but the important difference is the priority of this preload has been set to &lt;code&gt;LOWEST&lt;/code&gt;. We have successfully given the browser a hint that this preload exists but isn&#39;t important. Now this is such a simple page and example that the change actually has no effect on the performance of the page (other than slowing it down slightly because we are preloading print CSS!). But it does show how easy it is to modify a page&#39;s HTML using a Cloudflare Worker and experiment with priority hints.&lt;/p&gt;&lt;p&gt;If you are interested in the code used to create this very simple example I&#39;ve dropped it all into a &lt;a href=&quot;https://gist.github.com/Nooshu/63366a308137c33c74ab39a68e0a7102&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;modifying-a-preloaded-javascript-bundle&quot;&gt;Modifying a preloaded JavaScript bundle&lt;/h3&gt;&lt;p&gt;In this example I look at the UK version of the &lt;a href=&quot;https://www.nike.com/gb/&quot;&gt;Nike website&lt;/a&gt;. Running the site through WebPageTest and looking at the source I noticed that they are preloading two JavaScript files in order to boost its priority. The site is built using React, and as a whole has a lot of JavaScript, coming in at around 1.2 MB of compressed. So using a Cloudflare Worker and WebPageTest let&#39;s investigate what happens when we alter the priority of these 2 preloads.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Vendor.js preload&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;There&#39;s a file with &lt;code&gt;vendor.js&lt;/code&gt; in the filename that comes in at 170 KB in size. The file looks to contain some helper libraries for classnames, Brightcove video, and a production version of React v16.12.0. &lt;strong&gt;Note&lt;/strong&gt;: If any developers from Nike are reading this, you look to be including another version of React later in the page load (v16.13.0), so that&#39;s a pretty good optimisation to look at right there!&lt;/p&gt;&lt;p&gt;By default this &lt;code&gt;vendor.js&lt;/code&gt; file is given a medium priority, with a weight of 220. &lt;strong&gt;Note:&lt;/strong&gt; Very roughly speaking, the higher the weight value, the higher the priority. With a standard &lt;code&gt;preload&lt;/code&gt; this file downloads at request number 5 in the waterfall, as seen below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the default waterfall for Nike, with the vendor JS with a priority weight of 220.&quot; decoding=&quot;async&quot; height=&quot;282&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/guvLdHIojy-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/guvLdHIojy-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/guvLdHIojy-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/guvLdHIojy-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;But as mentioned above with priority hints we have the ability to give the browser a &lt;em&gt;hint&lt;/em&gt; that this preload should have a lower priority. So using our Cloudflare Worker to modify the HTML lets see what happens when we add &lt;code&gt;importance=&quot;low&quot;&lt;/code&gt; to this single preload. The resulting waterfall can be seen below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here&#39;s our modified waterfall where the vendor preload has been dropped in priority, weight has dropped from 220 to 147.&quot; decoding=&quot;async&quot; height=&quot;444&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/XvXMu_H9hw-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/XvXMu_H9hw-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/XvXMu_H9hw-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/XvXMu_H9hw-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Priority weight drops from 220 to 147 and from request 5 to 17.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Client.js preload&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Next on the list is the preload containing &lt;code&gt;client.js&lt;/code&gt; in the filename. This file is 83 KB of compressed JavaScript and looks to be the core code for the Nike React app. By default this file gets a priority of medium, weighted at 220 and is seen at request number 6 in the waterfall:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the default waterfall for Nike, with the client JS with a priority weight of 220.&quot; decoding=&quot;async&quot; height=&quot;299&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/dD1Y6cLaEP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/dD1Y6cLaEP-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/dD1Y6cLaEP-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/dD1Y6cLaEP-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Again we use priority hints to add &lt;code&gt;importance=&quot;low&quot;&lt;/code&gt; and investigate what happens:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here&#39;s our modified waterfall where the client preload has been dropped in priority, weight has dropped from 220 to 147.&quot; decoding=&quot;async&quot; height=&quot;435&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Jd9Zr0vznc-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Jd9Zr0vznc-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Jd9Zr0vznc-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/Jd9Zr0vznc-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Priority weight drops from 220 to 147 and from request 6 to 16.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Both together&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;So we&#39;ve dropped the priority of each preload individually, so let&#39;s see what happens when we do it for both at the same time:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see both preloads drop in priority weight from 220 to 147. This results in them both dropping down the request order.&quot; decoding=&quot;async&quot; height=&quot;456&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/t_bcUj-Y2G-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/t_bcUj-Y2G-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/t_bcUj-Y2G-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/t_bcUj-Y2G-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;With both preloads having &lt;code&gt;importance=&quot;low&quot;&lt;/code&gt; added, we see the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;vendor.js&lt;/code&gt; weight drops from 220 to 147. Request number drops from 5 to 15.&lt;/li&gt;&lt;li&gt;&lt;code&gt;client.js&lt;/code&gt; weight drops from 220 to 147. Request number drops from 6 to 18.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;So enough of looking at random waterfalls. We can see that the priority hints are working as expected. But what really matters is the performance a user sees. We now have 4 tests we can compare using WebPageTest:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Baseline: No HTML modifications, priority hints enabled but not used. Cloudflare Worker used as a basic proxy.&lt;/li&gt;&lt;li&gt;Vendor.js file preload set to &lt;code&gt;low&lt;/code&gt;: HTML modification to a single preload, priority hints enabled and used.&lt;/li&gt;&lt;li&gt;Client.js file preload set to &lt;code&gt;low&lt;/code&gt;: HTML modification to a single preload, priority hints enabled and used.&lt;/li&gt;&lt;li&gt;Both preloads set to &lt;code&gt;low&lt;/code&gt;: HTML modification to a both preloads, priority hints enabled and used.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;WebPageTest outputs a very interesting filmstrip and set of graphs:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Filmstrip showing all 4 tests side by side. Here we see the test with both preloads set to low performing best (visually)&quot; decoding=&quot;async&quot; height=&quot;275&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/3kMWYLXUGP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/3kMWYLXUGP-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/3kMWYLXUGP-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/3kMWYLXUGP-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the image above we see the baseline at the top, and both now low priority preloads at the bottom. The difference in performance is quite striking. Pixels are being rendered to the screen a whole 2 seconds faster. Not bad for a 3G Fast connection (on Chrome desktop)!&lt;/p&gt;&lt;p&gt;Next let&#39;s take a look at the timing information graph:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here a complete list of timing information for each of the test shown in a bar chart.&quot; decoding=&quot;async&quot; height=&quot;814&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/AJlh_CLkAX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/AJlh_CLkAX-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/AJlh_CLkAX-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/AJlh_CLkAX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The most important difference for me is the render metrics. Comparing the baseline to both preloads being set to &lt;code&gt;low&lt;/code&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Speed Index changes from 5,630 to 5,172, or an improvement of 8%.&lt;/li&gt;&lt;li&gt;First Contentful Paint and First Meaningful Paint change from 3,650 to 1,972, or an improvement of 46%.&lt;/li&gt;&lt;li&gt;Visually Complete from 10,900 to 6,800, or an improvement of 38%.&lt;/li&gt;&lt;li&gt;Total Blocking Time from 564 to 328, or an improvement of 39%.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now it isn&#39;t all positive, we do see certain metrics get worse:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Time to Interactive changes from 6,686 to 8,392 or a regression of 26%.&lt;/li&gt;&lt;li&gt;Largest Contentful Paint changes from 6,737 to 6,949, or a regression of 3%.&lt;/li&gt;&lt;li&gt;Load Time (Fully Loaded) changes from 13,354 to 13,487, or a regression of 1%.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The visually progress graph really shows the difference in the way the page is rendered for each test:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the visual progress graph for all 4 tests compared.&quot; decoding=&quot;async&quot; height=&quot;359&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/pvWsIu1YMf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/pvWsIu1YMf-300.webp 300w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/pvWsIu1YMf-600.webp 600w, https://nooshu.com/blog/2021/02/20/testing-priority-hints-with-webpagetest/pvWsIu1YMf-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The green line where both preloads are set to &lt;code&gt;low&lt;/code&gt; clearly starts a lot sooner than the baseline (blue). The green line is also ahead of the rest all the way up to 99% complete. Also note the interesting JavaScript rehydration dip for all tests around the 9 second mark.&lt;/p&gt;&lt;p&gt;I find it quite incredible what dropping the priority of two JavaScript files (170 KB and 83 KB) can do for page performance. If you are prioritising perceived performance metrics this could be considered a win, although there are regressions in other metrics. But it&#39;s also worth noting that this page is only preloading 2 JavaScript files out of the 59 (yes, 59!) JavaScript it loads in total. So with some further investigation into priority hints it may be possible to improve these other metrics too. This is left as an exercise for the reader.&lt;/p&gt;&lt;p&gt;If you are interested in the worker code for this experiment it can be &lt;a href=&quot;https://gist.github.com/Nooshu/58718d04e4d1026187cba3dc3ced5f6d&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;found in this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;looking-for-feedback&quot;&gt;Looking for feedback&lt;/h2&gt;&lt;p&gt;So although priority hints are currently on hold, the Chrome team are still looking for feedback for future iterations. There are a number of issues open &lt;a href=&quot;https://github.com/WICG/priority-hints/issues/&quot;&gt;in the github repo&lt;/a&gt; with feedback from the community and &lt;a href=&quot;https://twitter.com/addyosmani&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Addy Osmani&lt;/a&gt; has also stated the team are interested in &lt;a href=&quot;https://twitter.com/addyosmani/status/1361174119820132353&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;how much control developers would like over resource loading&lt;/a&gt;. Is it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;More to do with &quot;when&quot; (e.g. &lt;code&gt;&amp;lt;script load-after=&quot;first-paint&quot;&gt;&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;High-level influence (e.g. &lt;code&gt;importance=low/high&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Granular (modulo H/2 priority gotchas) (e.g &lt;code&gt;importance=3&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The team have seen priority hints come in most useful when there&#39;s demonstrable network contention between resources and you want to re-order requests for more optimal use of available bandwidth (e.g. using &lt;code&gt;importance=&quot;low&quot;&lt;/code&gt; to mark certain resources less important, so use bandwidth elsewhere).&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;In this post we&#39;ve looked over what priority hints are, their current status, and how to enable and experiment with them on your local browser. Taking this one step further you can also test on live sites using a Cloudflare Worker and WebPageTest. It just shows what a powerful combination these two tools are when used together!&lt;/p&gt;&lt;p&gt;I&#39;m personally a fan of priority hints and I&#39;d love to see them being developed in the future. I like the idea that I have &lt;em&gt;some&lt;/em&gt; influence over the priority of assets loading in a page. The web is such a varied medium with so many edge cases that it seems impossible (to me anyway) for browser heuristics to cover all of these issues automatically. So giving developers the means to steer a browser in a certain direction sounds like a win to me.&lt;/p&gt;&lt;p&gt;So if you&#39;ve spotted an issue that you think could be solved using priority hints, why not try them out in production using a Cloudflare Worker and WebPageTest and see what results come back. I&#39;m sure the Chrome team would be very interested in hearing about it!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;21/02/21: Initial post published. Thanks to &lt;a href=&quot;https://twitter.com/addyosmani&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Addy Osmani&lt;/a&gt; for the input around feedback and where the team have seen wins with priority hints.&lt;/li&gt;&lt;li&gt;22/02/21: Minor change related to what &#39;weight&#39; really means in terms of prioritisation in Chrome. It&#39;s a massive oversimplification that I plan to address in the future with a new section. Thanks to &lt;a href=&quot;https://twitter.com/programmingart&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Robin Marx&lt;/a&gt; for highlighting!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Using Puppeteer and Squoosh to fix the web performance of embedded tweets</title>
    <link href="https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/" />
    <updated>2021-02-06T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/</id>
    <content type="html">&lt;p&gt;In January I was doing some investigation work into the web performance of some of the UK Governments blog posts using &lt;a href=&quot;https://github.com/cloudfour/lighthouse-parade&quot;&gt;&lt;code&gt;lighthouse-parade&lt;/code&gt;&lt;/a&gt; and I stumbled across a recurring theme: some of the pages that performed the worst were ones that included Twitter embeds. Investigating further lead me to write the following Tweet (very ironic huh!):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;If you are wanting to reference a tweet on a page, don&#39;t embed, just use a simple image + alt text + link. Your users will thank you! • LCP: 600ms slower • 2.7MB more JS! • 25 more requests • LH score dropped 50% Test: real Moto G4 with 3G Fast. #perfmatters #webperf - 11:01 PM · Jan 16, 2021&quot; decoding=&quot;async&quot; height=&quot;912&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/oHRZmEQzlO-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/oHRZmEQzlO-300.webp 300w, https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/oHRZmEQzlO-598.webp 598w&quot; width=&quot;598&quot;&gt;&lt;/p&gt;&lt;p&gt;You are reading that correctly. As well as reducing page performance metrics and the Lighthouse score, a single embed adds an extra 2.7 MB of JavaScript to a page! That&#39;s a crazy amount of code for what is really just a simple bit of text and (maybe) a few images. I&#39;m &lt;a href=&quot;https://twitter.com/SimmerVigor/status/1345393494559502337&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;not the first&lt;/a&gt; to notice this issue either.&lt;/p&gt;&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;/h2&gt;&lt;p&gt;So the problem I have, and one I&#39;m looking to optimise is that I have many tweets embedded across lots of WordPress blog posts that I&#39;d like to quickly fix. I&#39;d also like for others to be able to follow guidance I write to fix these issues too. So I&#39;m looking for a fairly simple &amp; semi-automated solution.&lt;/p&gt;&lt;p&gt;Now WordPress comes with a &lt;a href=&quot;https://wordpress.com/support/wordpress-editor/blocks/twitter-block/&quot;&gt;really useful feature&lt;/a&gt; (for content authors). Simply paste a tweet into a blog post then let the WordPress OEmbed feature take over. &lt;a href=&quot;https://en-gb.wordpress.org/plugins/jetpack/&quot;&gt;Jetpack&lt;/a&gt; will pull in the Tweet data as a &lt;code&gt;&amp;lt;blockquote&gt;&lt;/code&gt;, then pull in the &lt;code&gt;widgets.js&lt;/code&gt; JavaScript and hydrate the tweet to make it more &quot;twittery&quot;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of a tweet dropped into a wordpress blog post that then gets expanded.&quot; decoding=&quot;async&quot; height=&quot;197&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/VQP7PPXKz1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/VQP7PPXKz1-300.webp 300w, https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/VQP7PPXKz1-564.webp 564w&quot; width=&quot;564&quot;&gt;&lt;/p&gt;&lt;p&gt;This is great for an author, it&#39;s very &quot;set it and forget it&quot;, but it&#39;s pretty poor from a web performance point of view. As far as I can tell you can&#39;t control how this link is expanded and hydrated once it hits the page in WordPress (please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;correct me on this&lt;/a&gt; if this assumption is wrong!). For example, we could simply omit the JavaScript &lt;code&gt;widget.js&lt;/code&gt; from being added to the page. Therefore no 2.7 MB JavaScript download, so it&#39;s progress. The downside is that the tweet doesn&#39;t look like it does on Twitter (see the image below):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of a tweet when the JavaScript has been blocked.&quot; decoding=&quot;async&quot; height=&quot;304&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/wm3-UnCxdD-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/wm3-UnCxdD-300.webp 300w, https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/wm3-UnCxdD-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;The above image is taken from Firefox when the Enhanced Tracking Protection setting is set to strict, as the browser will block all social media trackers by default. Personally I&#39;m fine with how it looks. I can still read the content, and if I want to see the actual tweet I can click on the date and it will take me to Twitter. But others will probably disagree. So how do we keep the &quot;look&quot; of a tweet, minimise the web performance impact it has on the page and still allow it to be accessible to screenreaders?&lt;/p&gt;&lt;h2 id=&quot;the-solution&quot;&gt;The solution&lt;/h2&gt;&lt;p&gt;So the solution I&#39;ve picked is actually very simple. Grab a screenshot of the tweet, use the following HTML:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;figure&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;custom_ID&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://twitter.com/....&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;noreferrer noopener&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/images/screenshot-of-tweet.jpg&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Tweet by [name] on [date/hour] (tweet content below)&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;123&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;321&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;figcaption&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&quot;Text contained within the tweet goes here&quot;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;figcaption&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;figure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The code above is simplified, but hopefully you get the idea. You&#39;d probably want to indicate that it links to an external page, and you could provide different image formats too (e.g. WebP, AVIF). The &lt;code&gt;alt&lt;/code&gt; text contains information about the person who tweeted, and the date it happened. This means that even multiple tweets from the same author on a page will be seen as unique to assistive technologies.&lt;/p&gt;&lt;p&gt;There&#39;s an interesting point you should consider before using this method. What if the author of the original tweet deletes it? So your screenshot now becomes a permanent record of the tweet they wanted removed. I think this is beyond the scope of this blog post, but it&#39;s something &lt;a href=&quot;https://twitter.com/edent&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Terence Eden&lt;/a&gt; discusses in his &lt;a href=&quot;https://shkspr.mobi/blog/2021/01/how-to-preserve-deleted-tweets-in-wordpress/&quot;&gt;How to preserve deleted Tweets in WordPress?&lt;/a&gt; blog post.&lt;/p&gt;&lt;p&gt;So quickly skipping over the morality question of preserving tweets, we run into the next issue. How do you screenshot lots of tweets and grab the tweet text without it taking hours? There are ways to screenshot tweets very easily: &lt;a href=&quot;https://tweetcyborg.com/&quot;&gt;Tweet Cyborg&lt;/a&gt; is a service that allows you to input any tweet and it will convert it to an image to download. Or you can even do it directly in both Chrome and Firefox Devtools:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome Devtools gives you the option to capture a node screenshot when you right click on an element.&quot; decoding=&quot;async&quot; height=&quot;342&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/ktyZos1SJJ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/ktyZos1SJJ-300.webp 300w, https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/ktyZos1SJJ-600.webp 600w, https://nooshu.com/blog/2021/02/06/using-puppeteer-and-squoosh-to-fix-twitter-embeds/ktyZos1SJJ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Both these methods work, but if you had to do this for 100&#39;s of tweets, it&#39;s going to take a while. And that isn&#39;t even taking into account capturing the text for the images &lt;code&gt;alt&lt;/code&gt; attribute!&lt;/p&gt;&lt;h2 id=&quot;puppeteer-to-the-rescue&quot;&gt;Puppeteer to the rescue!&lt;/h2&gt;&lt;p&gt;This is where we can use the fantastic &lt;a href=&quot;https://pptr.dev/&quot;&gt;Puppeteer&lt;/a&gt; to help us out. So what is Puppeteer? Quoting from the documentation:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So it allows you to programmatically open a browser, load a page, manipulate the page, capture data, then close the browser down and repeat. All without any input from us once started! And it just so happens to be able to capture screenshots too (since it uses the DevTools API after all, which we&#39;ve already seen has the ability to capture node screenshots above).&lt;/p&gt;&lt;h3 id=&quot;presenting-tweet-grabber&quot;&gt;Presenting &lt;code&gt;tweet-grabber&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;I know, I&#39;m terrible with names... but let&#39;s look beyond that. &lt;a href=&quot;https://github.com/Nooshu/tweet-grabber&quot;&gt;It&#39;s a Puppeteer script&lt;/a&gt; I&#39;ve created that allows you to quickly capture screenshots of multiple tweets. Functionality includes:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Ability to capture a single tweet, multiple (comma separated) tweets, or pass in a file with a list of tweets to be captured.&lt;/li&gt;&lt;li&gt;The uncompressed PNG of the tweet will be captured as well as the text from the tweet (in an accompanying txt file).&lt;/li&gt;&lt;li&gt;Automatic compression of the tweets using the &lt;a href=&quot;https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli&quot;&gt;Squoosh CLI&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So for example, the tweet you can see at the top of this blog post was captured using &lt;code&gt;tweet-grabber&lt;/code&gt; like so:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;$ node index.js https://twitter.com/TheRealNooshu/status/1350578919389470721
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The final compressed image as well as the tweet text is output to the &lt;code&gt;compressed_tweets&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;It&#39;s not a complicated script, all fairly straightforward. I&#39;ve already been putting it to good use to run through 10-15 tweets at a time, all output in less than 40 seconds. Much quicker than manually capturing these tweets!&lt;/p&gt;&lt;h3 id=&quot;what-about-copy-pasting-tweet-text&quot;&gt;What about copy / pasting tweet text?&lt;/h3&gt;&lt;p&gt;So &lt;a href=&quot;https://twitter.com/tomayac&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Thomas Steiner&lt;/a&gt; raised an excellent point after I&#39;d published this post, one which I hadn&#39;t considered. When embedding the tweet as an image you are preventing a user from easily copy and pasting the tweet text should they want to do this. A user could view the image &lt;code&gt;alt&lt;/code&gt; text and / or visit the original tweet on Twitter. But both of these methods aren&#39;t as simple as copying directly from a page. He also came up with a suggestion about using Puppeteer to &lt;a href=&quot;https://twitter.com/tomayac/status/1358356500104806402&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;capture the CSS for a tweet&lt;/a&gt;, while still keeping the original HTML (and discarding the &lt;code&gt;widget.js&lt;/code&gt;). This sounds like an interesting idea I&#39;d like to investigate further. Although I still need to consider the fact I will be writing guidance and maybe training a few &quot;non-technical&quot; folks on how to do this within the limitations of WordPress.&lt;/p&gt;&lt;p&gt;So keep this limitation in mind when using this method. There are a number of alternative solutions listed below that you may want to consider too.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: A simple solution I&#39;ve found to this issue is to use the &lt;code&gt;&amp;lt;figure&gt;&lt;/code&gt; and the &lt;code&gt;&amp;lt;figcaption&gt;&lt;/code&gt; elements. The text is shown below the images and can be seen by screen readers, search engines, and can be copy / pasted by users if required.&lt;/p&gt;&lt;h2 id=&quot;other-alternatives&quot;&gt;Other alternatives&lt;/h2&gt;&lt;p&gt;There are others who have looked into this problem too and have different solutions. I&#39;m linking to them here for completeness:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/wongmjane&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Jane Manchun Wong&lt;/a&gt; has worked on a solution &lt;a href=&quot;https://twitter.com/wongmjane/status/1343717786011357184&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;using graphql fragments, dataloader, relay and nexus&lt;/a&gt;. Follow up tweet &lt;a href=&quot;https://twitter.com/wongmjane/status/1330676158724116481&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;here&lt;/a&gt; with more info.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/luis_fades&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Luis Alvarez&lt;/a&gt; has built a &lt;a href=&quot;https://static-tweet.vercel.app/&quot;&gt;Static Tweet Next.js Demo&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/johanjanssens&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Johan Janssens&lt;/a&gt; has been working on a very work in progress Cloudflare Worker solution, code &lt;a href=&quot;https://gist.github.com/johanjanssens/b3b35a93bb182c15ae97c4ae900f4c0b&quot;&gt;seen here&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/SimmerVigor&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Lucas Pardue&lt;/a&gt; uses &lt;a href=&quot;https://lucaspardue.com/2021/01/04/2-meg-in-the-embed-and-the-little-tweet-said-give-over/&quot;&gt;a simple bash script&lt;/a&gt; to strip out the unwanted (and heavy) resources.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/edent&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Terence Eden&lt;/a&gt; is currently working on a really interesting and clever solution to convert a tweet to an SVG. Thread with info can be &lt;a href=&quot;https://twitter.com/edent/status/1358003200457121792&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;found here&lt;/a&gt;. A writeup of this method can be found on his blog: &lt;a href=&quot;https://shkspr.mobi/blog/2021/02/minimum-viable-tweet-to-svg-2/&quot;&gt;&quot;Minimum Viable Tweet to Semantic SVG&quot;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/edent&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Terence Eden&lt;/a&gt; investigated a similar image capture method back in 2016 using the &lt;a href=&quot;https://screenshotlayer.com/&quot;&gt;Scheenshotlayer API&lt;/a&gt; and &lt;code&gt;mogrify&lt;/code&gt; when &lt;a href=&quot;https://shkspr.mobi/blog/2016/11/making-a-twitter-collage/&quot;&gt;Making a Twitter Collage&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/bruno__quaresma&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Bruno Quaresma&lt;/a&gt; has created &lt;a href=&quot;https://tweetpik.vercel.app/&quot;&gt;TweetPik&lt;/a&gt;, a tool to easily convert tweets to images SVG, PNG and JPG.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/KyleMitBTV&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Kyle Mitofsky&lt;/a&gt; has created the &lt;a href=&quot;https://www.npmjs.com/package/eleventy-plugin-embed-tweet&quot;&gt;&lt;code&gt;eleventy-plugin-embed-tweet&lt;/code&gt;&lt;/a&gt; plugin that allows you to embed a tweet during the static build process.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/umaar&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Umar Hansa&lt;/a&gt; has created &lt;a href=&quot;https://github.com/umaar/better-twitter-embed&quot;&gt;&lt;code&gt;better-twitter-embed&lt;/code&gt;&lt;/a&gt; that he uses on his &lt;a href=&quot;https://umaar.com/dev-tips/feedback&quot;&gt;Dev Tips&lt;/a&gt; website.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So there are a few options around to use if you want to include tweets in your site without the web performance hit.&lt;/p&gt;&lt;h2 id=&quot;improvements-from-team-twitter&quot;&gt;Improvements from team Twitter&lt;/h2&gt;&lt;p&gt;I briefly discussed this issue with &lt;a href=&quot;https://twitter.com/addyosmani&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Addy Osmani&lt;/a&gt; who pointed me in the direction of a tweet from &lt;a href=&quot;https://twitter.com/CharlieCroom/status/1346215480999235589&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Charlie Croom&lt;/a&gt;, a web developer at Twitter. It sounds like the team are &lt;a href=&quot;https://twitter.com/tweetanor/status/1358025197371154438&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;fully aware of this issue&lt;/a&gt; and it&#39;s on their roadmap to be fixed in the future! So with any luck these workarounds won&#39;t be needed in the future.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Embedded tweets are bad for performance, but there are ways to avoid the performance impact. There are downsides to the image capture solution I presented above, but I feel they are fairly minor compared to the 2.7 MB of JavaScript and 25 extra requests embedding a tweet on a page inflicts upon a user visiting a page. If it doesn&#39;t work for you there are other solutions available too, thanks to the wonderful open source community.&lt;/p&gt;&lt;p&gt;As always &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;please let me know&lt;/a&gt; if you have any feedback and comments. I&#39;m especially interested in how this script could be improved!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;06/02/21: Initial post published. Added a link to &lt;a href=&quot;https://twitter.com/edent&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Terence Eden&lt;/a&gt;&#39;s &quot;Making a Twitter Collage&quot; blog post (Thanks Terence). Added link to TweetPik (thanks &lt;a href=&quot;https://twitter.com/lfredolo&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Alfredo Lopez&lt;/a&gt; for the link).&lt;/li&gt;&lt;li&gt;07/02/21: Added two other ways of embedding tweets (&lt;code&gt;eleventy-plugin-embed-tweet&lt;/code&gt; &amp; &lt;code&gt;better-twitter-embed&lt;/code&gt;). Thanks to &lt;a href=&quot;https://twitter.com/slightlylate&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Alex Russell&lt;/a&gt; &amp; &lt;a href=&quot;https://twitter.com/umaar&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Umar Hansa&lt;/a&gt; for pointing them out. Added feedback from &lt;a href=&quot;https://twitter.com/tomayac&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Thomas Steiner&lt;/a&gt; about the copy &amp; pasting limitation if using an image.&lt;/li&gt;&lt;li&gt;11/02/21: Updated the HTML sample in the page to make use of &lt;code&gt;&amp;lt;figure&gt;&lt;/code&gt; and the &lt;code&gt;&amp;lt;figcaption&gt;&lt;/code&gt;. This replicates the HTML output by WordPress when using a caption.&lt;/li&gt;&lt;li&gt;15/02/21: Added a link to &lt;a href=&quot;https://twitter.com/edent&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Terence Eden&lt;/a&gt; blog post on his semantic SVG version of a tweet. Updated the HTML to improve overall accessibility, as the initial version failed the &lt;a href=&quot;https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html&quot;&gt;4.1.2 Name, Role, Value WCAG Success criterion&lt;/a&gt;. Many thanks to &lt;a href=&quot;https://twitter.com/arnauddelafosse&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Arnaud Delafosse&lt;/a&gt; for his feedback, and &lt;a href=&quot;https://twitter.com/simevidas&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Šime Vidas&lt;/a&gt; for flagging it.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>The web performance of internal systems is important, so optimise them too</title>
    <link href="https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/" />
    <updated>2021-02-03T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/</id>
    <content type="html">&lt;p&gt;A few weeks ago I had a really interesting chat with a service team from the &lt;a href=&quot;https://www.gov.uk/government/organisations/department-for-international-trade&quot;&gt;Department for International Trade (DIT)&lt;/a&gt;, who were interested in improving the web performance of one of their services. These are the conversations I really enjoy in my current job role. I get to speak about a subject I really enjoy, (virtually) meet new people, teach them a new skill they may not know much about, and the cherry on the top is the potential to improve a users experience of the service. Unfortunately my heart sank a little when they informed me that the service was internal facing and not publicly accessible to the web. This meant my usual goto tools for evaluating web performance weren&#39;t available. These are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developers.google.com/speed/pagespeed/insights/&quot;&gt;PageSpeed Insights&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://web.dev/chrome-ux-report-data-studio-dashboard/&quot;&gt;CrUX Dashboard&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://yellowlab.tools/&quot;&gt;Yellow Lab Tools&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;But that got me thinking, how can teams test their internal services and products? In this blog post I&#39;m going to investigate this.&lt;/p&gt;&lt;h2 id=&quot;its-never-been-more-important-for-fast-internal-systems&quot;&gt;It&#39;s never been more important for fast internal systems&lt;/h2&gt;&lt;p&gt;As everyone knows (I&#39;d hope!) the whole world has changed due to &lt;a href=&quot;https://en.wikipedia.org/wiki/Coronavirus_disease_2019&quot;&gt;a global pandemic&lt;/a&gt;. With lockdowns happening across the globe this has meant people have had to work from home (if they can). This fact has caused a measurable effect on &lt;a href=&quot;https://www.fastly.com/blog/how-covid-19-is-affecting-internet-performance&quot;&gt;internet performance across the globe&lt;/a&gt;, generally resulting in higher traffic and slower download times. Because of this, the digital tools workers rely on to do their jobs need to be quick to load and become interactive to user input.&lt;/p&gt;&lt;p&gt;Performance issues that may not have been a problem before because your internal user was at the office on the local area network (LAN) where the system is located, suddenly are big issues. What compounds this issue even further is that many people may live in rural areas with slow, unstable connections. Or maybe live on streets in major cities with high &lt;a href=&quot;https://en.wikipedia.org/wiki/Contention_ratio&quot;&gt;contention ratio&lt;/a&gt; connections. Being as we are talking about internal systems, you may even need to throw a &lt;a href=&quot;https://en.wikipedia.org/wiki/Virtual_private_network&quot;&gt;virtual private network (VPN)&lt;/a&gt; into the mix just to be able to connect to your office network to use them. And as I&#39;m sure any VPN user will know, they can sometimes have quite a &lt;a href=&quot;https://www.cloudflare.com/learning/access-management/vpn-speed/&quot;&gt;negative effect on internet speed&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So now&#39;s a good time to remember that staff members are users too. Or if you want to convert that phrase into Government speak: &lt;a href=&quot;https://gds.blog.gov.uk/2015/09/28/civil-servants-are-users-too/&quot;&gt;&quot;Civil servants are users too&quot;&lt;/a&gt;. So make sure you optimise your internal systems too, as &lt;a href=&quot;https://www.tecnostress.it/wp-content/uploads/2010/02/final_webstress_survey_report_229296.pdf&quot;&gt;slow website performance causes stress&lt;/a&gt;. But what tools can you use to do this, given that these internal systems may not be publicly available? Read on to find out.&lt;/p&gt;&lt;h2 id=&quot;webpagetest&quot;&gt;WebPageTest&lt;/h2&gt;&lt;p&gt;First thing I should clear up: when testing an internal system you won&#39;t be able to test using the &lt;strong&gt;public&lt;/strong&gt; instance of &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;. But what you can do is setup your own &lt;strong&gt;private&lt;/strong&gt; instance of WebPageTest. That way you can have it sitting within your own internal network.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of a WebPageTest private instance.&quot; decoding=&quot;async&quot; height=&quot;381&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/QAj2Ttiu05-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/QAj2Ttiu05-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/QAj2Ttiu05-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/QAj2Ttiu05-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once up and running you get access to a huge amount of data for all of your internal systems. And of course &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;there are guides&lt;/a&gt; out there on how to extract as much info as you can out of WebPageTest. Setting up your own instance isn&#39;t as complicated as it sounds. There are a few excellent guides on how to do this for yourself on AWS in just a few minutes:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://calendar.perfplanet.com/2014/webpagetest-private-instances-in-five-minutes/&quot;&gt;WebPagetest Private Instances in Five Minutes - Patrick Meenan&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.robinosborne.co.uk/2019/05/20/a-step-by-step-guide-to-setting-up-an-autoscaling-private-webpagetest-instance/&quot;&gt;A Step by Step Guide to setting up an AutoScaling Private WebPageTest instance - Robin Osborne&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.peterhedenskog.com/blog/2015/07/private-wpt-instance/&quot;&gt;Private WebPageTest instances on AWS - Peter Hedenskog&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://andydavies.me/blog/2012/09/18/how-to-create-an-all-in-one-webpagetest-private-instance/&quot;&gt;Configuring an ‘all-in-one&#39; WebPageTest Private Instance - Andy Davies&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;What this also opens up for you is the ability to run WebPageTest periodically over time via automation. There are a few ways to do this:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://getfal.co/&quot;&gt;Falco - an Open Source WebPageTest runner&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.robinosborne.co.uk/2019/07/01/automating-and-orchestrating-webpagetest/&quot;&gt;Automating WebPageTest via the WebPageTest API - Robin Osborne&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/andydavies/WPT-Bulk-Tester&quot;&gt;WebPageTest Bulk Tester - Andy Davies&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So as you make changes to the system you can see if that are making a positive (or negative) effect on performance.&lt;/p&gt;&lt;h2 id=&quot;lighthouse&quot;&gt;Lighthouse&lt;/h2&gt;&lt;p&gt;The next goto tool is one you probably have on your computer already, and that is &lt;a href=&quot;https://developers.google.com/web/tools/lighthouse&quot;&gt;Lighthouse&lt;/a&gt; by Google. If you have a copy of Google Chrome on your computer, then using Lighthouse to audit an internal system is dead simple:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of this blog with the lighthouse audit panel open.&quot; decoding=&quot;async&quot; height=&quot;237&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/xMlwxiFOoR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/xMlwxiFOoR-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/xMlwxiFOoR-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/xMlwxiFOoR-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In DevTools simply look for the &#39;Lighthouse&#39; tab and click &#39;generate report&#39;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the returned lighthouse report for this blog.&quot; decoding=&quot;async&quot; height=&quot;285&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/JVCIGdcrya-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/JVCIGdcrya-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/JVCIGdcrya-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/JVCIGdcrya-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;A minute of so later after the audit has run it will return a result like those in the image above. One thing I&#39;d highly recommend when doing this is setting up a new Chrome profile specifically for Lighthouse testing, as browser extensions can &lt;a href=&quot;https://www.debugbear.com/blog/2020-chrome-extension-performance-report&quot;&gt;have a huge negative performance impact&lt;/a&gt; on the result depending on what they are doing to the page.&lt;/p&gt;&lt;p&gt;But there&#39;s a lot more to Lighthouse than the audit panel. Here are just a few options available to you:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;You can run Lighthouse &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot;&gt;using the Command line interface (CLI)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;You can run Lighthouse across &lt;a href=&quot;https://github.com/cloudfour/lighthouse-parade&quot;&gt;all pages of a site very easily&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Compare before and after performance results using &lt;a href=&quot;https://googlechrome.github.io/lighthouse-ci/viewer/&quot;&gt;Lighthouse CI Diff&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Automatically run Lighthouse at regular intervals across multiple sites &lt;a href=&quot;https://github.com/verivox/lighthouse-monitor&quot;&gt;using a test runner&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.debugbear.com/blog/custom-lighthouse-audits&quot;&gt;Add your own custom audits&lt;/a&gt; to monitor specific parts of a site&lt;/li&gt;&lt;li&gt;Share results publicly via a &lt;a href=&quot;https://gist.github.com/discover&quot;&gt;Github Gist&lt;/a&gt; and the &lt;a href=&quot;https://googlechrome.github.io/lighthouse/viewer/&quot;&gt;Lighthouse Report Viewer&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;sitespeed-io&quot;&gt;Sitespeed.io&lt;/h2&gt;&lt;p&gt;I&#39;m bumping &lt;a href=&quot;https://www.sitespeed.io/&quot;&gt;Sitespeed.io&lt;/a&gt; up the list as I really don&#39;t think it gets enough love. It truly is a fantastic set of tools to improve your site&#39;s performance. It&#39;s also quick and easy to setup via a simple &lt;code&gt;docker&lt;/code&gt; or &lt;code&gt;npm&lt;/code&gt; command. It&#39;s easy to run on your local machine for quick testing, or can also be configured for continuous testing using &lt;a href=&quot;https://aws.amazon.com/&quot;&gt;AWS&lt;/a&gt; or &lt;a href=&quot;https://www.digitalocean.com/&quot;&gt;Digital Ocean&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the dashboard sitespeed.io gives you.&quot; decoding=&quot;async&quot; height=&quot;434&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/43jX1UEImI-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/43jX1UEImI-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/43jX1UEImI-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/43jX1UEImI-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;With sitespeed.io you can continuously monitor as many pages as you need and this data can then be pushed into Graphite/Grafana to give you dashboards &lt;a href=&quot;https://dashboard.sitespeed.io/d/000000064/page-metrics-mobile?orgId=1&amp;var-base=sitespeed_io&amp;var-path=emulatedMobileFirstView&amp;var-testname=alexa&amp;var-group=www_google_com&amp;var-page=_&amp;var-browser=chrome&amp;var-connectivity=3g&amp;var-function=median&amp;var-resulturl=https:%2F%2Fs3.amazonaws.com%2Fresults.sitespeed.io&amp;var-screenshottype=jpg&quot;&gt;like this&lt;/a&gt;. Under the hood, Sitespeed.io is actually a collection of tools all packaged together. These are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.sitespeed.io/documentation/coach/how-to/&quot;&gt;Coach&lt;/a&gt; - An automated tool that gives you advice on how to make your page faster.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sitespeed.io/documentation/browsertime/introduction/&quot;&gt;Browsertime&lt;/a&gt; - It&#39;s at the heart of the Sitespeed tools as it interacts with the test browsers (e.g. Chrome, Firefox, iOS Safari). It collects performance metrics, images, videos and much more.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sitespeed.io/documentation/pagexray/&quot;&gt;PageXray&lt;/a&gt; - Used for converting HTTP Archive format (HAR) files to JSON for easier reading and use. (There&#39;s more info on HAR files later in the post).&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sitespeed.io/documentation/throttle/&quot;&gt;Throttle&lt;/a&gt; - CLI tool for accurately throttling your network connection when performance testing. &lt;strong&gt;Note&lt;/strong&gt;: it throttles the whole computer&#39;s connection, so remember to stop it when you&#39;ve finished!&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sitespeed.io/documentation/compare/&quot;&gt;Compare&lt;/a&gt; - An online tool so you can quickly and easily compare HAR files (e.g. before / after results).&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;chrome-devtools&quot;&gt;Chrome DevTools&lt;/h2&gt;&lt;p&gt;All modern browsers come with developer tools built in. We&#39;ve come a long way since &lt;a href=&quot;https://hacks.mozilla.org/2017/10/saying-goodbye-to-firebug/&quot;&gt;Firebug in Firefox&lt;/a&gt;! And these tools are becoming more and more powerful with every browser release. This is fantastic for developers, and ultimately end users too, since in theory websites should be less buggy right? (hah!).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the chrome developer tools performance report for a GOV.UK page.&quot; decoding=&quot;async&quot; height=&quot;597&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/cYmIVBGZCb-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/cYmIVBGZCb-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/cYmIVBGZCb-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/cYmIVBGZCb-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The above image shows you the detailed information that an audit in the &#39;Performance&#39; tab can give you about the &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/evaluate-performance&quot;&gt;runtime performance of your web page&lt;/a&gt;. But there&#39;s so much more to Chrome DevTools than just the performance tab:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The network tab gives you &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/network&quot;&gt;detailed information&lt;/a&gt; about all the requests (and responses back) in the browser.&lt;/li&gt;&lt;li&gt;You can &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/memory-problems/heap-snapshots&quot;&gt;gather detailed information about memory usage&lt;/a&gt; including &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/memory-problems&quot;&gt;learning how to fix them&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Look for unused CSS and JavaScript in a page using the &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/coverage&quot;&gt;&#39;Coverage&#39; tab&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;There&#39;s actually an incredibly comprehensive post all about &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference&quot;&gt;Performance Analysis in DevTools&lt;/a&gt; that covers many other aspects.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And if you are looking for non-Google articles on the subject, you can read:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://building.calibreapp.com/debugging-react-performance-with-react-16-and-chrome-devtools-c90698a522ad&quot;&gt;Profiling React performance with React 16 and Chrome Devtools - Ben Schwarz&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.debugbear.com/blog/devtools-network&quot;&gt;Debugging web performance with the Chrome DevTools Network tab - DebugBear&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.debugbear.com/blog/debugging-javascript-memory-leaks&quot;&gt;Debugging JavaScript memory leaks using Chrome DevTools - DebugBear&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;firefox-devtools&quot;&gt;Firefox DevTools&lt;/h2&gt;&lt;p&gt;There are other browsers out there that can also help when it comes to evaluating performance issues via their DevTools. As a Firefox user, I use its developer tools every day. It too comes with a really comprehensive set of tabs you can use to look for site issues:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the Firefox developer tools performance panel.&quot; decoding=&quot;async&quot; height=&quot;427&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/0zMH-2HI76-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/0zMH-2HI76-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/0zMH-2HI76-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/0zMH-2HI76-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You can use the tools to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Identify and debug &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Tools/Performance/Scenarios/Intensive_JavaScript&quot;&gt;intensive JavaScript on the page&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Easily &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor/Performance_analysis&quot;&gt;visualise the types of assets on a page&lt;/a&gt; both with a cold and a warm cache.&lt;/li&gt;&lt;li&gt;Inspect your site for Accessibility issues (Firefox has a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Tools/Accessibility_inspector&quot;&gt;great set of a11y tools&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;libraries-for-extending-analytics-data&quot;&gt;Libraries for extending analytics data&lt;/h2&gt;&lt;p&gt;This may not always be the case for internal tools, but if you are tracking their usage via some form of analytics (e.g. Google Analytics, Fathom, Matomo) you can extend the data captured to include more comprehensive web performance data.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of perfume.js pushing web performance data into Google Analytics.&quot; decoding=&quot;async&quot; height=&quot;414&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/7C3Eh8eM8m-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/7C3Eh8eM8m-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/7C3Eh8eM8m-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/7C3Eh8eM8m-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There are a couple of libraries you can use to do this:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Zizzamia/perfume.js&quot;&gt;Perfume.js&lt;/a&gt; - Can be configured to capture &lt;a href=&quot;https://zizzamia.github.io/perfume/#/real-user-measurements/&quot;&gt;a large set of RUM data&lt;/a&gt; for pushing into an analytics tool.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://getanalytics.io/plugins/perfumejs/&quot;&gt;Analytics - Perfume.js plugin&lt;/a&gt; - Wrapper for Perfume.js to easily push web performance data into many different analytics tools.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleChrome/web-vitals&quot;&gt;web-vitals&lt;/a&gt; - accurately capture &lt;a href=&quot;https://web.dev/vitals/#core-web-vitals&quot;&gt;Core Web Vitals&lt;/a&gt; information, as well as &lt;a href=&quot;https://web.dev/vitals/#other-web-vitals&quot;&gt;all other Web Vitals&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Perfume.js really stands out in the amount of Real User Monitoring (RUM) data it captures. It is fully configurable to capture as much, or as little data as you need. There are a few tutorials on how to do this here:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://medium.com/@zizzamia/first-contentful-paint-with-a-touch-of-perfume-js-cd11dfd2e18f&quot;&gt;First (Contentful) Paint with a touch of Perfume(.js) - Leonardo Zizzamia&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://medium.com/@zizzamia/time-to-interactive-with-rum-862ba874392c&quot;&gt;Time to Interactive with RUM - Leonardo Zizzamia&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=9DZAVpAubtQ&quot;&gt;Video: Improving site web performance with Perfume.js + Analytics - 19 mins&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;javascript-analysis&quot;&gt;JavaScript analysis&lt;/h2&gt;&lt;p&gt;I&#39;m not here to comment on the use of JavaScript in an internal system, although I do believe you should &lt;a href=&quot;https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/&quot;&gt;minimise it&#39;s usage&lt;/a&gt; if you wish to improve its web performance and &lt;a href=&quot;https://www.gov.uk/service-manual/technology/using-progressive-enhancement&quot;&gt;overall resilience&lt;/a&gt;. But assuming you are using it, then you should make sure it is as optimised as much as possible.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of Bundle Wizard visualisation to see what a JavaScript file is made up of.&quot; decoding=&quot;async&quot; height=&quot;380&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/14RtNMymQ1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/14RtNMymQ1-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/14RtNMymQ1-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/14RtNMymQ1-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Thankfully there are a whole range of tools you can use to do this:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/aholachek/bundle-wizard&quot;&gt;bundle-wizard&lt;/a&gt; - CLI tool used to generate visualisations to see what a JavaScript bundle is made up of (see image above) so you can remove anything not needed.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://bundlephobia.com/&quot;&gt;Bundle Phobia&lt;/a&gt; - Find the cost of what adding an npm package to your bundle would do.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/webpack-bundle-analyzer&quot;&gt;Webpack Bundle Analyzer&lt;/a&gt; - Visualize the internals of webpack output files with this tools interactive zoomable treemap.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/source-map-explorer&quot;&gt;source-map-explorer&lt;/a&gt; - Use &lt;a href=&quot;https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/&quot;&gt;source maps&lt;/a&gt; to analyse JavaScript code bloat (this tool also works with Sass and LESS for CSS analysis).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Specific framework performance analysis tools and articles are also available:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/reactopt/reactopt&quot;&gt;reactopt&lt;/a&gt; - A CLI React performance optimization tool that identifies potential unnecessary page re-rendering.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.tracerbench.com/&quot;&gt;TracerBench&lt;/a&gt; - a controlled performance benchmarking tool for web applications. Providing clear, actionable and usable insights into performance deltas.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.debugbear.com/blog/measuring-react-app-performance&quot;&gt;Measuring React app performance - DebugBear&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;css-analysis&quot;&gt;CSS Analysis&lt;/h2&gt;&lt;p&gt;As well as the &#39;Coverage&#39; tab mentioned in Chrome DevTools earlier, there are also tools you can run via the CLI that will analyse your CSS, looking at its complexity as well as identifying unused selectors across a whole website:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/macbre/analyze-css&quot;&gt;analyze-css&lt;/a&gt; - CSS selectors complexity and performance analyzer run using the CLI.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/oyvindeh/ucss&quot;&gt;uCSS&lt;/a&gt; - Crawl a whole site looking for unused CSS selectors that can be then removed.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;server-benchmarking&quot;&gt;Server benchmarking&lt;/h2&gt;&lt;p&gt;Although the &lt;a href=&quot;https://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/&quot;&gt;performance golden rule&lt;/a&gt; states that:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;80-90% of the end-user response time is spent on the frontend.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It&#39;s still a good idea to make sure that the backend / server is optimised. After all &lt;a href=&quot;https://csswizardry.com/2019/08/time-to-first-byte-what-it-is-and-why-it-matters/&quot;&gt;Time to First Byte matters&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;HTTPStat running in the terminal and reporting on timings from visiting GOV.UK.&quot; decoding=&quot;async&quot; height=&quot;616&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/gDPlXUsjby-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/gDPlXUsjby-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/gDPlXUsjby-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/gDPlXUsjby-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You also want to make sure that your server will be able to continue to function under load should that ever occur. There are a number of tools available to help you do both of these things:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/reorx/httpstat&quot;&gt;httpstat&lt;/a&gt; - A small Python script to visualise the connection timing data returned from &lt;code&gt;curl&lt;/code&gt; (see image above).&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nghttp2.org/documentation/h2load-howto.html&quot;&gt;h2load&lt;/a&gt; - A benchmarking tool for HTTP/2 and HTTP/1.1 run from the CLI.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/rakyll/hey&quot;&gt;Hey&lt;/a&gt; - A tiny program for sending load to a server.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://k6.io/open-source&quot;&gt;k6&lt;/a&gt; - Build realistic load tests by writing JavaScript. Comes with an API and CLI.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.smashingmagazine.com/2018/10/performance-server-timing/&quot;&gt;Measuring Performance With Server Timing&lt;/a&gt; - Configure your server to send a header containing server timing information. Exposing backend server performance timings to the frontend so they can be easily observed in the browser.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;automation-using-puppeteer&quot;&gt;Automation using Puppeteer&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://pptr.dev/&quot;&gt;Puppeteer&lt;/a&gt; is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Most things you can do manually in a browser can be replicated using Puppeteer, so how can this be leveraged to test web performance? &lt;a href=&quot;https://twitter.com/addyosmani&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Addy Osmani&lt;/a&gt; has blogged about using it for web performance testing and also shared the code on Github. These tests can be easily run via the CLI to test both internal and external websites.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/addyosmani/puppeteer-webperf#automating-web-perf-measurement-with-puppeteer&quot;&gt;Automating Web Perf measurement with Puppeteer&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://addyosmani.com/blog/puppeteer-recipes/&quot;&gt;Web Performance Recipes With Puppeteer&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;browser-extensions&quot;&gt;Browser Extensions&lt;/h2&gt;&lt;p&gt;There are a whole host of browser extensions you can use to evaluate web performance. When running these, I&#39;d recommend using a separate profile with a minimal number of extensions enabled. This is because some browser extensions interact with the page and can slow down performance. Leading to skewed results. Extensions you can use are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/denar90/sloth&quot;&gt;sloth&lt;/a&gt; - An extension that slows down the CPU and network in the browser, allowing you to easily mimic page performance on slower devices.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/perfmap/hgpnhiajcdppfbogcpfdgcceepgkhdmk?hl=en&amp;gl=GB&quot;&gt;Perfmap&lt;/a&gt; - When used, the browser will create a heatmap of the resources loaded in the browser and their individual performance impact according to the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API&quot;&gt;Resource Timing API&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleChrome/web-vitals-extension&quot;&gt;Web Vitals Chrome Extension&lt;/a&gt; - Chrome extension that reports the core web vitals metrics (LCP, CLS, FID) for any page visited. &lt;strong&gt;Note&lt;/strong&gt;: this is soon to be built into Chrome DevTools, and can &lt;a href=&quot;https://twitter.com/addyosmani/status/1355795453980372994&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;currently be seen in Canary&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://gist.github.com/tkadlec/683b26344cde774170b94c0fcf0088b4&quot;&gt;perf-diagnostics.css&lt;/a&gt; - This isn&#39;t strictly a browser extension, it&#39;s a selection of CSS you can add to your page for debugging common performance issues. Simple and effective way to highlight images without a &lt;code&gt;width&lt;/code&gt; / &lt;code&gt;height&lt;/code&gt; attribute, among a number of others.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There are also extensions available to specifically help improve the performance when using certain JavaScript frameworks:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot;&gt;React Developer Tools&lt;/a&gt; - This extension add a &#39;Profiler&#39; tab to DevTools, allowing you to drill down into the performance of your React application.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://augury.rangle.io/&quot;&gt;Angular Augury&lt;/a&gt; - A Chrome and Firefox extension that allows you to debug and profile Angular applications.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi?hl=en&quot;&gt;Ember Inspector&lt;/a&gt; - Debug and dig into the detail of a web application built with Ember.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;network-throttling&quot;&gt;Network Throttling&lt;/h2&gt;&lt;p&gt;Network throttling, as the name suggests this is a way to slow your network connection down. It&#39;s important to realise that many users won&#39;t have a fast (and stable) broadband connection in a major city. Many users could be in rural areas with poor broadband and very little mobile signal. By throttling your own network connection you get an idea of the performance of a site for users under these specific network conditions.&lt;/p&gt;&lt;p&gt;Now you may be asking: &quot;Why would I throttle my network when Chrome Devtools has this built in?&quot;. Well it&#39;s important to realise that &lt;a href=&quot;https://www.debugbear.com/blog/network-throttling-methods&quot;&gt;not all network throttling methods are created equally&lt;/a&gt;. When throttling using Chrome DevTools it applies a delay at the browser-level when each response is received. Lighthouse runs the test at full speed, then simulates the connection speed afterwards, sacrificing accuracy for reporting speed. The following tools are much more accurate. They use OS-level network throttling which works at a much lower level. &lt;strong&gt;Note&lt;/strong&gt;: these tools below throttle the connection for the &lt;em&gt;whole&lt;/em&gt; machine, so make sure you minimise the number of applications running when testing, and disable throttling once done!&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nshipster.com/network-link-conditioner/&quot;&gt;Network Link Conditioner&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/sitespeedio/throttle&quot;&gt;Throttle&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I use &lt;code&gt;throttle&lt;/code&gt; all the time as it&#39;s so simple to use:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Enable 3G Slow&lt;/span&gt;
throttle 3gslow

&lt;span class=&quot;token comment&quot;&gt;# Use a custom connection profile&lt;/span&gt;
throttle &lt;span class=&quot;token parameter variable&quot;&gt;--up&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1600&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--down&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;780&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--rtt&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;250&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Disable throttle&lt;/span&gt;
throttle &lt;span class=&quot;token parameter variable&quot;&gt;--stop&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;har-file-analysis&quot;&gt;HAR file analysis&lt;/h2&gt;&lt;p&gt;I mentioned HTTP Archive format (HAR) files earlier in the post. These files are a way of capturing a web browsers network interaction with a website. The great thing about these files is you can capture them for any site you can get to via your browser (internal or external). Capturing them is very simple in both Firefox and Chrome developer tools:&lt;/p&gt;&lt;h3 id=&quot;firefox&quot;&gt;Firefox&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Image shows saving of a HAR file from the network tab in Firefox&quot; decoding=&quot;async&quot; height=&quot;238&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/Z89bx_5ozF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/Z89bx_5ozF-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/Z89bx_5ozF-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/Z89bx_5ozF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;chrome&quot;&gt;Chrome&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Image shows saving of a HAR file from the network tab in Chrome&quot; decoding=&quot;async&quot; height=&quot;382&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/I-Y7-fp_6I-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/I-Y7-fp_6I-300.webp 300w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/I-Y7-fp_6I-600.webp 600w, https://nooshu.com/blog/2021/02/03/the-importance-of-internal-system-performance/I-Y7-fp_6I-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once captured there are a bunch of different tools you can use to view and analyse them:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://toolbox.googleapps.com/apps/har_analyzer/&quot;&gt;HAR analyzer&lt;/a&gt; - In-browser HAR analysis tool from Google.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://yslow.org/command-line-har/&quot;&gt;YSlow Command Line HAR analyser&lt;/a&gt; - Very old now, but YSlow will analyse the HAR file looking for performance improvements.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sitespeed.io/documentation/pagexray/&quot;&gt;PageXray&lt;/a&gt; - Convert a HAR file to JSON so it is easier to read and interact with (used in Sitespeed.io).&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sitespeed.io/documentation/compare/&quot;&gt;Compare&lt;/a&gt; - An online tool so you can quickly and easily compare HAR files (e.g. before / after results).&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://www.softwareishard.com/har/viewer/&quot;&gt;HAR Viewer&lt;/a&gt; - Simple online HAR viewer to visualise the metrics within.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.charlesproxy.com/&quot;&gt;Charles Proxy&lt;/a&gt; - HTTP proxy / HTTP monitor that can accept HAR files as input for detailed analysis.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.telerik.com/fiddler&quot;&gt;Fiddler&lt;/a&gt; - a debugging proxy server tool that can log, inspect, and alter traffic between a computer and a web server. It can also be used to analyse HAR files.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/google/har2csv&quot;&gt;Har2CSV&lt;/a&gt; - A CLI tool which provides an easy way to convert HAR file HTTP log entries into a flat CSV format to make it easier to analyze.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Nooshu/har2csv&quot;&gt;Har2CSV &amp; Har2TSV&lt;/a&gt; - My own version of the har2csv tool above but written with &lt;a href=&quot;https://stedolan.github.io/jq/&quot;&gt;jq&lt;/a&gt; rather than node.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;web-apis&quot;&gt;Web APIs&lt;/h2&gt;&lt;p&gt;Going back to browser basics there are a few APIs you can use to measure the performance of a site if you want to do this yourself without using any abstraction libraries.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance/now&quot;&gt;performance.now()&lt;/a&gt; - The &lt;code&gt;now()&lt;/code&gt; method of the browser &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance&quot;&gt;Performance&lt;/a&gt; interface returns a high resolution timestamp from when the method was called. Allowing you to measure the time between 2 calls very easily. Adding these before and after specific code will allow you to measure, and optimise this code.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://w3c.github.io/navigation-timing/&quot;&gt;Navigation Timing&lt;/a&gt; - This API allows developers to capture high resolution timing data related to the &lt;a href=&quot;https://w3c.github.io/navigation-timing/#processing-model&quot;&gt;navigation of a document&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://w3c.github.io/resource-timing/&quot;&gt;Resource Timing&lt;/a&gt; - This API allows developers to collect complete timing information about the resources loaded by a document.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/navigation-and-resource-timing/&quot;&gt;Assessing Loading Performance in Real Life with Navigation and Resource Timing - Jeremy Wagner&lt;/a&gt; - A very comprehensive blog post all about how the two APIs listed above can be used to assess the loading performance of a web page.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;Hopefully the resources listed above will give you the tools needed to improve your internal systems. Just because a service (or website) is only able to be accessed internally on a company network, doesn&#39;t mean that it can&#39;t be (and shouldn&#39;t be!) optimised for web performance. With so many people working remotely at the moment, don&#39;t assume that everyone has the luxury of a fast and stable connection. Because remember, you and your colleagues are users too!&lt;/p&gt;&lt;p&gt;As always, if you have any feedback about this post, or if I&#39;ve missed anything please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;. Credit will be given in the changelog below.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;03/02/21: Initial post published.&lt;/li&gt;&lt;li&gt;03/03/21: Added &#39;Har2CSV&#39; to HAR file analysis. Thanks to &lt;a href=&quot;https://twitter.com/paulcalvano&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Paul Calvano&lt;/a&gt; for the link.&lt;/li&gt;&lt;li&gt;04/03/21: Added my version of &#39;Har2CSV&#39; that uses jq.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>The importance of `@font-face` source order when used with preload</title>
    <link href="https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/" />
    <updated>2021-01-23T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/</id>
    <content type="html">&lt;p&gt;The other day I decided to run a quick &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest (WPT)&lt;/a&gt; run over the latest version of the &lt;a href=&quot;https://www.whitehouse.gov&quot;&gt;White House&lt;/a&gt; website launched for President Biden&#39;s term in office (it&#39;s amazing what I do for fun in lockdown huh!). The WPT run returned something curious:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The waterfall from WebPageTest using Chrome on a Moto G4 on a 3G Fast connection.&quot; decoding=&quot;async&quot; height=&quot;644&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/ZNOaOTVjwY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/ZNOaOTVjwY-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/ZNOaOTVjwY-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/ZNOaOTVjwY-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The above waterfall was captured using Chrome on a real Moto G4 with a 3G Fast connection. That&#39;s a lot of font files being downloaded (red requests), 14 files in total.&lt;/p&gt;&lt;p&gt;So let&#39;s investigate what is happening here. First thing to notice is that there are 6 fonts that are loading right after the HTML has downloaded and parsed (request 2-7). This is a sure sign that these fonts are being &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#preloading&quot;&gt;preloaded&lt;/a&gt;. What usually happens is the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction&quot;&gt;DOM&lt;/a&gt; is constructed, then the CSS downloaded to create the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/CSSOM&quot;&gt;CSSOM&lt;/a&gt;. These are both combined to form the &lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction&quot;&gt;render tree&lt;/a&gt;. At this point, fonts referenced in the &lt;code&gt;@font-face&lt;/code&gt; rule will be discovered and requested by the browser (assuming they are needed to render the specified text on the page).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;s&gt;This blog post isn&#39;t a criticism of the excellent work the team at the &lt;a href=&quot;https://www.usds.gov/&quot;&gt;U.S. Digital Service&lt;/a&gt; does. It&#39;s just an interesting observation I&#39;ve never encountered or considered before. Being a Civil Servant myself, I understand how hard everyone works to deliver user friendly services (especially under current conditions!). Keep being awesome!&lt;/s&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note 2&lt;/strong&gt;: Turns out USDS &lt;a href=&quot;https://twitter.com/nas5w/status/1352966520784252928&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;didn&#39;t build this website&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note 3&lt;/strong&gt;: As of 29th Jan the font loading issue has now been fixed by the White House developer team and can be &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1354930248786333701&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;seen on the live site&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;font-load-priority&quot;&gt;Font load priority&lt;/h2&gt;&lt;p&gt;There&#39;s an important point to consider here. The order in which you list your font sources in the &lt;code&gt;@font-face&lt;/code&gt; rule is very important. According to the &lt;a href=&quot;https://www.w3.org/TR/css-fonts-3/#src-desc&quot;&gt;CSS Fonts Module Level 3 specification&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;It is required for the &lt;code&gt;@font-face&lt;/code&gt; rule to be valid. Its value is a prioritized, comma-separated list of external references or locally-installed font face names. When a font is needed the user agent iterates over the set of references listed, using the first one it can successfully activate. Fonts containing invalid data or local font faces that are not found are ignored and the user agent loads the next font in the list.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Or in other words, the browser will start at the top of the list looking for a font format it supports. The first one it encounters is loaded, and if successful the sources listed later are ignored. Or in code it looks like this:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bodytext&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.woff2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* WOFF2 tried first */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.woff&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* WOFF tried second */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.ttf&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;opentype&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* TTF tried if others not supported. */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&#39;s a large crossover between browsers that &lt;a href=&quot;https://caniuse.com/woff&quot;&gt;support WOFF&lt;/a&gt; and those that support &lt;a href=&quot;https://caniuse.com/woff2&quot;&gt;WOFF2&lt;/a&gt;. Or to phrase it another way, if a browser supports WOFF2, it also supports WOFF. This is important, as if you are listing your WOFF fonts first in the &lt;code&gt;@font-face&lt;/code&gt; rule, the browser is going to download and use these instead of the WOFF2 versions (which offer &lt;a href=&quot;https://paulcalvano.com/2018-07-25-brotli-compression-how-much-will-it-reduce-your-content/&quot;&gt;much better compression&lt;/a&gt; because they use &lt;a href=&quot;https://github.com/google/brotli&quot;&gt;Brotli&lt;/a&gt;). So for example if you do this:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bodytext&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.woff&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* WOFF tried first */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.woff2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* If WOFF is successful, WOFF2 is ignored */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.ttf&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;opentype&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* TTF tried if other not supported. */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Assuming the WOFF font doesn&#39;t 404, the WOFF2 font will never be requested by the browser!&lt;/p&gt;&lt;h2 id=&quot;the-waterfall&quot;&gt;The Waterfall&lt;/h2&gt;&lt;p&gt;The above information can now be used to explain what is happening with the waterfall we see above.&lt;/p&gt;&lt;h3 id=&quot;preload-requests-2-7&quot;&gt;Preload (requests 2-7)&lt;/h3&gt;&lt;p&gt;Examining the code from the White House website we can see the following in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...fonts/Decimal-Book.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;anonymous&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...fonts/Decimal-Semibold.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;anonymous&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...fonts/Decimal-Medium_Web.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;anonymous&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...fonts/MercuryTextG2-Roman-Pro_Web.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;anonymous&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...fonts/MercuryTextG2-Semibold-Pro_Web.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;anonymous&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...fonts/MercurySSm-Medium-Pro_Web.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;anonymous&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These &lt;code&gt;preload&lt;/code&gt; links tell the browser to immediately make the requests for the WOFF2 versions of the fonts as soon as the browser parses the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;, which it happily does as it trusts that these fonts will be used later in the pages lifecycle. Once these requests have been made and are sent over the network the browser &lt;em&gt;is&lt;/em&gt; going to get a response back (assuming the fonts are available and don&#39;t 404). Even if the browser were to cancel the requests, it will still receive bytes back from the server for the fonts. They just wouldn&#39;t be used. There&#39;s an example waterfall of this &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#a-vary-unusual-waterfall-empty-gaps-between-download-data&quot;&gt;happening here&lt;/a&gt; and a blog post &lt;a href=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/&quot;&gt;all about it here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;css-request-9&quot;&gt;CSS (request 9)&lt;/h3&gt;&lt;p&gt;At request 9 we can see the CSS file being downloaded and parsed by the browser. Also notice how only 2 of the preloaded fonts have been fully downloaded. This is a critical part of the page load with limited bandwidth available. Under the hood the browser still needs fonts to be able to render text on screen. A &lt;code&gt;preload&lt;/code&gt; will prime the browser cache, but it won&#39;t add a font to the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet&quot;&gt;&lt;code&gt;FontFaceSet&lt;/code&gt;&lt;/a&gt;. For that you either need to use the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API&quot;&gt;CSS Font Loading API&lt;/a&gt;, or the traditional &lt;code&gt;@font-face&lt;/code&gt; rule.&lt;/p&gt;&lt;p&gt;Within this CSS file on request 9 we see the &lt;code&gt;@font-face&lt;/code&gt; rules (I&#39;ve only referenced 1 as an example, but there are many):&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; MercuryTextG2-Semibold-Pro_Web&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.../fonts/MercuryTextG2-Semibold-Pro_Web.woff&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    	&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.../fonts/MercuryTextG2-Semibold-Pro_Web.woff2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 400&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; block
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Notice the ordering of the &lt;code&gt;src&lt;/code&gt; fonts in this &lt;code&gt;@font-face&lt;/code&gt; rule. The WOFF version of the font is listed first, then WOFF2 listed second.&lt;/p&gt;&lt;h2 id=&quot;the-result&quot;&gt;The result&lt;/h2&gt;&lt;p&gt;The browser is now making 8 more font requests to WOFF font files (requests 10-17). To the browser it sees these as completely separate files. It doesn&#39;t know that the glyphs contained within them are actually identical to the ones from the preloaded fonts! The browser is now doubling up on the number of fonts requested because it has stopped at the WOFF font &lt;code&gt;src&lt;/code&gt; in the &lt;code&gt;@font-face&lt;/code&gt; rule. It has no knowledge about the WOFF2 version directly below because it has found the first one it supports, then stopped (as defined in the specifications).&lt;/p&gt;&lt;p&gt;This is probably easier to explain on a waterfall chart:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the waterfall we can see the preloaded WOFF2 fonts being requested, then later after CSS parsing another 8 font requests to the WOFF versions of the fonts.&quot; decoding=&quot;async&quot; height=&quot;381&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/D3z425AgU4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/D3z425AgU4-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/D3z425AgU4-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/D3z425AgU4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The above waterfall is from Chrome on a Moto G4 with a 3G Fast connection, but I have also managed to replicate the same result on &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1352225153753772033&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;iOS Safari&lt;/a&gt;, &lt;a href=&quot;https://www.webpagetest.org/result/210121_DiVT_b883fc1c5fb118a5e782afe0c48e398c/1/details/#waterfall_view_step1&quot;&gt;Microsoft Edge (Chromium)&lt;/a&gt;, and &lt;a href=&quot;https://twitter.com/TheRealNooshu/status/1352228844925808640&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Firefox&lt;/a&gt; (which &lt;a href=&quot;https://caniuse.com/link-rel-preload&quot;&gt;now supports &lt;code&gt;preload&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;&lt;h2 id=&quot;cloudflare-workers&quot;&gt;Cloudflare Workers&lt;/h2&gt;&lt;p&gt;Here&#39;s where I get the chance to play with &lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers&lt;/a&gt; again. For information on how this is done I recommend reading &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&#39;&lt;/a&gt; excellent &#39;&lt;a href=&quot;https://andydavies.me/blog/2020/09/22/exploring-site-speed-optimisations-with-webpagetest-and-cloudflare-workers/&quot;&gt;Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers&lt;/a&gt;&#39; blog post, or you can watch him talk about it at London Web Performance &lt;a href=&quot;https://ldnwebperf.org/sessions/webpagetest-and-cloudflare-workers-a-performance-consultants-dream-combo/&quot;&gt;here&lt;/a&gt;. If you&#39;d like to see a fully commented sample of my worker code I&#39;ve created a &lt;a href=&quot;https://gist.github.com/Nooshu/7396a316c227138a2875733780721aa2&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;modifying-the-preloads&quot;&gt;Modifying the &lt;code&gt;preloads&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;First let&#39;s investigate what happens if we &#39;fix&#39; the &lt;code&gt;preload&lt;/code&gt; links and point them to the WOFF fonts referenced in the &lt;code&gt;@font-face&lt;/code&gt; rules.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;We now see the waterfall has changed and the WOFF fonts are being preloaded, removing the need for then to be requested by the @font-face rules.&quot; decoding=&quot;async&quot; height=&quot;381&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/hnWR5Cc076-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/hnWR5Cc076-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/hnWR5Cc076-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/hnWR5Cc076-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the resulting waterfall we now see the WOFF fonts preloaded in requests 2-7. This in turn reduces the number of fonts requested by the &lt;code&gt;@font-face&lt;/code&gt; rules after the CSS has been parsed.&lt;/p&gt;&lt;h3 id=&quot;modifying-the-css&quot;&gt;Modifying the CSS&lt;/h3&gt;&lt;p&gt;Now let&#39;s do the reverse, let&#39;s update the CSS &lt;code&gt;@font-face&lt;/code&gt; rules and swap the order so the WOFF2 fonts are first. This will then match the WOFF2 fonts listed in the &lt;code&gt;preload&lt;/code&gt; links in the head.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the WOFF2 fonts preloaded and very few font requests after CSS parsing.&quot; decoding=&quot;async&quot; height=&quot;381&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/RD5pN9xzVp-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/RD5pN9xzVp-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/RD5pN9xzVp-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/RD5pN9xzVp-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In this waterfall the &lt;code&gt;preload&lt;/code&gt; is untouched, but I&#39;ve simply made the WOFF2 fonts come first in the &lt;code&gt;src&lt;/code&gt; order like so:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; MercuryTextG2-Semibold-Pro_Web&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.../fonts/MercuryTextG2-Semibold-Pro_Web.woff2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    	&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.../fonts/MercuryTextG2-Semibold-Pro_Web.woff&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 400&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; block
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s amazing what such a small change can do to a waterfall.&lt;/p&gt;&lt;h3 id=&quot;effect-on-performance&quot;&gt;Effect on performance&lt;/h3&gt;&lt;p&gt;But it&#39;s not a cleaner looking waterfall that&#39;s important, it&#39;s the performance a user sees after the change. So the first thing to note is the impact on the number of bytes downloaded:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Number of bytes downloaded from the different versions. We see the original with the most, then the preloaded WOFF fonts, and the least, preloaded WOFF2 with fixed @font-face rules.&quot; decoding=&quot;async&quot; height=&quot;415&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/xIs7EvsWpF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/xIs7EvsWpF-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/xIs7EvsWpF-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/xIs7EvsWpF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Version&lt;/th&gt;&lt;th&gt;Total Size (bytes)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Original&lt;/td&gt;&lt;td&gt;2,133,401&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fixed preload&lt;/td&gt;&lt;td&gt;1,904,496&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fixed &lt;code&gt;@font-face&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1,696,008&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Reordering the &lt;code&gt;@font-face&lt;/code&gt; rule alone has reduced the number of bytes downloaded by 440KB. That&#39;s a pretty decent saving by simply swapping a few 2&#39;s in the font filename and &lt;code&gt;format()&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;But where you really see the difference is in the visual progress graph:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The visual progress really shows the difference fixing the font face can make on perceived performance&quot; decoding=&quot;async&quot; height=&quot;361&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/OWtG7epcFf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/OWtG7epcFf-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/OWtG7epcFf-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/OWtG7epcFf-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Version&lt;/th&gt;&lt;th&gt;Visually Complete (ms)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Original&lt;/td&gt;&lt;td&gt;8,668&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fixed preload&lt;/td&gt;&lt;td&gt;5,681&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fixed &lt;code&gt;@font-face&lt;/code&gt;&lt;/td&gt;&lt;td&gt;5,311&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;We&#39;ve shaved off almost 3.4 seconds from the visual complete time on a Moto G4 / 3G Fast connection, a 39% reduction. That&#39;s a huge improvement in performance for such a minor change. Your users will really notice that change. If you are interested in the full test results then &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=210122_DiE8_9bd4ed17dfe19a6643e1a446aa4b2e73-l:Original,210122_DiNK_fde7b22ce04effc0b999e1ef60cd2cd9-l:Fixed%20Preload,210123_DiMH_9625beaae8c5f65e57e4fadf9d7102f2-l:Fixed%20Fontface&amp;medianMetric=SpeedIndex&amp;bg=000000&amp;text=ffffff&quot;&gt;here&#39;s the WebPageTest comparison view&lt;/a&gt; with all three tests.&lt;/p&gt;&lt;h2 id=&quot;is-this-a-problem-in-the-real-world&quot;&gt;Is this a problem in the real world?&lt;/h2&gt;&lt;p&gt;So a good question to ask is now: is this a one-off occurrence, or does it happen quite frequently? Well, with December data from the &lt;a href=&quot;https://httparchive.org/&quot;&gt;HTTP Archive&lt;/a&gt; and the help of &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; we are able to answer this question:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Mobile (abs)&lt;/th&gt;&lt;th&gt;Desktop (abs)&lt;/th&gt;&lt;th&gt;Mobile (%)&lt;/th&gt;&lt;th&gt;Desktop (%)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Download same WOFF / WOFF2 font (one fails)&lt;/td&gt;&lt;td&gt;155,541&lt;/td&gt;&lt;td&gt;132,044&lt;/td&gt;&lt;td&gt;2.17&lt;/td&gt;&lt;td&gt;2.19&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Download same WOFF / WOFF2 font (both return 200)&lt;/td&gt;&lt;td&gt;69,805&lt;/td&gt;&lt;td&gt;59,387&lt;/td&gt;&lt;td&gt;0.97&lt;/td&gt;&lt;td&gt;0.99&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;We see 155,541 (2.17%) sites try to download both WOFF and WOFF2 fonts with the same filename. What most likely happens in these instances is the WOFF2 file fails, so the browser moves onto the WOFF fallback. This still isn&#39;t great for web performance since an extra request / response is made and adds time to the page load and blocks text rendering (I&#39;m over-simplifying here I know). But what&#39;s worse is that we see 69,805 (0.97%) sites successfully double download both WOFF and WOFF2 versions of the exact same font! That&#39;s a lot of unnecessary bytes being downloaded which simply won&#39;t be used. If you are interested in the full dataset for this query you can &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1wquKjkjNlgvu9kNz580Bpr8n0BB0xmKdJWnCqgt_w10/edit#gid=1696451362&quot;&gt;view it here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;spotting-the-preload-issue&quot;&gt;Spotting the &lt;code&gt;preload&lt;/code&gt; issue&lt;/h2&gt;&lt;p&gt;So how would you know if this is an issue for your site? Well the easiest thing to do is to look in your browser console. Both Firefox and Chrome will give you a set of warnings if you are preloading assets that aren&#39;t actually used in the page:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;If you preload assets that aren&#39;t used in the page load the browser will most likely tell you in the console. Warnings from Firefox about the unused fonts.&quot; decoding=&quot;async&quot; height=&quot;212&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/DG12xyo9RS-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/DG12xyo9RS-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/DG12xyo9RS-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/DG12xyo9RS-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;what-if-the-first-src-fails&quot;&gt;What if the first &lt;code&gt;src&lt;/code&gt; fails&lt;/h2&gt;&lt;p&gt;So let&#39;s now look at what happens with a waterfall when the first source in the &lt;code&gt;@font-face&lt;/code&gt; fails. This isn&#39;t related to the White House website but the HTTP Archive data shows that it is happening for approximately 86,000 (1.2%) of sites in the HTTP Archive data. There&#39;s a caveat with this data. The only thing we know is that the browser tries to download both types of font files (WOFF &amp; WOFF2). We also know that one of these fonts doesn&#39;t return a &lt;code&gt;200&lt;/code&gt; status code (hence the fallback font then loads). But we don&#39;t have an exact figure on if one of the font requests was triggered by a &lt;code&gt;preload&lt;/code&gt;, then falling back to &lt;code&gt;@font-face&lt;/code&gt;. We also don&#39;t have the data on what source order the fonts are loaded in if using &lt;code&gt;@font-face&lt;/code&gt;. So all in all, it&#39;s complicated! But what we &lt;em&gt;do&lt;/em&gt; know is that the browser tried to load fonts of both types, and for some reason one of them failed (&lt;code&gt;4xx&lt;/code&gt; status code).&lt;/p&gt;&lt;p&gt;What does this look like on a waterfall and what&#39;s the performance impact?&lt;/p&gt;&lt;h3 id=&quot;the-waterfall-and-performance-impact&quot;&gt;The waterfall &amp; performance impact&lt;/h3&gt;&lt;p&gt;Below you can see the annotated waterfall for a site that requests the WOFF2 font first but fails. This test was again run on a real Moto G4 / 3G Fast connection:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the waterfall we see lots happening where by both sets of WOFF and WOFF2 fonts are loaded over the lifecycle of the page.&quot; decoding=&quot;async&quot; height=&quot;326&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/bj1JSgOfup-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/bj1JSgOfup-300.webp 300w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/bj1JSgOfup-600.webp 600w, https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/bj1JSgOfup-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There&#39;s a fair amount going on in this waterfall so let&#39;s step through it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;On request 37 we see the TCP negotiation happen for the font domain. Once completed the font request goes out for the WOFF2 font. This whole process takes 2 seconds to complete. It takes ~865 ms to fail. That includes a full round trip to the server and back.&lt;/li&gt;&lt;li&gt;On request 41 it takes a full 1.7 seconds to fail. Notice how once failed the browser almost immediately requests the fallback WOFF font (request 46).&lt;/li&gt;&lt;li&gt;Notice how this fail / request pattern for all 4 fonts are aligned. Requests 37 &amp; 43, 39 &amp; 45, 40 &amp; 44, and 41 &amp; 46 are all font pairings (WOFF2 vs WOFF).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So in other words the browser must receive a font failure before it can request the fallback. This lack of primary font source is adding 2 seconds to the font load in this test case. What&#39;s worse in the test case above is that the browser ends up downloading the larger of the two font file types. Since WOFF files can be 20-30% larger than WOFF2 files. Ultimately the user gets a double whammy in terms of poor performance: added time to make a successful font request, and more data to download.&lt;/p&gt;&lt;p&gt;So how can you check for this? Well, again I&#39;d look to the browser console. If you notice &lt;code&gt;404&lt;/code&gt; errors for fonts, I&#39;d put those errors pretty high up your prioritisation list to fix!&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;So in summary, when you are preloading fonts make sure that what is preloaded matches with the &lt;code&gt;src&lt;/code&gt; defined in your &lt;code&gt;@font-face&lt;/code&gt; rules. Remember the first &lt;code&gt;src&lt;/code&gt; the browser finds wins. If these don&#39;t match then your users may be downloading two sets of exactly the same fonts. Make sure you check your browser console for &lt;code&gt;preload&lt;/code&gt; warnings and font &lt;code&gt;404&lt;/code&gt; errors. As both of these issues can seriously impact page performance and adding to the data requirements for the site.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;23/01/21: Initial post published. Thanks to &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for the HTTP Archive data and &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for the Cloudflare Worker debugging.&lt;/li&gt;&lt;li&gt;24/01/21: Added more detail about the performance impact of fonts failing to load, thanks to &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for the &lt;a href=&quot;https://twitter.com/tunetheweb/status/1352513595754610690?s=21&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;scenario&lt;/a&gt;. Added links for more information about Cloudflare workers. Added info about USDS having no involvement in the White House website. Added a link to the gist with a sample of the cloudflare worker code I used.&lt;/li&gt;&lt;li&gt;29/01/21: Added update to say the issue with fonts has now been fixed.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>How to run a WebPageTest test</title>
    <link href="https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/" />
    <updated>2020-12-31T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Table of contents&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#simple-testing-tab&quot;&gt;Simple testing tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#url-input-field&quot;&gt;URL input field&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#test-configuration&quot;&gt;Test Configuration&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#include-repeat-view&quot;&gt;Include Repeat View&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#run-lighthouse-audit&quot;&gt;Run Lighthouse Audit&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#running-a-test&quot;&gt;Running a test&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#advanced-testing-tab&quot;&gt;Advanced Testing tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#url-input-field-1&quot;&gt;URL input field&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#test-location&quot;&gt;Test Location&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#browser&quot;&gt;Browser&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#advanced-settings-dropdown&quot;&gt;Advanced Settings dropdown&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#test-settings-tab&quot;&gt;Test Settings tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#connection&quot;&gt;Connection&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#number-of-tests-to-run&quot;&gt;Number of Tests to Run&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#repeat-view&quot;&gt;Repeat View&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-video&quot;&gt;Capture Video&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#label&quot;&gt;Label&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#advanced-tab&quot;&gt;Advanced tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#stop-test-at-document-complete&quot;&gt;Stop Test at Document Complete&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#disable-javascript&quot;&gt;Disable JavaScript&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#clear-ssl-certificate-caches&quot;&gt;Clear SSL Certificate Caches&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#ignore-ssl-certificate-errors&quot;&gt;Ignore SSL Certificate Errors&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#disable-compatibility-view-ie-only&quot;&gt;Disable Compatibility View (IE Only)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-network-packet-trace-tcpdump&quot;&gt;Capture network packet trace (tcpdump)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#save-response-bodies&quot;&gt;Save response bodies&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#preserve-original-user-agent-string&quot;&gt;Preserve original User Agent string&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#user-agent-string&quot;&gt;User Agent String&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#append-to-ua-string&quot;&gt;Append to UA String&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-full-size-video&quot;&gt;Capture Full Size Video&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#minimum-test-duration&quot;&gt;Minimum test duration&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#custom-headers&quot;&gt;Custom headers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#inject-script&quot;&gt;Inject Script&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#chromium-tab&quot;&gt;Chromium tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-lighthouse-report&quot;&gt;Capture Lighthouse Report&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#emulate-mobile-browser&quot;&gt;Emulate Mobile Browser&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-dev-tools-timeline&quot;&gt;Capture Dev Tools Timeline&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-v8-runtime-call-stats&quot;&gt;Capture V8 Runtime Call Stats&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-chrome-trace-abouttracing&quot;&gt;Capture Chrome Trace (about://tracing)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#trace-categories&quot;&gt;Trace Categories&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-network-log&quot;&gt;Capture Network Log&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#enable-data-reduction&quot;&gt;Enable Data Reduction&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#host-resolver-rules&quot;&gt;Host Resolver Rules&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#command-line&quot;&gt;Command-line&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#auth-tab&quot;&gt;Auth tab&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#script-tab&quot;&gt;Script tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#script-includes-sensitive-data&quot;&gt;Script includes sensitive data&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#discard-all-http-headers&quot;&gt;Discard all HTTP headers&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#block-tab&quot;&gt;Block tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#block-requests-containing&quot;&gt;Block Requests Containing&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#block-domains&quot;&gt;Block Domains&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#spof-tab&quot;&gt;SPOF tab&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#custom-tab&quot;&gt;Custom tab&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#custom-metrics&quot;&gt;Custom Metrics&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#hidden-settings&quot;&gt;Hidden settings&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#bulk-testing-tab&quot;&gt;Bulk Testing Tab&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#hidden-input-fields&quot;&gt;Hidden input fields&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#visual-comparison-tab&quot;&gt;Visual comparison tab&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#traceroute-tab&quot;&gt;Traceroute tab&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#common-scenarios&quot;&gt;Common Scenarios&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#testing-http2-against-http11&quot;&gt;Testing HTTP/2 against HTTP/1.1&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#testing-a-site-without-third-party-scripts-blocked&quot;&gt;Testing a site without third-party scripts blocked&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#useful-tips--tricks&quot;&gt;Useful tips &amp; tricks&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#how-do-i-see-all-the-wpt-advanced-settings&quot;&gt;How do I see all the WPT advanced settings?&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In this post I&#39;m going to examine all the individual settings from the homepage of &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest (WPT)&lt;/a&gt;. You could consider this blog post a prequel to my two other blog posts related to WebPageTest: &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;&#39;How to read a WebPageTest Waterfall View chart&#39;&lt;/a&gt; and &lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/&quot;&gt;&#39;How to read a WebPageTest Connection View chart&#39;&lt;/a&gt;. Both blog posts give you information on how to interpret the results of the tests you run off the back of what I&#39;m about to show you in this post.&lt;/p&gt;&lt;h2 id=&quot;simple-testing-tab&quot;&gt;Simple testing tab&lt;/h2&gt;&lt;p&gt;The first section I&#39;ll cover goes against the WPT ordering of the option tabs. The first tab is &#39;Advanced Testing&#39;, but I&#39;m going to introduce the &#39;Simple Testing&#39; tab first just so we cover some basic concepts before diving into the finer details.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The simple testing tab allows you to quickly start testing without the need to delve into the more advanced configuration options.&quot; decoding=&quot;async&quot; height=&quot;231&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/sCN348rOGj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/sCN348rOGj-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/sCN348rOGj-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/sCN348rOGj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Most of this is probably pretty self-explanatory, but some of the details will help you understand how WPT works.&lt;/p&gt;&lt;h3 id=&quot;url-input-field&quot;&gt;URL input field&lt;/h3&gt;&lt;p&gt;Short and sweet, this is the URL you want WPT to examine. For the &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;public instance&lt;/a&gt; of WPT this URL will need to be publicly accessible on the internet. If you need to test private sites that are on an internal network, or behind a basic HTTP Basic authentication there are options that I will cover later in the post.&lt;/p&gt;&lt;h3 id=&quot;test-configuration&quot;&gt;Test Configuration&lt;/h3&gt;&lt;p&gt;When you select the &#39;Simple Testing&#39; tab almost all the custom configuration options are preselected for you. The dropdown gives you a set of options for you to choose from:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Mobile - Slow 3G (400 Kbps download, 400 Kbps upload, 400ms Latency)&lt;/li&gt;&lt;li&gt;Mobile - Regular 3G (1.6 Mbps download, 768 Kbps upload, 300ms Latency)&lt;/li&gt;&lt;li&gt;Mobile - Fast 3G (1.6 Mbps download, 768 Kbps upload, 150ms Latency)&lt;/li&gt;&lt;li&gt;Mobile - 4G (9 Mbps download, 9 Mbps upload, 170ms Latency)&lt;/li&gt;&lt;li&gt;Desktop (5 Mbps download, 5 Mbps upload, 28ms Latency)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The mobile devices available are a Motorola G4, and all test using the default Chrome browser. The desktop device is a virtual machine (VM) running Chrome. The Moto G4&#39;s are real devices all currently located in Pat Meenan&#39;s basement in Dulles, Virginia (not a joke, 100% true).&lt;/p&gt;&lt;p&gt;As for the different connection speeds available and how that works, a whole separate blog post could be written about this topic. Pat describes the setup in the &lt;a href=&quot;https://web.archive.org/web/20210121185916/https://www.webpagetest.org/forums/showthread.php?tid=13071&amp;pid=23952#pid23952&quot;&gt;forum here&lt;/a&gt;, and in the &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/master/dev/System%20Design/mobile_testing.md&quot;&gt;WPT documentation here&lt;/a&gt;. These resources are a few years old now so a more up to date TL;DR; is that most phones reverse-tether their networking through a Raspberry Pi where &lt;a href=&quot;https://man7.org/linux/man-pages/man8/tc-netem.8.html&quot;&gt;netem&lt;/a&gt; is used for traffic shaping. Quoting from Pat in the forum post below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;FWIW, we did a LOT of testing at Google with actual networks and dummynet. With dummynet we could simulate a given set of conditions just about exactly (with more consistency) but it&#39;s worth noting that it&#39;s exactly that - one set of conditions. In the case of the default 3G profile I have set up right now, it&#39;s basically modeled around the 60-70th percentile of measured connections globally so it doesn&#39;t match an exact market - some will be faster and some will be slower.&lt;/p&gt;&lt;p&gt;The main things that you can&#39;t duplicate with traffic shaping are:&lt;/p&gt;&lt;p&gt;Radio start-up delay - given a few seconds of idle, most phones will shift the radio into low power modes and it can take anywhere from a few dozen ms to several seconds before the radio comes back and is able to transmit data. It&#39;s more important for multi-page navigations where the radio can turn off between each step but it could also add completely dead time to the beginning of a page load (but that case is completely out of the control of the site owner). In really extreme cases it&#39;s possible for the radio to turn off while waiting for a server response - but if your TTFB is that long you have serious issues to look into anyway.&lt;/p&gt;&lt;p&gt;Network architecture - on traffic-shaped WiFi you will still be going out through your normal ISP&#39;s network (Verizon Fios in the case of Dulles). You won&#39;t go through any carrier proxies, deal with them routing the traffic back to a central egress point or coming from a Carrier&#39;s mobile IP block in case there is any server logic tied to it.&lt;/p&gt;&lt;p&gt;The agents themselves work perfectly fine on actual networks and the test results come back over the wired connection from the tethered host that is driving the agents so the data use is just for the actual browsing and isn&#39;t really why I chose to use WiFi. The main issue is the insane variability using an actual carrier network. The performance varies quite significantly from day to day and even within a day which makes it REALLY hard to compare results to each other.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Old WPT documentation from Pat can be found in this &lt;a href=&quot;https://web.archive.org/web/20170220014223/https://sites.google.com/a/webpagetest.org/docs/system-design/mobile-testing&quot;&gt;link here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The traffic shaping models come from real world connection data. For example the 3G profile is based around the 60-70th percentile of measured connections globally in 2014. Latencies haven&#39;t changed much since then so they are still relevant today. It&#39;s worth noting that these are approximations. Some connections real users experience will be quicker, others will be slower.&lt;/p&gt;&lt;p&gt;The whole purpose of this dropdown is to give you an idea of what a user with the selected device and connection speed will experience when loading the URL you want to test.&lt;/p&gt;&lt;h3 id=&quot;include-repeat-view&quot;&gt;Include Repeat View&lt;/h3&gt;&lt;p&gt;When WPT first loads your URL, the browser has what is known as a &#39;cold cache&#39;. All assets that are needed must be downloaded. By enabling this checkbox the test will run twice on the defined page (browser closing between test runs). This is a great way to test the caching strategy for the page being tested, as the second run will be using a &#39;warm cache&#39;. So in theory if the page has an efficient caching strategy configured for the page assets, very little will need to be downloaded on the second run.&lt;/p&gt;&lt;h3 id=&quot;run-lighthouse-audit&quot;&gt;Run Lighthouse Audit&lt;/h3&gt;&lt;p&gt;Again, quite self-explanatory. As well as running a WPT audit across the URL, a separate audit will be completed using &lt;a href=&quot;https://developers.google.com/web/tools/lighthouse&quot;&gt;Lighthouse&lt;/a&gt;. This separate audit will use &#39;3G Fast&#39; as the connection speed (as this is the default in Lighthouse tests), and any connection settings selected via the dropdown will be ignored. This Lighthouse report will be run on an emulated Moto G4 with 4x simulated CPU slowdown.&lt;/p&gt;&lt;h2 id=&quot;running-a-test&quot;&gt;Running a test&lt;/h2&gt;&lt;p&gt;Now we&#39;ve gone over what everything in the simple testing tab is, it&#39;s time to run a test. Simply enter the URL to be tested, select your options and press the big yellow &#39;Start Test&#39; button to the right.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Enter the test details into the tab and click the &#39;start test&#39; button to the right to kick off the testing.&quot; decoding=&quot;async&quot; height=&quot;264&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/77CvZNdr4V-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/77CvZNdr4V-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/77CvZNdr4V-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/77CvZNdr4V-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The next screen you will see will look like below. Giving you details about the URL being tested, the test instance being used, number of test runs (more on this later), and the amount of time the test has been running.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Test started screen with the distinctive arrows telling you which stage of the test is currently running.&quot; decoding=&quot;async&quot; height=&quot;256&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/kcVsZbSDqf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/kcVsZbSDqf-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/kcVsZbSDqf-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/kcVsZbSDqf-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now the time it will take to complete the test depends on the test settings selected. So for example, a &#39;Slow 3G&#39; setting is going to take much longer to load a page than &#39;4G&#39;. But don&#39;t worry, you don&#39;t have to keep the page open while the test runs. Tests run completely independently of your web browser. You can return to the test later via the same test URL (or the &#39;Test History&#39; link in the main navigation).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: At this point I highly recommend signing up for a WPT account. If you plan on doing a lot of testing WPT will save all of your tests so you can refer to them again at a later date should you need to.&lt;/p&gt;&lt;p&gt;Once the testing is complete you will see a result that looks similar to below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Test completed page with lots of stats and information to dig through.&quot; decoding=&quot;async&quot; height=&quot;359&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/cfgsH8lK51-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/cfgsH8lK51-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/cfgsH8lK51-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/cfgsH8lK51-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The returned test results are immutable and can&#39;t be modified once saved (or even deleted). If you scroll further down the page you&#39;ll be presented with a huge amount of interesting web performance data for you to dig through. The information on this page is beyond the scope of this article, but if you are interested in knowing more about the resulting waterfall charts, I have a blog post all about them &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;here&lt;/a&gt;. For the similar looking but different connection view chart, I have a blog post &lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/&quot;&gt;here&lt;/a&gt; you can read.&lt;/p&gt;&lt;h2 id=&quot;advanced-testing-tab&quot;&gt;Advanced Testing tab&lt;/h2&gt;&lt;p&gt;So now we&#39;ve covered some of the core concepts of how to run a simple WPT test, let&#39;s get stuck into the more advanced (and interesting) settings you can modify. The bulk of the rest of this article will be spent in this section.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The advanced tab gives us a lot more options play with, including granular control over the location and browser used in the test.&quot; decoding=&quot;async&quot; height=&quot;430&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/7jctovgqV--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/7jctovgqV--300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/7jctovgqV--600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/7jctovgqV--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Again there are some self-explanatory fields, but I will cover them here for completeness.&lt;/p&gt;&lt;h3 id=&quot;url-input-field-2&quot;&gt;URL input field&lt;/h3&gt;&lt;p&gt;This is identical to the &#39;Simple Testing&#39; explanation given above.&lt;/p&gt;&lt;h3 id=&quot;test-location&quot;&gt;Test Location&lt;/h3&gt;&lt;p&gt;This is one of the major differences from the &#39;Simple Testing&#39; setup. Under &#39;Advanced Testing&#39; you are given the options of the type of device you wish to use for testing, and geographically where in the world the test is run from. So for example you can choose from a set of real (not simulated) Android or iOS devices (all located in Dulles, Virginia). You are also given the option to choose from many test locations dotted across the globe. There are way too many to list here, but you can find detailed information about each location on the &lt;a href=&quot;https://www.webpagetest.org/getLocations.php?f=html&quot;&gt;Location Status page&lt;/a&gt;. This page is also a great way to spot the busy locations and avoid them should you want your test be added to a shorter queue.&lt;/p&gt;&lt;p&gt;Now you may be asking why test location is important. Well depending on your users location in relation to your origin server (assuming a CDN isn&#39;t used), this latency will have a big difference on the overall page performance. There&#39;s actually a whole chapter dedicated to this topic in &lt;a href=&quot;https://hpbn.co/primer-on-latency-and-bandwidth/#speed-of-light-and-propagation-latency&quot;&gt;High Performance Browser Networking&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/igrigorik&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Ilya Grigorik&lt;/a&gt;. So which location should you choose? Well if you have any analytics data available for the site you want to test, it would be worth checking where the majority of your users are browsing from. If 90% of your users are coming from a particular country, then it would make sense to choose a test location in (or as close to) the country as possible. This will give you a more accurate representation of how most of &lt;em&gt;your&lt;/em&gt; users experience your website.&lt;/p&gt;&lt;h3 id=&quot;browser&quot;&gt;Browser&lt;/h3&gt;&lt;p&gt;The next setting to choose is the browser you want to test your site with. Now these will change depending on the device / location you choose in the &#39;Test Location&#39; dropdown above it. This is another setting that can be driven by your user data if you know the most popular browser visiting your site. Or if you are more interested in how different browsers handle specific server / page setups, this is a great way to directly compare them using the huge amount of data returned from each of the tests. Most of the time you may only be interested in how your site performs on a Chromium based browser (since they are &lt;a href=&quot;https://www.w3counter.com/globalstats.php&quot;&gt;pretty popular&lt;/a&gt;!). If so, go right ahead and choose Chrome.&lt;/p&gt;&lt;h2 id=&quot;advanced-settings-dropdown&quot;&gt;Advanced Settings dropdown&lt;/h2&gt;&lt;p&gt;Beyond this point is where the number of configuration options ramps up rapidly. Each of the tabs beneath this dropdown has its own set of options to configure independently.&lt;/p&gt;&lt;h3 id=&quot;test-settings-tab&quot;&gt;Test Settings tab&lt;/h3&gt;&lt;p&gt;This tab contains most of the &#39;high-level&#39; settings for the test. Some of these will look familiar to areas covered above in the &#39;Simple Testing&#39; tab.&lt;/p&gt;&lt;h4 id=&quot;connection&quot;&gt;Connection&lt;/h4&gt;&lt;p&gt;Pretty obvious really: what connection speed do you wish to replicate when the test is being run. I covered a fair amount of how this works in the &#39;Test Configuration&#39; section above. But this time there are many more options available for you to choose from. A full list of options as well as the bandwidth and latency values available can be &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/blob/26e3cf061c2395c949e125a155647db08eaf18b5/www/settings/connectivity.ini.sample&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;It&#39;s worth noting that you should always select some form of bandwidth shaping from the options. &#39;Native Connection (No traffic shaping)&#39; is an option, but the results returned won&#39;t really be comparable from test run to test run over time. It&#39;s likely to be an incredible quick connection, so not representative of most of your users. Also, as the amount of traffic changes from hour to hour, it&#39;s likely that the true download / upload will differ depending on how busy the network is at that point time. By selecting some form of traffic shaping you can be sure that this variable is consistent across test runs.&lt;/p&gt;&lt;h4 id=&quot;number-of-tests-to-run&quot;&gt;Number of Tests to Run&lt;/h4&gt;&lt;p&gt;When you enter a URL for WPT to test it doesn&#39;t only test the URL once, it will run the same test multiple times. This is because network latency and traffic can have a huge effect on how a website performs, and these variables are constantly changing. In order to have a more accurate (and representative) result, the test should be run multiple times. For example, should you run a single test run that has very little traffic across the network at that point in time, the resulting performance data will come back very positive. Whereas in reality the network isn&#39;t always like this. So by repeating the test WPT is sampling these variables at different points in time and looking for a happy median.&lt;/p&gt;&lt;p&gt;Note that between each standard test run no cache is shared (unless &#39;repeat view&#39; is selected), so each time WPT visits the page it is starting from a &#39;cold cache&#39;. On the public instance of WPT you can run a maximum of 9 test runs. If you set up your own &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Private%20Instances/README.md&quot;&gt;private instance&lt;/a&gt; you can run as many runs as you choose. You should always select an odd number of test runs due to the fact that WPT will select the median result once the test has completed. An odd number of runs allows WPT to easily select the run that sits in the middle in terms of performance.&lt;/p&gt;&lt;h4 id=&quot;repeat-view&quot;&gt;Repeat View&lt;/h4&gt;&lt;p&gt;This was covered in the &#39;Include Repeat View&#39; above. But it&#39;s worth mentioning that if you select &#39;repeat view&#39;, this will be doubling the number of tests run against that particular page. For example, say you have 9 runs set: each run tests with a first view (cold cache), then again with a repeat view (warm cache). So the page will be visited 18 times. The median run for each first view and repeat view is then highlighted in the &#39;Performance Results&#39;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What metric is used to select the median run?&lt;/strong&gt; By default WPT will use the &#39;loadTime&#39; metric, but you can also use the &#39;SpeedIndex&#39; metric to perform this comparison too. Simply append &lt;code&gt;?medianMetric=SpeedIndex&lt;/code&gt; to your test result URL and you will see the &#39;Performance Results&#39; summary change. In the examples below, the top image is using &#39;SpeedIndex&#39;, the bottom image is using &lt;code&gt;LoadTime&lt;/code&gt;. Notice how the run selected for the &#39;repeat view&#39; median changes from 3 to 2.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Performance results summary using the speed index to select the median run.&quot; decoding=&quot;async&quot; height=&quot;177&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QVJDC8jj0Y-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QVJDC8jj0Y-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QVJDC8jj0Y-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QVJDC8jj0Y-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Performance results summary using the load time to select the median run.&quot; decoding=&quot;async&quot; height=&quot;171&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n2IyAptcXX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n2IyAptcXX-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n2IyAptcXX-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n2IyAptcXX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h4 id=&quot;capture-video&quot;&gt;Capture Video&lt;/h4&gt;&lt;p&gt;Very simple: do you want to capture a video of the page loading under the conditions you have selected?&lt;/p&gt;&lt;p&gt;Why is this useful? Well, a really important aspect of web performance is the perceived performance a user experiences. If a site loads completely in 8 seconds, but it goes from a blank screen to fully rendered over the space of a single frame, a users perceived performance isn&#39;t going to be as good as if the site loads more incrementally over the same time period. If you can get the first pixels rendered to the screen within the first couple of seconds or so, it gives a user &lt;em&gt;something&lt;/em&gt; to look at. The user perceives that something is happening during this loading period, giving them the perception that the page is loading quicker.&lt;/p&gt;&lt;p&gt;The &#39;Capture video&#39; setting allows you to examine the page load frame by frame and also exactly how the browser is rendering the page. This is incredibly useful when you come to comparing results. WPT has the ability to render multiple video&#39;s side by side from different tests, thus allowing you to demonstrate exactly what difference your changes to the site made (both good and bad!).&lt;/p&gt;&lt;h4 id=&quot;label&quot;&gt;Label&lt;/h4&gt;&lt;p&gt;The last input field for this tab is a free text area allowing you to give a particular test a user friendly label. Should you wish to compare tests before and after a particular set of changes, this is a good way to remind yourself at a later date. Note that this is an optional field and it can be added (or modified) once the test has completed. This is very useful when it comes to comparing tests via the &#39;Filmstrip view&#39;, as can be seen in this &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#what-do-the-filmstrip-thumbnail-border-colours-signify&quot;&gt;section here&lt;/a&gt;&lt;/p&gt;&lt;h3 id=&quot;advanced-tab&quot;&gt;Advanced tab&lt;/h3&gt;&lt;p&gt;This tab gives us a whole bunch of exciting advanced settings to play with.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Image of the advanced tab with all the settings displayed.&quot; decoding=&quot;async&quot; height=&quot;942&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/d7DHpLENwY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/d7DHpLENwY-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/d7DHpLENwY-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/d7DHpLENwY-802.webp 802w&quot; width=&quot;802&quot;&gt;&lt;/p&gt;&lt;p&gt;I&#39;ll go over each of them individually.&lt;/p&gt;&lt;h4 id=&quot;stop-test-at-document-complete&quot;&gt;Stop Test at Document Complete&lt;/h4&gt;&lt;p&gt;When a WPT run completes it will usually wait for the &#39;Document Complete&#39; event from the browser to fire and all network activity over the network to go quiet for 2 seconds. At this point WPT considers the page to have completely loaded. But &lt;a href=&quot;https://web.archive.org/web/20210119130225/https://www.webpagetest.org/forums/showthread.php?tid=9&amp;pid=30&quot;&gt;under certain conditions this may not always happen&lt;/a&gt;. For example, old versions of Internet Explorer didn&#39;t fire the &lt;code&gt;onload&lt;/code&gt; event when it was still waiting for &lt;em&gt;something&lt;/em&gt; to download. So the test would simply time out after 2 minutes waiting for this to happen. To avoid this you can check the &#39;Stop Test at Document Complete&#39; checkbox. This will force the test to finish once the document is complete (&lt;code&gt;onload&lt;/code&gt; event has fired). This point is represented by a &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#document-complete---blue-line-&quot;&gt;blue vertical line&lt;/a&gt; on the WPT waterfall chart.&lt;/p&gt;&lt;h4 id=&quot;disable-javascript&quot;&gt;Disable JavaScript&lt;/h4&gt;&lt;p&gt;Very easy option to explain. Simply disable JavaScript in the browser that is running the test. Not to get too preachy, but this is a great way to test what your users will see when the JavaScript for a site fails in some way (e.g. network error, blocked, malformed code etc). Remember the use of the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement&quot;&gt;Progressive Enhancement&lt;/a&gt; methodology is a great way to give users in this situation a simplified experience (not just a blank page).&lt;/p&gt;&lt;h4 id=&quot;clear-ssl-certificate-caches&quot;&gt;Clear SSL Certificate Caches&lt;/h4&gt;&lt;p&gt;This is a setting that is useful when the browser is being run on a Windows based instance. Windows by default doesn&#39;t clear certificates. When intermediate certificates already exist in the cache this can prompt a browser to hide certificate checks (Chrome with EV certs, and Internet Explorer (IE)). Around the end of 2012 WPT &lt;a href=&quot;https://web.archive.org/web/20200924134347/https://www.webpagetest.org/forums/showthread.php?tid=12109&amp;pid=20445&quot;&gt;changed the default behaviour&lt;/a&gt; for clearing the OS certificate caches which caused SSL negotiation times to increase and skewed the results (since it was clearing all the certificates including root providers). In this case IE was having to validate root certificates too, which takes time. Since there&#39;s no way to clear only leaf certificates, the option to clear all SSL certificate caches was moved to the advanced options to be run on a per-test basis and disabled by default. This isn&#39;t an issue for linux instances because there is no certificate caching. I have a whole post about the &lt;a href=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/&quot;&gt;impact of SSL certificate revocation on web performance&lt;/a&gt; should you be interested. Quoting what Pat said in the forum post below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;With the 2.10 release I changed the default behavior for clearing the OS certificate caches.&lt;/p&gt;&lt;p&gt;Somewhere around 6 months ago I started clearing the OS certificate caches which cased SSL negotiation times to go up significantly for the IE agents that were running on Windows Vista or later (IE on XP does not do CRL/OCSP checks).&lt;/p&gt;&lt;p&gt;There was quite a bit of push-back because it was clearing all of the certificates, including the certs from the root providers so IE was validating things like verisign&#39;s root cert which is something that will hardly ever happen to end users.&lt;/p&gt;&lt;p&gt;The real world lies somewhere in between but I don&#39;t have a way to just clear the leaf certificates and it was skewing site&#39;s performance enough that it was hiding other issues so I made it an option on a per-test basis and it is disabled by default (it&#39;s available in the advanced settings if you want to re-enable it for a given test).&lt;/p&gt;&lt;/blockquote&gt;&lt;h4 id=&quot;ignore-ssl-certificate-errors&quot;&gt;Ignore SSL Certificate Errors&lt;/h4&gt;&lt;p&gt;Sometimes a browser will show a SSL certificate error if the certificate chain is broken, or a certificate simply doesn&#39;t exist. Possibly caused by differences in the root certificate lists or other browser-specific policies. Another reason could be if you are running a private WPT instance to test a domain that doesn&#39;t have correct SSL certificates configured. In these instances you can check the &#39;Ignore SSL Certificate Errors&#39; checkbox to bypass this issue. &lt;strong&gt;Note&lt;/strong&gt;: This &lt;a href=&quot;https://web.archive.org/web/20210116124520/https://www.webpagetest.org/forums/showthread.php?tid=15551&amp;pid=31299#pid31299&quot;&gt;doesn&#39;t work in Firefox&lt;/a&gt; as Firefox has disabled &lt;em&gt;any&lt;/em&gt; way to ignore these types of errors for security reasons. So it may only be a setting that works in IE and Chrome.&lt;/p&gt;&lt;p&gt;The quote from the forum post is below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Firefox disabled any way to ignore cert errors as a security measure so it no longer works for Firefox (at this point it may be Chrome and IE-only). The legacy Windows agent used to force it by intercepting the API calls internal to Firefox but that is no longer possible with the cross-platform agent.&lt;/p&gt;&lt;p&gt;The only way to do it now for Firefox is to run a private instance of WPT and install the certs (or a custom root) on the test machines so they see the self-signed certs as valid.&lt;/p&gt;&lt;/blockquote&gt;&lt;h4 id=&quot;disable-compatibility-view-ie-only&quot;&gt;Disable Compatibility View (IE Only)&lt;/h4&gt;&lt;p&gt;There&#39;s &lt;a href=&quot;https://en.wikipedia.org/wiki/Quirks_mode&quot;&gt;a lot of history&lt;/a&gt; behind this setting. For web developers who worked on sites through this time (myself included) it will probably bring back frustrating memories. A TL;DR; is basically in order for newer versions of IE to not &#39;break the web&#39;, the browser implemented a feature that allowed it to mimic older versions (e.g. make IE8 render like IE7 did). This was needed because of certain IE specific coding practices that existed and were very popular. This setting forces IE to load in &#39;Strict mode&#39; no matter what the &lt;code&gt;DOCTYPE&lt;/code&gt; or &lt;code&gt;http-equiv=&quot;X-UA-Compatible&quot;&lt;/code&gt; meta tag says. (Thankfully) it is very unlikely you will need this setting on the modern web.&lt;/p&gt;&lt;h4 id=&quot;capture-network-packet-trace-tcpdump&quot;&gt;Capture network packet trace (tcpdump)&lt;/h4&gt;&lt;p&gt;Although WPT can give you a lot of information about the order in which a page loaded as well as all the intricacies involved in this process, sometimes you need to dig a little deeper into the low level details. This is where capturing a network packet trace comes in extremely useful. To do this WPT uses a command-line tool called &lt;a href=&quot;https://www.tcpdump.org/&quot;&gt;tcpdump&lt;/a&gt;. What this tool does is allow us to display TCP/IP and other packets being transmitted or received over the network for the instance that is running the test. If checked WPT will save what is known as a Packet CAPture (PCAP) file that can be downloaded on the test result page:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Once selected you get the option to download the PCAP file in the results.&quot; decoding=&quot;async&quot; height=&quot;273&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2m9BggRfw1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2m9BggRfw1-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2m9BggRfw1-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2m9BggRfw1-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The great thing about packet level capture when done via a WPT instance is the isolation. The only network traffic that is being captured is solely related to the page being tested. For example, if you were to run a capture on your local machine you&#39;d most likely have to filter so much data due to all the other processes running on the machine in the background.&lt;/p&gt;&lt;p&gt;So what can you do with this PCAP file once you have it? Well, you can use &lt;code&gt;tcpdump -r [capture_filename]&lt;/code&gt; to read in the actual file to your local version of &lt;code&gt;tcpdump&lt;/code&gt; and &lt;a href=&quot;https://danielmiessler.com/study/tcpdump/&quot;&gt;examine the internals using the CLI&lt;/a&gt;. Or if you are after a GUI interface then it&#39;s hard to beat &lt;a href=&quot;https://www.wireshark.org/&quot;&gt;Wireshark&lt;/a&gt;. If you are decoding HTTPS traffic you will also need the TLS Key Log file. Follow the instructions under the &#39;Load your data in Wireshark&#39; header from &lt;a href=&quot;https://vanwilgenburg.wordpress.com/2015/11/22/how-to-capture-and-decode-http2-traffic-with-wireshark/&quot;&gt;this page&lt;/a&gt; on how to decode secure traffic. If you are looking to capture traffic from iOS devices then you are going to need to go through quite a few other steps that &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; covers in his &lt;a href=&quot;https://andydavies.me/blog/2019/12/12/capturing-and-decrypting-https-traffic-from-ios-apps/&quot;&gt;&#39;Capturing and Decrypting HTTPS Traffic From iOS Apps Using Frida&#39;&lt;/a&gt; blog post.&lt;/p&gt;&lt;p&gt;A really quick way to open Wireshark and not worry about all the settings, grab the PCAP and the TLS keylog file and run the following command:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;wireshark -r your-pcap-file.pcap -o tls:keylog_file:your-keylog-file.keylog&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Word of warning, Wireshark is an incredibly powerful (and complex) bit of software with a steep learning curve associated with it. But once mastered, it unlocks a whole new world of data for you to examine.&lt;/p&gt;&lt;h4 id=&quot;save-response-bodies&quot;&gt;Save response bodies&lt;/h4&gt;&lt;p&gt;By default when a WPT test runs the response bodies of the requests are discarded once downloaded. This is fine for most cases, but what about if you wanted to store these bodies for future reference. Say you were making a change to a pages &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag in order to improve performance. Wouldn&#39;t it be great to be able to view that HTML in the future once you knew which version performed best. This is exactly what this option does. You could almost consider it &#39;view source&#39; functionality for WPT tests.&lt;/p&gt;&lt;p&gt;It&#39;s worth nothing that WPT only does this with text based responses (HTML, CSS, JS etc). Images and other binary formats aren&#39;t stored in this process. There currently isn&#39;t an option to enable this functionality, even when using your own private instance, but Pat has mentioned that it wouldn&#39;t be too hard to add if there were more requests for it in the future.&lt;/p&gt;&lt;h4 id=&quot;preserve-original-user-agent-string&quot;&gt;Preserve original User Agent string&lt;/h4&gt;&lt;p&gt;One issue running WPT against a live site is that it will make impressions on your analytics data. Since to an analytics tool a visit from a WPT browser looks like any other browser visiting the site. For that reason WPT appends a PTST string to the User-Agent (UA) string. A random example of this is given below:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 PTST/201209.031210&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Using the &#39;PTST&#39; string you can easily filter out any data with it in the &lt;code&gt;user-agent&lt;/code&gt; header. Now you may be asking why PTST? What does it stand for? Well, it actually stand for PageTeST, which was the name of the IE plugin version of WPT before there was a web interface in the version we see today.&lt;/p&gt;&lt;h4 id=&quot;user-agent-string&quot;&gt;User Agent String&lt;/h4&gt;&lt;p&gt;This setting allows you to completely override the UA string should you wish to do so. For example if I enter &#39;my-custom-ua-string&#39; into the input box, the header for every request will be:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;User-Agent: my-custom-ua-string PTST/201223.161206&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now if you wanted a completely clean UA string without the PTST information you&#39;d simply check the &#39;Preserve original User Agent string&#39; checkbox as well. That will give you the following output:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;User-Agent: my-custom-ua-string&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This functionality isn&#39;t supported by all browsers. For example it doesn&#39;t work in IE11 and Edge. It does work in Chrome (including mobile device versions), Firefox, and Safari on iOS.&lt;/p&gt;&lt;h4 id=&quot;append-to-ua-string&quot;&gt;Append to UA String&lt;/h4&gt;&lt;p&gt;This input box allows you to append a custom string to the end of the UA string in the file requests from the chosen test browser. This advanced functionality was added off the back of a request by &lt;a href=&quot;https://twitter.com/rick_viscomi&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rick Viscomi&lt;/a&gt; that can be &lt;a href=&quot;https://web.archive.org/web/20210118072425/https://www.webpagetest.org/forums/showthread.php?tid=14152&amp;pid=27433&quot;&gt;seen here&lt;/a&gt;. Additional functionality was added in this request that allows the test ID (and other details) to be appended to the UA string when you run a test via the API. This allows you to extract the test URL from your logs in the logging software you use. See &lt;a href=&quot;https://web.archive.org/web/20210118072425/https://www.webpagetest.org/forums/showthread.php?tid=14152&amp;pid=27433&quot;&gt;this thread&lt;/a&gt; for more details on how to do this.&lt;/p&gt;&lt;h4 id=&quot;capture-full-size-video&quot;&gt;Capture Full Size Video&lt;/h4&gt;&lt;p&gt;By default WPT will capture a video of the page load (assuming the &#39;Capture Video&#39; checkbox is checked in the &#39;Test Settings&#39; tab). But it won&#39;t capture the full size video. By default it captures a video which is scaled down by approximately 50%. In my simple test using Chrome:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Default: 408px x 360px&lt;/li&gt;&lt;li&gt;Full: 904px x 680px&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The dimensions of the video captured depend on what device the test is running on. For example, running a test on a mobile device will produce a video with mobile phone dimensions.&lt;/p&gt;&lt;p&gt;This setting is extremely useful if you are looking to examine the finer visual details of how a page is loading, or if you are wanting to show the video in a presentation.&lt;/p&gt;&lt;p&gt;What this setting also unlocks is the full size images in the &#39;filmstrip view&#39;. Selecting &#39;Huge&#39; will give you a filmstrip full of images where the largest side is 600px. You can further increase their size to the full native width if you modify the &lt;code&gt;thumbSize&lt;/code&gt; query parameter in the URL for the test.&lt;/p&gt;&lt;h4 id=&quot;minimum-test-duration&quot;&gt;Minimum test duration&lt;/h4&gt;&lt;p&gt;As mentioned earlier in the &#39;Stop Test at Document Complete&#39; explanation, by default WPT will stop a test once the &#39;Document Complete&#39; event has fired and the network has gone quiet for 2 seconds. But there may be some instances where you want to force a test to run for a minimum number of seconds. A use case where I&#39;ve used this setting is in &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-test-a-404-pages-web-performance&quot;&gt;testing the web performance of a 404 page&lt;/a&gt;. Without this setting WPT would see the 404 status code and immediately stop the test. Another example where you may use this setting is for long running XHR requests. If they take longer than 2 seconds the response will never be captured.&lt;/p&gt;&lt;h4 id=&quot;custom-headers&quot;&gt;Custom headers&lt;/h4&gt;&lt;p&gt;This textarea allows you to add custom headers to &lt;strong&gt;all&lt;/strong&gt; requests made by the browser to the server. This can be useful when it comes to various debugging scenarios. You add each custom header on a newline:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Add custom headers to every request that the browser makes to the server. Make sure to add each on a newline.&quot; decoding=&quot;async&quot; height=&quot;144&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8pVlDxWqA2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8pVlDxWqA2-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8pVlDxWqA2-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8pVlDxWqA2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;For example these request headers could be used to prevent the test agent from being blocked by a server, or another way to exclude tests from your analytics data.&lt;/p&gt;&lt;h4 id=&quot;inject-script&quot;&gt;Inject Script&lt;/h4&gt;&lt;p&gt;The inject script textarea is an incredibly useful tool when it comes to debugging. When the test runs the script will be injected into the page once the document exists. This will be as near to the opening &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag as it can be. But the exact position in the source usually depends on what browser is being used.&lt;/p&gt;&lt;p&gt;Some example use cases for this setting include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://andydavies.me/blog/2019/08/07/experimenting-with-link-rel-equals-preconnect-using-custom-script-injection-in-webpagetest/&quot;&gt;Experimenting with resource hints&lt;/a&gt; to see how they effect web performance&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://gist.github.com/Nooshu/c0c6dbeaad55a438ec30849bf8be1d96#file-simplified-wpt-font-injection-js&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Injecting custom performance marks&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; for various page events&lt;/li&gt;&lt;li&gt;Setting cookies / local storage for the &lt;a href=&quot;https://web.archive.org/web/20201128195853/https://www.webpagetest.org/forums/showthread.php?tid=16229&amp;pid=32790&quot;&gt;removal of page overlays&lt;/a&gt; and cookie banners&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://css-tricks.com/recipes-for-performance-testing-single-page-applications-in-webpagetest/&quot;&gt;Performance testing Single Page Apps (SPA)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Quote from Andy Davies about cookie banner testing below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I tend to work out what cookie or local storage entry the overlay is checking for and then add it via a WPT script, or inject JS&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It&#39;s alway good practice to wrap your custom script in an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/IIFE&quot;&gt;Immediately Invoked Function Expression (IIFE)&lt;/a&gt;, that way you know your variable &amp; function names won&#39;t clash with what is already set on the page. Here&#39;s a very simple example of a script you could add to the textarea:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// this will trigger a font load&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; customFont1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;custom font name&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;url([FONT_URL_HERE])&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// other font properties here&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// add the font to the document&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;customFont1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// monitor the font load (optional)&lt;/span&gt;
  customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loaded&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// log some info in the console&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Font status:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// log some info on the WPT waterfall chart&lt;/span&gt;
    performance&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mark&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;wpt.customFont1Loaded&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// if there&#39;s an error, tell us about it&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Font status:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we are loading a new font into the page programmatically using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API&quot;&gt;CSS Font Loading API&lt;/a&gt;. We are adding a &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-add-custom-marks-using-the-user-timing-api&quot;&gt;performance mark to the WPT waterfall chart&lt;/a&gt; so we know the exact point at which the font is loaded. There are so many uses for the &#39;Inject Script&#39; functionality, I&#39;m sure there could be a whole blog post dedicated to it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important note&lt;/strong&gt;: If you are looking to inject a script while using WPT scripting (&#39;Script&#39; tab, covered later), you will need to use a different method. Either &lt;code&gt;exec&lt;/code&gt; or &lt;code&gt;execAndWait&lt;/code&gt;. Documentation for these can be &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Scripting.md#exec&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;That brings us to the end of the &#39;Advanced&#39; tab. Next on our list is the &#39;Chromium&#39; tab.&lt;/p&gt;&lt;h3 id=&quot;chromium-tab&quot;&gt;Chromium tab&lt;/h3&gt;&lt;p&gt;The &#39;Chromium&#39; tab has a whole bunch of settings specific to the Chromium based browsers (Chrome, Edge, Opera, Brave, Samsung, Vivaldi).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Tab dedicated to chromium settings.&quot; decoding=&quot;async&quot; height=&quot;568&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/NE1kIbPeNp-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/NE1kIbPeNp-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/NE1kIbPeNp-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/NE1kIbPeNp-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h4 id=&quot;capture-lighthouse-report&quot;&gt;Capture Lighthouse Report&lt;/h4&gt;&lt;p&gt;This was covered above in the &#39;Simple Testing&#39; section above under &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#run-lighthouse-audit&quot;&gt;&#39;Run Lighthouse Audit&#39;&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;emulate-mobile-browser&quot;&gt;Emulate Mobile Browser&lt;/h4&gt;&lt;p&gt;This setting isn&#39;t just emulating the viewport settings of the device selected in the dropdown. Behind the scenes there is a reference VM that tests the performance of all the real devices in the device lab. WPT uses this data to throttle the CPU to closely match the performance of the real devices. If you examine the device options in this dropdown you will see it closely matches the real devices available in the lab. The setup for this emulation can be seen in the &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/blob/master/www/settings/mobile_devices.ini&quot;&gt;code here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;It&#39;s worth noting that the results from using this setting are still estimates of the performance compared to a real device. A real device will have different RAM available, and the CPU architecture will be different. Remember that you will still need to throttle the network connetion via the &#39;Test Settings&#39; tab (covered earlier).&lt;/p&gt;&lt;h4 id=&quot;capture-dev-tools-timeline&quot;&gt;Capture Dev Tools Timeline&lt;/h4&gt;&lt;p&gt;If you are running the Chrome browser this setting allows you to download the full DevTools timeline from the browser running the test. This file includes network, layout, and JavaScript parsing / executing data. The file can be opened via your local version of Chrome DevTools, or you can open it in the online viewer using the &#39;(view)&#39; link:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The timeline link highlighted is available on the test summary page and leads to more detailed information as to what the browser main thread is doing.&quot; decoding=&quot;async&quot; height=&quot;495&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/m5SOaY0YHn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/m5SOaY0YHn-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/m5SOaY0YHn-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/m5SOaY0YHn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This data gives you an incredible amount of detail about what is happening on the browsers main thread. If you are interested in finding out more about this functionality I&#39;ve written about it in a bit more detail &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-inspect-the-browser-main-thread---part-1&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;capture-v8-runtime-call-stats&quot;&gt;Capture V8 Runtime Call Stats&lt;/h4&gt;&lt;p&gt;I&#39;m replicating the order listed in the &#39;Chromium&#39; tab, but this setting makes a lot more sense if you read &#39;Capture Chrome Trace&#39; and &#39;Trace Categories&#39; first. This setting is effectively a shortcut to enable the &#39;Capture Chrome Trace&#39; setting and only select the &lt;code&gt;v8&lt;/code&gt; and &lt;code&gt;disabled-by-default-v8.runtime_stats&lt;/code&gt; categories. This setting is mainly used by the Chrome browser development team to easily collect data on where the &lt;a href=&quot;https://v8.dev/&quot;&gt;v8 engine&lt;/a&gt; is spending its time. There are many advantages to collecting this data in a WPT instance: &#39;clean&#39; data with only a single tab running and no extensions, easily repeatable results, bulk testing thousands of pages using the API, and collection of data from a set of real devices.&lt;/p&gt;&lt;h4 id=&quot;capture-chrome-trace-about-tracing&quot;&gt;Capture Chrome Trace (about://tracing)&lt;/h4&gt;&lt;p&gt;This setting is a reference to the &lt;code&gt;chrome://tracing/&lt;/code&gt; tab that you can access in Chrome. It gives you access to an incredible amount of underlying data not only from a page load perspective, but also the underlying performance of the Chrome browser itself at a system level. Once enabled you can access this data via the following links:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The trace link highlighted is available on the test summary page and leads to more detailed information as to what the browser main thread is doing.&quot; decoding=&quot;async&quot; height=&quot;495&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/_YjJVW2oP9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/_YjJVW2oP9-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/_YjJVW2oP9-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/_YjJVW2oP9-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: A word of warning about enabling this option. If you are only interested in the web performance of a web page, DON&#39;T enable this option. Some categories can add a lot of overhead to the tests, which in turn can result in the skewing of overall timings. This setting is primarily targeted at browser engineers, not web developers. For more information about this setting and the data visualisations available using it I have written about them more &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-inspect-the-browser-main-thread---part-2&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;trace-categories&quot;&gt;Trace Categories&lt;/h4&gt;&lt;p&gt;This setting is used in conjunction with the &#39;Capture Chrome Trace&#39; setting above, and is only relevant when the checkbox is checked. The amount of data returned when capturing a Chrome trace is huge. Now you may only be interested in one particular type of data. This input box allows you to select only the data you are interested in (e.g. for &lt;code&gt;blink&lt;/code&gt; data, delete all other categories listed). Again as mentioned above, you only really need to be concerned with this setting if you are a browser developer.&lt;/p&gt;&lt;h4 id=&quot;capture-network-log&quot;&gt;Capture Network Log&lt;/h4&gt;&lt;p&gt;This setting is similar to the &#39;Capture V8 Runtime Call Stats&#39; mentioned earlier. It is effectively a shortcut to enable tracing and select only the &lt;code&gt;netlog&lt;/code&gt; category in the &#39;Trace Categories&#39; input field. Enabling this also passes the command-line flag to store the non-trace version of the &lt;code&gt;netlog&lt;/code&gt; too: &lt;code&gt;chrome://net-internals&lt;/code&gt; generates a JSON file that can be used for debugging, and the trace-version is layered on top of this. Enabling this option makes WPT capture both netlog versions.&lt;/p&gt;&lt;h4 id=&quot;enable-data-reduction&quot;&gt;Enable Data Reduction&lt;/h4&gt;&lt;p&gt;This setting is a browser option that users can choose to enable on their devices. For example, if a user is on a limited data plan they may prefer to view what may be considered a &#39;cutdown&#39; version of a website. A site with fewer images, different image compression, slightly different (and more data efficient) page design.&lt;/p&gt;&lt;p&gt;In practical terms WPT will append the &lt;code&gt;save-data: on&lt;/code&gt; header to all of the requests sent to the server. More info about this header can be found on the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data&quot;&gt;Save-Data MDN page&lt;/a&gt;, and I&#39;ve written a blog post about its &lt;a href=&quot;https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/&quot;&gt;usage here&lt;/a&gt;. I also highly recommend this talk by &lt;a href=&quot;https://twitter.com/TimVereecke&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tim Vereecke&lt;/a&gt; from London Web Performance, November 2019 - &lt;a href=&quot;https://www.youtube.com/watch?v=X0BbSvSbwD0&quot;&gt;Data-s(h)aver strategies&lt;/a&gt;. It has a whole heap of information about this header and how you could be using it.&lt;/p&gt;&lt;h4 id=&quot;host-resolver-rules&quot;&gt;Host Resolver Rules&lt;/h4&gt;&lt;p&gt;This setting is basically just like a hosts file only with wildcard(&lt;code&gt;*&lt;/code&gt;) support. It&#39;s also what WPT uses for Chrome when the &lt;code&gt;setDns&lt;/code&gt; WPT script command is used. So for example entering &lt;code&gt;MAP * 127.0.0.1&lt;/code&gt; into the input field forces all hostnames to be mapped to &lt;code&gt;127.0.0.1&lt;/code&gt; (you&#39;re not going to get much back if you use this exact command!).&lt;/p&gt;&lt;p&gt;An example use case for this setting is if you are testing your live website, but you wanted to map some requests back to your development server instead. Very useful for some areas of testing and debugging.&lt;/p&gt;&lt;h4 id=&quot;command-line&quot;&gt;Command-line&lt;/h4&gt;&lt;p&gt;Chrome comes with &lt;a href=&quot;https://peter.sh/experiments/chromium-command-line-switches/&quot;&gt;a whole bunch&lt;/a&gt; of options you can enable &amp; disable via the command-line when you run it. Some change behaviour of features, others are for debugging or experimenting. You can enter these command-line options into this input box and they will be passed through to Chrome in the test.&lt;/p&gt;&lt;p&gt;A specific use case I&#39;ve used this for is to disable HTTP/2 using the &lt;code&gt;--disable-http2&lt;/code&gt; flag. So if you want to compare the performance of HTTP/2 vs HTTP/1.1 for the same site using WPT, simply pass in this setting and it will force the use of the HTTP/1.1 protocol. You can then compare the results of HTTP/1.1 vs HTTP/2 using WPT.&lt;/p&gt;&lt;h3 id=&quot;auth-tab&quot;&gt;Auth tab&lt;/h3&gt;&lt;p&gt;This is a much simpler and self explanatory tab compared to the previous tabs. This tab allows WPT to access a website that is sitting behind HTTP Basic authentication (BA). Useful if you wish to test a site that isn&#39;t publicly accessible.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Allow WPT to access a website behind HTTP authentication. Image shows input fields for a username and password.&quot; decoding=&quot;async&quot; height=&quot;317&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JMjAEcZrzG-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JMjAEcZrzG-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JMjAEcZrzG-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JMjAEcZrzG-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As the warning says in the tab, make sure you use a test account when using this functionality! You don&#39;t want to be sharing an account with higher privileges with anyone by them viewing the test results. Note that when in use the test won&#39;t appear in the &#39;Test History&#39; tab in the main navigation because it is automatically set to private.&lt;/p&gt;&lt;h3 id=&quot;script-tab&quot;&gt;Script tab&lt;/h3&gt;&lt;p&gt;The script tab allows you to automate testing via the use of the WPT scripting language.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The script tab holds a textarea that allows you to enter scripts using the WPT scripting language.&quot; decoding=&quot;async&quot; height=&quot;368&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QuHOBCujXz-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QuHOBCujXz-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QuHOBCujXz-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QuHOBCujXz-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The documentation for the script commands can be &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Scripting.md&quot;&gt;found here&lt;/a&gt;. Possible use cases for these scripts include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Following a user journey across multiple pages&lt;/li&gt;&lt;li&gt;Acceptance of cookie banners and the setting off cookies&lt;/li&gt;&lt;li&gt;Login to a member area for performance testing&lt;/li&gt;&lt;li&gt;Test a sites performance with third-parties blocked&lt;/li&gt;&lt;li&gt;Performance &lt;a href=&quot;https://css-tricks.com/recipes-for-performance-testing-single-page-applications-in-webpagetest/&quot;&gt;testing Single Page Applications (SPAs)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;An example of a very basic script looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;setCookie https://www.gov.uk/ cookies_policy={&quot;essential&quot;:true,&quot;settings&quot;:true,&quot;usage&quot;:true,&quot;campaigns&quot;:true}
setCookie https://www.gov.uk/ cookies_preferences_set=true
setCookie https://www.gov.uk/ global_bar_seen={&quot;count&quot;:999,&quot;version&quot;:8}
navigate https://www.gov.uk/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This script sets three cookies that will hide the cookie banner on GOV.UK then navigate to the homepage. This example can be useful if you want a clearer picture of the Cumulative Layout Shift (CLS) seen on a page (since cookie banners can cause a high CLS value on page load).&lt;/p&gt;&lt;h4 id=&quot;script-includes-sensitive-data&quot;&gt;Script includes sensitive data&lt;/h4&gt;&lt;p&gt;There may be times where you need to include sensitive data in your test script (e.g. login cookie data, personal information etc). If you share these results with other people you may not want them to be able to see this data. In this case select this checkbox and the &lt;code&gt;cookie&lt;/code&gt; headers and WPT script will be discarded in the results and no longer viewable (see images below):&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Checkbox unchecked&lt;/strong&gt; &lt;img alt=&quot;With the checkbox unchecked the original script is visible in the test result.&quot; decoding=&quot;async&quot; height=&quot;349&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/la4TmByEp1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/la4TmByEp1-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/la4TmByEp1-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/la4TmByEp1-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Checkbox checked&lt;/strong&gt; &lt;img alt=&quot;With the checkbox checked the original script is removed from the test result.&quot; decoding=&quot;async&quot; height=&quot;288&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3h-kKlNWLP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3h-kKlNWLP-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3h-kKlNWLP-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3h-kKlNWLP-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h4 id=&quot;discard-all-http-headers&quot;&gt;Discard all HTTP headers&lt;/h4&gt;&lt;p&gt;When enabled, all of the request headers will be removed from the reporting in the test results. So clicking on a request and navigating to the &#39;Request&#39; tab, the corresponding panel will be empty.&lt;/p&gt;&lt;h3 id=&quot;block-tab&quot;&gt;Block tab&lt;/h3&gt;&lt;p&gt;The block tab does &quot;exactly what it says on the tin&quot;, it blocks requests and domains listed in the two separate textareas.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The block tab allows you to block certain requests, allowing you to see the the difference these requests can have on performance.&quot; decoding=&quot;async&quot; height=&quot;435&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/bnnLbKNirM-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/bnnLbKNirM-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/bnnLbKNirM-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/bnnLbKNirM-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;These textareas take a space separated list of strings. These blocks cause what is known as a &#39;fast fail&#39;. The browser will try to grab the resource, fail, then move on.&lt;/p&gt;&lt;p&gt;Some use cases for blocking include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Testing to see how fast a site would be without adverts&lt;/li&gt;&lt;li&gt;Testing to see how fast a site would be without an A/B testing library&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For this you would run a standard test, then run a second test with said request(s) blocked, then compare the two versions.&lt;/p&gt;&lt;h4 id=&quot;block-requests-containing&quot;&gt;Block Requests Containing&lt;/h4&gt;&lt;p&gt;This textarea allows you to block individual requests by doing a substring (case-sensitive) match. An example of what you could paste in the textarea and what it blocks is seen below:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;// block all requests containing &#39;spacer.gif&#39;, &#39;ckeditor&#39;, &#39;texedit&#39;, and &#39;www.example-domain.co.uk&#39;
spacer.gif ckeditor texedit www.example-domain.co.uk
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that the blocked requests don&#39;t appear in the resulting waterfall at all, but you can see they were blocked at the top of the test page in the test details:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Blocked URL strings are displayed in the results at the top of the page.&quot; decoding=&quot;async&quot; height=&quot;120&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2TPuqgDwCL-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2TPuqgDwCL-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2TPuqgDwCL-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/2TPuqgDwCL-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This method works by using the Chrome extension API (when testing in Chrome). An extension is used that blocks requests much like a standard adblocker does. Because of this, there can be some time increases due to the overhead of the extension API. When it comes to blocking domains (like we have with &lt;code&gt;www.example-domain.co.uk&lt;/code&gt; in the example above), you are better to use the &#39;Block Domains&#39; setting detailed next.&lt;/p&gt;&lt;h4 id=&quot;block-domains&quot;&gt;Block Domains&lt;/h4&gt;&lt;p&gt;Very similar to the &#39;Block Requests&#39; functionality, only this time it accepts a space separated list of hostnames (full, not wildcards). An example of what you could paste in the textarea and what it blocks is seen below:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;// block Google Fonts, Google Analytics and Google Tag Manager
fonts.googleapis.com www.google-analytics.com www.googletagmanager.com
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The great thing about the block domains functionality is that it runs at the DNS level rather than via the extension API (in Chrome). So it has less overhead compared to matching URL substrings. But for that reason it only works with full URL strings.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: It is possible to use both blocking functions via &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Scripting.md#request-manipulation&quot;&gt;WPT scripting&lt;/a&gt; using the &lt;code&gt;block&lt;/code&gt; and the &lt;code&gt;blockDomains&lt;/code&gt; commands. It&#39;s also possible to do the inverse with the &lt;code&gt;blockDomainsExcept&lt;/code&gt; command where you block all URL&#39;s except the one listed. &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; has a great article on &#39;&lt;a href=&quot;https://andydavies.me/blog/2018/02/19/using-webpagetest-to-measure-the-impact-of-3rd-party-tags/&quot;&gt;Measuring the Impact of 3rd-Party Tags With WebPageTest&lt;/a&gt;&#39; where he uses this technique to great effect.&lt;/p&gt;&lt;h3 id=&quot;spof-tab&quot;&gt;SPOF tab&lt;/h3&gt;&lt;p&gt;SPOF stands for Single Point Of Failure. The textarea accepts a list of hosts that you want to silently fail (one host per line).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Single Point of Failure tab accepts a single host per line that should silently fail. Paste them into the textarea given.&quot; decoding=&quot;async&quot; height=&quot;356&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/mLvW-W8fXs-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/mLvW-W8fXs-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/mLvW-W8fXs-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/mLvW-W8fXs-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now you may be asking &quot;What&#39;s the difference between &#39;Block&#39; and &#39;SPOF&#39;?&quot;. As mentioned before when you block a URL substring or domain it will &#39;fail fast&#39;. The browser will know almost instantly and move on with the page load. SPOF on the other hand will redirect the request to &lt;code&gt;blackhole.webpagetest.org&lt;/code&gt; and then silently fail. This is essentially leaving the browser hanging waiting for a response.&lt;/p&gt;&lt;p&gt;A great use case for this is to establish what happens when one of your third-party dependencies fails (e.g. an ad-network, a/b testing tool, JavaScript CDN). Do you really want your site to slow to a crawl because a third-parties infrastructure (outside of your control) fails? This can happen even to the largest tech companies in the world as &lt;a href=&quot;https://www.theguardian.com/technology/2020/dec/14/google-suffers-worldwide-outage-with-gmail-youtube-and-other-services-down&quot;&gt;Google recently showed&lt;/a&gt;. So it&#39;s a good idea to test if you have a hard dependency with a third-party now, rather than wait for the failure.&lt;/p&gt;&lt;h3 id=&quot;custom-tab&quot;&gt;Custom tab&lt;/h3&gt;&lt;p&gt;The custom tab allows you to define custom JavaScript that will run at the end of a test to collect custom metrics (that will be returned in the results).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The custom tab consists of a single textarea for inputting JavaScript that will return custom metrics.&quot; decoding=&quot;async&quot; height=&quot;265&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JyS5CbEjCL-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JyS5CbEjCL-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JyS5CbEjCL-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/JyS5CbEjCL-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h4 id=&quot;custom-metrics&quot;&gt;Custom Metrics&lt;/h4&gt;&lt;p&gt;This is where you paste in the name of the metric being captured along with the JavaScript to be executed at the &lt;strong&gt;end&lt;/strong&gt; of the test (remember to &lt;code&gt;return&lt;/code&gt; a value from the JavaScript!). An extremely simple example of this in action can be seen below:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[num-total-scripts]
return document.querySelectorAll(&#39;script&#39;).length;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This example, taken from &lt;a href=&quot;https://twitter.com/tkadlec&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tim Kadlec&lt;/a&gt;&#39;s excellent &lt;a href=&quot;https://timkadlec.com/remembers/2020-04-16-webpagetest-custom-metrics-with-request-data/&quot;&gt;&#39;WebPageTest Custom Metrics with Request Data&#39;&lt;/a&gt; article simply counts the number of script tags on the page and returns them via a custom metric. The results look like this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Custom metrics can be viewed inside an individual test run data.&quot; decoding=&quot;async&quot; height=&quot;339&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/eBrJuO1Hyx-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/eBrJuO1Hyx-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/eBrJuO1Hyx-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/eBrJuO1Hyx-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Custom metrics data is also added to the JSON data that you &lt;a href=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/&quot;&gt;can query via the API&lt;/a&gt;. More complex examples can be seen in the &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/custom_metrics.md&quot;&gt;WPT documentation related to Custom Metrics&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;hidden-settings&quot;&gt;Hidden settings&lt;/h2&gt;&lt;p&gt;WPT comes with a number of hidden settings that aren&#39;t displayed in the UI by default. To enable these settings you must pass in a query parameter.&lt;/p&gt;&lt;h3 id=&quot;bulk-testing-tab&quot;&gt;Bulk Testing Tab&lt;/h3&gt;&lt;p&gt;You can enable this setting by appending &lt;code&gt;?bulk=1&lt;/code&gt; to the &lt;a href=&quot;https://www.webpagetest.org/?bulk=1&quot;&gt;WPT URL&lt;/a&gt;&lt;strong&gt;Note&lt;/strong&gt;: You will need an account to use this setting. This will give you a whole new tab under the &#39;Advanced Settings&#39; called &#39;Bulk Testing&#39;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The bulk testing tab is now visible in the UI when appending ?bulk=1.&quot; decoding=&quot;async&quot; height=&quot;301&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n-vrlnIjyV-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n-vrlnIjyV-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n-vrlnIjyV-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/n-vrlnIjyV-803.webp 803w&quot; width=&quot;803&quot;&gt;&lt;/p&gt;&lt;p&gt;In terms of usage it&#39;s pretty self explanatory. This setting adds the ability to test multiple URL&#39;s all using the same test settings via the UI. You can either paste in a list of URL&#39;s into the textarea, or upload a file with a list. Make sure each URL is on it&#39;s own line. For example, if you wanted to test multiple pages on the BBC News website you would paste this:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;https://www.bbc.co.uk/news
https://www.bbc.co.uk/news/coronavirus
https://www.bbc.co.uk/news/politics
https://www.bbc.co.uk/news/technology
https://www.bbc.co.uk/news/stories&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can also add labels to individual test pages like so:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Homepage=https://www.bbc.co.uk/news
COVID=https://www.bbc.co.uk/news/coronavirus
//...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you&#39;ve submitted the test you can bookmark the page and come back to it later. No need to keep the window open, the tests run independent of the browser window:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the URL&#39;s we want to performance test running.&quot; decoding=&quot;async&quot; height=&quot;341&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/tJmV6PMsD7-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/tJmV6PMsD7-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/tJmV6PMsD7-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/tJmV6PMsD7-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once completed WPT gives you a handy table with specific metrics listed, as well as links to many sources of raw data you can examine further:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the completed results for the 5 URL&#39;s we entered into the bulk testing tab.&quot; decoding=&quot;async&quot; height=&quot;407&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/AOisYVkqD9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/AOisYVkqD9-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/AOisYVkqD9-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/AOisYVkqD9-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The page even gives you the ability to compare test URL&#39;s via the checkbox and compare button to the left hand side of the table. Doing so will direct you towards the compare view page which I talk about more in the &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#visual-comparison-tab&quot;&gt;&#39;Visual comparison tab&#39;&lt;/a&gt; section below.&lt;/p&gt;&lt;p&gt;It&#39;s also worth noting that all of these tests are visible from the &#39;Test History&#39; link in the main navigation. They will each be listed separately, and a link to the &#39;Bulk Test&#39; summary table can also be seen on the list. Again, you can compare results from this view as well:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The bulk test results appear on the &#39;Test History&#39; page too.&quot; decoding=&quot;async&quot; height=&quot;220&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3vXuO1xnoX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3vXuO1xnoX-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3vXuO1xnoX-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/3vXuO1xnoX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;hidden-input-fields&quot;&gt;Hidden input fields&lt;/h3&gt;&lt;p&gt;There are also quite a number of query parameters that won&#39;t display anything in the UI, but they will add a hidden input field to the page source and pull the query value from the query parameter. These aren&#39;t all limited for use on the homepage, many can be used on the results pages too (e.g. &#39;result&#39;, &#39;details&#39;, &#39;compare&#39;).&lt;/p&gt;&lt;h4 id=&quot;iq-iq&quot;&gt;iq (&lt;code&gt;?iq=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;With this you can specify the JPEG image compression level used for screenshots and video capture. Value range between 30-100.&lt;/p&gt;&lt;h4 id=&quot;pngss-pngss&quot;&gt;pngss (&lt;code&gt;?pngss=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Set to &lt;code&gt;?pngss=1&lt;/code&gt; to save a full resolution lossless PNG of the fully loaded page being tested.&lt;/p&gt;&lt;h4 id=&quot;discard-discard&quot;&gt;discard (&lt;code&gt;?discard=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;The discard parameter allows you to discard the first x number of tests, but it is highly likely that this isn&#39;t implemented in the new agents. The value provided is the number of test runs to be discarded.&lt;/p&gt;&lt;h4 id=&quot;timeout-timeout&quot;&gt;timeout (&lt;code&gt;?timeout=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This is the maximum time a test is allowed to run in seconds. The default is 120 seconds.&lt;/p&gt;&lt;h4 id=&quot;appendua-appendua&quot;&gt;appendua (&lt;code&gt;?appendua=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This parameter accepts a string that will be appended to the user agent string. See the &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#append-to-ua-string&quot;&gt;&#39;Append to UA String&#39;&lt;/a&gt; section for more details about this setting.&lt;/p&gt;&lt;h4 id=&quot;keepvideo-keepvideo&quot;&gt;keepvideo (&lt;code&gt;?keepvideo=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This parameter if set to &lt;code&gt;?keepvideo=1&lt;/code&gt; when you run a test, will instruct WPT to keep the raw video without any additional compression so the frames can be analysed further.&lt;/p&gt;&lt;h4 id=&quot;medianmetric-medianmetric&quot;&gt;medianMetric (&lt;code&gt;?medianMetric=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This parameter it used to instruct WPT which metric will be used to decide the median run. By default &lt;code&gt;loadTime&lt;/code&gt; is used, but you can also pass in &lt;code&gt;SpeedIndex&lt;/code&gt; if you wish to use that. I discussed this setting in the &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#repeat-view&quot;&gt;&#39;Repeat View&#39;&lt;/a&gt; section earlier.&lt;/p&gt;&lt;h4 id=&quot;affinity-affinity&quot;&gt;affinity (&lt;code&gt;?affinity=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;I&#39;ll refer to the &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest-docs/blob/master/dev/api.md&quot;&gt;API docs&lt;/a&gt; for this explanation:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Specify a string that will be used to hash the test to a specific test agent. The tester will be picked by index among the available testers. If the number of testers changes then the tests will be distributed to different machines but if the counts remain consistent then the same string will always run the tests on the same test machine. This can be useful for controlling variability when comparing a given URL over time or different parameters against each other (using the URL as the hash string).&lt;/p&gt;&lt;/blockquote&gt;&lt;h4 id=&quot;tester-tester&quot;&gt;tester (&lt;code&gt;?tester=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This allows you to specify a specific test machine to run your test on. Note that this value must match a &#39;Computer Name&#39; from the list of test machines &lt;a href=&quot;https://www.webpagetest.org/getTesters.php&quot;&gt;found here&lt;/a&gt;, else it will never run.&lt;/p&gt;&lt;h4 id=&quot;minimal-minimal&quot;&gt;minimal (&lt;code&gt;?minimal=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;In theory this should run a cut down version of the tests optimised for individual run speeds. But at the moment it doesn&#39;t actually do anything. So consider it a work in progress.&lt;/p&gt;&lt;h4 id=&quot;noopt-noopt&quot;&gt;noopt (&lt;code&gt;?noopt=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Enable / disable the optimisation checks as seen under the &#39;Performance Review&#39; section in the test results (e.g. &lt;code&gt;keep-alive&lt;/code&gt;, GZip, CDN Usage etc). Enable this by setting &lt;code&gt;?noopt=1&lt;/code&gt;, this will speed up your tests.&lt;/p&gt;&lt;h4 id=&quot;debug-debug&quot;&gt;debug (&lt;code&gt;?debug=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This setting keeps the debug output of the agent along with the test results. Mainly used for debugging test instances.&lt;/p&gt;&lt;h4 id=&quot;throttle-cpu-throttle-cpu&quot;&gt;throttle_cpu (&lt;code&gt;?throttle_cpu=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;I touched on the methodology behind CPU throttling for mobile emulation in the &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#emulate-mobile-browser&quot;&gt;&#39;Emulate Mobile Browser&#39;&lt;/a&gt; section. The &lt;code&gt;throttle_cpu&lt;/code&gt; parameter allows you to throttle the CPU for Chromium based browsers (Chrome, Edge, Opera, Brave, Samsung, Vivaldi). This excludes the physical Android devices. The amount goes from 0 (no throttling) upwards. The highest value in the &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/blob/master/www/settings/mobile_devices.ini&quot;&gt;settings ini&lt;/a&gt; is 5.5 for MotoE and AndroidOne emulated devices. This signifies that these devices will have 5.5x slowdown.&lt;/p&gt;&lt;h4 id=&quot;browser-width-browser-width&quot;&gt;browser_width (&lt;code&gt;?browser_width=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Set the width of the browser window you wish to use in the test. Set using display pixels. Usage: &lt;code&gt;https://www.webpagetest.org/?browser_width=800&lt;/code&gt;&lt;/p&gt;&lt;h4 id=&quot;browser-height-browser-height&quot;&gt;browser_height (&lt;code&gt;?browser_height=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Set the height of the browser window you wish to use in the test. Set using display pixels. Usage: &lt;code&gt;https://www.webpagetest.org/?browser_height=600&lt;/code&gt;. Set both height and width by listing query parameters: &lt;code&gt;https://www.webpagetest.org/?browser_width=800&amp;browser_height=600&lt;/code&gt;.&lt;/p&gt;&lt;h4 id=&quot;width-width&quot;&gt;width (&lt;code&gt;?width=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Here we set the width of the actual viewport using &lt;a href=&quot;https://tomroth.com.au/css-pixels/&quot;&gt;CSS pixels&lt;/a&gt;. Usage: &lt;code&gt;https://www.webpagetest.org/?width=320&lt;/code&gt;.&lt;/p&gt;&lt;h4 id=&quot;height-height&quot;&gt;height (&lt;code&gt;?height=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Here we set the height of the actual viewport using &lt;a href=&quot;https://tomroth.com.au/css-pixels/&quot;&gt;CSS pixels&lt;/a&gt;. Usage: &lt;code&gt;https://www.webpagetest.org/?height=640&lt;/code&gt;.&lt;/p&gt;&lt;h4 id=&quot;thumbsize-thumbsize&quot;&gt;thumbsize (&lt;code&gt;?thumbsize=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This setting is used on the filmstrip page (&lt;code&gt;compare.php&lt;/code&gt;) and it controls the size of the largest side in the in the resulting thumbnails in the filmstrip. For example, if the thumbnails are landscape, this value will refer to the width of the image. For portrait it will refer to the height of the thumbnail. You can enter a custom value, or you can select the preset thumbnail sizes from under the filmstrip. I&#39;ve written a little more about the filmstrip view &lt;a href=&quot;https://nooshu.github.io/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-the-filmstrip-view-and-waterfall-chart-are-related&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&quot;fps-fps&quot;&gt;fps (&lt;code&gt;?fps=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;How many frames per second are captured by the video for desktop devices. This is set to 10fps. Increasing this value adds overhead to the tests.&lt;/p&gt;&lt;h4 id=&quot;discard-timeline-discard-timeline&quot;&gt;discard_timeline (&lt;code&gt;?discard_timeline=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Don&#39;t keep the DevTools traces and timeline data after the test has finished and they have been processed.&lt;/p&gt;&lt;h4 id=&quot;htmlbody-htmlbody&quot;&gt;htmlbody (&lt;code&gt;?htmlbody=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Set this value to &lt;code&gt;?htmlbody=1&lt;/code&gt; if you only want to save the response of the base HTML page. This is a slightly different setting than discussed in the &lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#save-response-bodies&quot;&gt;&#39;Save response bodies&#39;&lt;/a&gt; section above which saves &lt;em&gt;all&lt;/em&gt; the test responses.&lt;/p&gt;&lt;h4 id=&quot;disable-video-disable-video&quot;&gt;disable_video (&lt;code&gt;?disable_video=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This setting stops &lt;a href=&quot;https://ffmpeg.org/&quot;&gt;ffmpeg&lt;/a&gt; from running on the instance (&lt;code&gt;?disable_video=1&lt;/code&gt;). Usually, even if &lt;code&gt;video=0&lt;/code&gt; and a video isn&#39;t being captured, ffmpeg still runs.&lt;/p&gt;&lt;h4 id=&quot;lighthousethrottle-lighthousethrottle&quot;&gt;lighthouseThrottle (&lt;code&gt;?lighthouseThrottle=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;Use Lighthouse&#39;s version of CPU throttling with the Lighthouse audit rather that WPT&#39;s own version.&lt;/p&gt;&lt;h4 id=&quot;warmup-warmup&quot;&gt;warmup (&lt;code&gt;?warmup=&lt;/code&gt;)&lt;/h4&gt;&lt;p&gt;This used to allow for a throw-away test run before collecting data to warm up server caches. This functionality is no longer hooked up.&lt;/p&gt;&lt;h2 id=&quot;visual-comparison-tab&quot;&gt;Visual comparison tab&lt;/h2&gt;&lt;p&gt;Now that we&#39;ve gone over a lot of the basic and advanced settings above, explaining this tab is much easier. This tab is essentially a set of shortcuts allowing you to quickly compare two sites with each other using a real device on a network connection speed of your choosing.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we are comparing two sites against each other on a mobile using a 4G connection.&quot; decoding=&quot;async&quot; height=&quot;331&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/dxXsYyX937-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/dxXsYyX937-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/dxXsYyX937-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/dxXsYyX937-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once the test has been started simply sit back and wait while WPT completes 3 test runs across each URL.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see both tests listed, and the stages they are at in terms of the testing process.&quot; decoding=&quot;async&quot; height=&quot;134&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/1GxEMUudL4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/1GxEMUudL4-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/1GxEMUudL4-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/1GxEMUudL4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once the tests have completed on all URL&#39;s, you will be shown the standard compare view for each of the sites under the corresponding test conditions:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Visual comparison of the results on the standard compare view page.&quot; decoding=&quot;async&quot; height=&quot;361&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/5jqtkw2O7--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/5jqtkw2O7--300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/5jqtkw2O7--600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/5jqtkw2O7--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This view allows you to visually compare the loading speed and render process for each of the sites. If you scroll down the page you can also compare waterfall charts. You&#39;ll also be presented with 6 graphs with information ranging from &#39;Visual Progress&#39; all the way through to &#39;Total Bytes&#39; loaded by each page (and their corresponding filetype). I&#39;ve written about the compare view in the links below if you wish to know more:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.github.io/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#what-do-the-filmstrip-thumbnail-border-colours-signify&quot;&gt;&#39;What do the filmstrip thumbnail border colours signify&#39;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.github.io/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-the-filmstrip-view-and-waterfall-chart-are-related&quot;&gt;&#39;How the filmstrip view and waterfall chart are related&#39;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Nooshu/wpt-jq-data-scripts&quot;&gt;&#39;Create the compare view graphs using jq&#39;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;traceroute-tab&quot;&gt;Traceroute tab&lt;/h2&gt;&lt;p&gt;This may be one of the least well used of the main tabs available in WPT. The tab allows you to complete an &lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol&quot;&gt;Internet Control Message Protocol (ICMP)&lt;/a&gt; trace route to a specific host / IP address from a particular test location. The test can be run multiple times (default is 3), and then you are presented the results via the UI:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Traceroute showing 3 runs from Dulles, VA to CNN.com.&quot; decoding=&quot;async&quot; height=&quot;611&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/vLlIobIKR--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/vLlIobIKR--300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/vLlIobIKR--600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/vLlIobIKR--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example above I&#39;ve chosen a test instance in Dulles, VA and the host &lt;code&gt;edition.cnn.com&lt;/code&gt;. The test has been run 3 times, and each test lists 4 hops along with the time to get to each hop, and their corresponding IP address. If there&#39;s no response back you will &lt;a href=&quot;https://web.archive.org/web/20130209083931/https://www.webpagetest.org/forums/showthread.php?tid=536&amp;pid=2590&quot;&gt;simply receive a blank page&lt;/a&gt; once the tests have completed.&lt;/p&gt;&lt;p&gt;Quote from Pat&#39;s forum post below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;You can now run traceroutes from the test agents through the web UI. It&#39;s still pretty experimental but you can get to it here: http://www.webpagetest.org/traceroute&lt;/p&gt;&lt;p&gt;Here is a sample traceroute to o.aolcdn.com from the Dulles test agent.&lt;/p&gt;&lt;p&gt;When there&#39;s no response you&#39;ll get a blank entry (may update this to display a * so it looks more like the command-line). The traceroutes are done with ICMP for now (eventually I&#39;d like to implement it as a TCP port-80 traceroute but that&#39;s a lot harder to do on Windows than it should be).&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This is essentially the same as the &lt;a href=&quot;https://linux.die.net/man/8/traceroute&quot;&gt;&lt;code&gt;traceroute&lt;/code&gt; command&lt;/a&gt; that you can run in a terminal window. Only instead or running it from your local machine you are running it from the location of the test instance.&lt;/p&gt;&lt;p&gt;A use case for this functionality would be if you were interested in the traffic across the network between the instance and site server at any point in time. If you happen to be seeing unusually poor performance from an instance to the site server, running a &lt;code&gt;traceroute&lt;/code&gt; between the two will allow you to evaluate and understand why this may be occurring.&lt;/p&gt;&lt;h2 id=&quot;common-scenarios&quot;&gt;Common Scenarios&lt;/h2&gt;&lt;p&gt;In this section I&#39;m going to list a few scenarios you may want to test and the settings you will use. I&#39;ll add more of these over time as I encounter them.&lt;/p&gt;&lt;h3 id=&quot;testing-http-2-against-http-1-1&quot;&gt;Testing HTTP/2 against HTTP/1.1&lt;/h3&gt;&lt;p&gt;Testing a site using both protocols is fairly straightforward using WPT. That is assuming your server already has HTTP/2 enabled. The method for testing relies on the fact that browsers that don&#39;t support a specific protocol will simply &#39;fallback&#39; to a version that they do. This is in order that browsers &#39;Don&#39;t break the web&#39;. If a browser were to simply fail on encountering an unsupported protocol, progress on the web would be very difficult!&lt;/p&gt;&lt;p&gt;Step one is to test the site using HTTP/2. Since it is already enabled, this is really just a standard test run. So select your device and connection speeds then run the test. Remember to add a label to the test so you know which version you are looking at later:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Run the first test, label with HTTP/2 and select your settings.&quot; decoding=&quot;async&quot; height=&quot;547&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/Ma9jghlZI7-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/Ma9jghlZI7-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/Ma9jghlZI7-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/Ma9jghlZI7-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Next on the list is to run the HTTP/1.1 version of the test. Keep all the same settings, you just need one minor adjustment in the &#39;Chromium&#39; tab. In the &#39;Command-line&#39; input enter &lt;code&gt;--disable-http2&lt;/code&gt;. Remember to add a label marking it as HTTP/1.1. We then run the 2nd test:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Set up the Chromium tab to disable HTTP/2 in Chrome.&quot; decoding=&quot;async&quot; height=&quot;534&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QsTEHwKDf7-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QsTEHwKDf7-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QsTEHwKDf7-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/QsTEHwKDf7-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This command-line flag has disabled HTTP/2 in the browser so in order to communicate with the server it is having to fallback to HTTP/1.1. Now that we have both tests we are able to compare the results in WPT and examine which performs better.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Compare the results using WebPageTest by selecting the checkbox and clicking the &#39;compare&#39; button.&quot; decoding=&quot;async&quot; height=&quot;128&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/xQwG5fhJ_A-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/xQwG5fhJ_A-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/xQwG5fhJ_A-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/xQwG5fhJ_A-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Using the command-line to disable features is a great way to test the difference between new and old features. The challenge often lies in finding the right command-line flag. &lt;a href=&quot;https://peter.sh/experiments/chromium-command-line-switches/&quot;&gt;This page&lt;/a&gt; should help you find these.&lt;/p&gt;&lt;p&gt;You could use the same method to test:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;the difference between TLS versions using &lt;a href=&quot;https://peter.sh/experiments/chromium-command-line-switches/#ssl-version-max&quot;&gt;&lt;code&gt;--ssl-version-max&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;how a website loads without webfonts using &lt;a href=&quot;https://peter.sh/experiments/chromium-command-line-switches/#disable-remote-fonts&quot;&gt;&lt;code&gt;--disable-remote-fonts&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;the difference &lt;code&gt;preconnect&lt;/code&gt; makes using &lt;code&gt;--disable-preconnect&lt;/code&gt; to disable it (assuming the flag is still available).&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;testing-a-site-without-third-party-scripts-blocked&quot;&gt;Testing a site without third-party scripts blocked&lt;/h3&gt;&lt;p&gt;In this scenario we use the WPT scripting language to block third-party scripts. I&#39;ll give a brief overview but you can read a much more detailed version of this technique by reading the blog post &lt;a href=&quot;https://andydavies.me/blog/2018/02/19/using-webpagetest-to-measure-the-impact-of-3rd-party-tags/&quot;&gt;&#39;Measuring the Impact of 3rd-Party Tags With WebPageTest&#39;&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;First we will need to run a standard test run. This can be done via the UI or with a script. Here I&#39;m using the UI:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Run the times website through WPT without any modifications.&quot; decoding=&quot;async&quot; height=&quot;547&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/ThezkfApa9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/ThezkfApa9-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/ThezkfApa9-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/ThezkfApa9-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Next we need to run the same test but with a small tweak. We will use the &#39;Script&#39; tab to write a very simple script that will navigate to &#39;https://www.thetimes.co.uk/&#39; and block all other domains:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In this script we are blocking all domains except for the thetimes.co.uk, then navigating to it.&quot; decoding=&quot;async&quot; height=&quot;337&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8Y56m6PJRu-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8Y56m6PJRu-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8Y56m6PJRu-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/8Y56m6PJRu-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once both tests are complete it is time to compare the results:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Compare the two sets of results to see what the impact of those third-parties are.&quot; decoding=&quot;async&quot; height=&quot;105&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/LA4fkx1z7I-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/LA4fkx1z7I-300.webp 300w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/LA4fkx1z7I-600.webp 600w, https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/LA4fkx1z7I-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now the method for blocking other domains is quite a drastic one, as it may be blocking domains that the site requires (e.g. a CDN or an assets domain). But as a quick test to see the impact these domains have of performance it works really well. The stats from the 2 tests say it all really:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Visually complete: 11.2s vs 3.7s (block vs no-block)&lt;/li&gt;&lt;li&gt;Fully Loaded: 9.6s vs 40.8s (block vs no-block)&lt;/li&gt;&lt;li&gt;Requests: 39 vs 177 (block vs no-block)&lt;/li&gt;&lt;li&gt;Bytes: 1.1 MB vs 2.4 MB&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The test above gives some quick insight into the impact third-party domains can have on performance.&lt;/p&gt;&lt;h2 id=&quot;useful-tips-and-tricks&quot;&gt;Useful tips &amp; tricks&lt;/h2&gt;&lt;p&gt;In this section I&#39;ll document any useful tips and tricks I run into related to running a WPT test.&lt;/p&gt;&lt;h3 id=&quot;how-do-i-see-all-the-wpt-advanced-settings&quot;&gt;How do I see all the WPT advanced settings?&lt;/h3&gt;&lt;p&gt;Here&#39;s a little tip from &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/simonhearne&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Simon Hearne&lt;/a&gt;. What about if you don&#39;t want to click through all the advanced tabs to find the setting you are looking for? Wouldn&#39;t it be great if you could see them all on the same tab. Well, now you can with this handy little snippet:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.ui-tabs-hide&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;el&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; el&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;display&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;block&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simply paste this into your browser DevTools console and execute it. The resulting page will &lt;a href=&quot;https://twitter.com/AndyDavies/status/1326180254801653761&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;look like this&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;We come to the end of another long blog post related to WebPageTest. In the post I&#39;ve tried to document every setting I could find related to running a WebPageTest test, and also give you an explanation as to what they do and how they can be used. Please consider this a living document, so if you have spotted any errors or I&#39;ve missed anything, please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;. Credit will be given in the post changelog. If you would like to learn a lot more about WebPageTest, I highly recommend the book: &quot;&lt;a href=&quot;http://shop.oreilly.com/product/0636920033592.do&quot;&gt;Using WebPageTest&lt;/a&gt;&quot; by &lt;a href=&quot;https://twitter.com/rick_viscomi&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rick Viscomi&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/marcelduran&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Marcel Duran&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Finally, another huge thanks go to &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; who I&#39;ve inundated with random questions while writing this post!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;31/12/20: Initial post published.&lt;/li&gt;&lt;li&gt;31/12/20: Added info about needing an account for bulk testing, and the handy Wireshark snippit for faster examination. Many thanks to &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for this info!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Fixing a legacy HTTP/1.1 server configuration with Cloudflare Workers a CDN</title>
    <link href="https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/" />
    <updated>2020-11-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/</id>
    <content type="html">&lt;p&gt;So the configuration discussed in this post I&#39;ve run into a couple of times recently while examining the performance of some websites you&#39;d class as running on &#39;legacy&#39; infrastructure. When a site is running on HTTP/1.1, you expect to see multiple TCP connections opened to download all the site assets. The HTTP/1.1 specification (&lt;a href=&quot;https://tools.ietf.org/html/rfc2616#section-8.1.4&quot;&gt;RFC 2616&lt;/a&gt;) used to recommend:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;But that RFC was later obsoleted by &lt;a href=&quot;https://tools.ietf.org/html/rfc7230#section-6.4&quot;&gt;RFC 7230&lt;/a&gt; which changed this to:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Previous revisions of HTTP gave a specific number of connections as a ceiling, but this was found to be impractical for many applications. As a result, this specification does not mandate a particular maximum number of connections but, instead, encourages clients to be conservative when opening multiple connections.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;In terms of practical application by browser vendors they usually set the maximum number of connections to a single domain to 6. So if you see a waterfall that has a higher number than 6 connections to the same domain, that&#39;s probably worth investigating:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;This waterfall chart shows 9 TCP connections for 10 page assets.&quot; decoding=&quot;async&quot; height=&quot;257&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/WTkBkmIWA8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/WTkBkmIWA8-300.webp 300w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/WTkBkmIWA8-600.webp 600w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/WTkBkmIWA8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the WebPageTest waterfall chart seen above we have a total of 10 page assets being downloaded. But as you will notice from the orange and purple lines, there are 9 TCP connections established to do this! So what&#39;s happening here?&lt;/p&gt;&lt;h2 id=&quot;the-connection-header&quot;&gt;The &lt;code&gt;connection&lt;/code&gt; header&lt;/h2&gt;&lt;p&gt;On examining this waterfall closer it was possible to verify the server was running HTTP/1.1 as expected. But I noticed there was also a problematic little response header included too: &lt;code&gt;connection: close&lt;/code&gt;. We can refer back to RFC 7230 for an explanation as to what this header is doing:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The &quot;close&quot; connection option is defined for a sender to signal that this connection will be closed after completion of the response.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Put simply, for this page communicating using HTTP/1.1, the server is opening up 6 connections to download the page assets. The inclusion of &lt;code&gt;connection: close&lt;/code&gt; response header means that these TCP connections can&#39;t be reused for other assets after one has downloaded, they are simply closed. Another brand new TCP connection must be opened for the other assets. This isn&#39;t good! One of the major improvements of HTTP/1.1 over HTTP/1.0 was the ability to allow TCP connections to persist across multiple asset downloads. So by including the &lt;code&gt;connection: close&lt;/code&gt; response header you are &lt;a href=&quot;https://twitter.com/tunetheweb/status/1321901576999546883&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;basically bringing back HTTP/1.0&lt;/a&gt; for your users.&lt;/p&gt;&lt;h2 id=&quot;tcp-connections-are-expensive&quot;&gt;TCP connections are expensive&lt;/h2&gt;&lt;p&gt;Now you may be asking: &quot;What&#39;s the big deal?. How much difference can opening and closing multiple connections be?&quot;. Well it actually has a big impact on a number of areas:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Opening and closing connections takes up CPU time and memory on the server, client, and many other bits of hardware in-between.&lt;/li&gt;&lt;li&gt;Congestion across a network is increased because there are more packets being transmitted purely to setup TCP connections.&lt;/li&gt;&lt;li&gt;Web performance is decreased because there&#39;s added latency when established a new connection to download other assets.&lt;/li&gt;&lt;li&gt;More efficient TCP congestion control features can&#39;t be used. Each new TCP connection suffers from &lt;a href=&quot;https://tools.ietf.org/html/rfc5681#section-3.1&quot;&gt;TCP Slow Start&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;By minimising the opening of new TCP connections you are improving both the web performance and battery life for your users. Focussing internally you will also be reducing load on your server and the overall network. Basically everyone wins!&lt;/p&gt;&lt;h2 id=&quot;focus-on-web-performance&quot;&gt;Focus on web performance&lt;/h2&gt;&lt;p&gt;Lets take some time and focus on the web performance aspect of this configuration. If only there were a way to modify the request / responses coming from our inefficient server config... Thankfully there is! Using a &lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Worker&lt;/a&gt; we can do exactly that.&lt;/p&gt;&lt;h3 id=&quot;how-does-it-work&quot;&gt;How does it work?&lt;/h3&gt;&lt;p&gt;First and foremost lets get something out the way. It actually doesn&#39;t. In this instance the Cloudflare Worker isn&#39;t actually doing anything for us. And this is where I regret putting the technology used in the blog post URL! It makes things very hard to change if it needs to! First I think it&#39;s important to go over how it &lt;em&gt;doesn&#39;t&lt;/em&gt; work, and give you an idea of where my mental model broke down:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How it doesn&#39;t work&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In my head I was seeing a Cloudflare Worker as a simple single layered black box that does everything for you. E.g. when a users connection is established they are connecting directly to the worker, and the worker is connecting directly to the origin. This &lt;strong&gt;isn&#39;t&lt;/strong&gt; how it works at all. The worker isn&#39;t like a load balancer (which does pass through), it&#39;s much more complex than that.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Lots of layers&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;First it&#39;s important to understand that a connection is going through multiple disconnected layers, not just the worker itself. So for example the layers involved are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Browser communicates back and forth with the Edge nginx&lt;/li&gt;&lt;li&gt;Edge nginx communicates back and forth with the Worker&lt;/li&gt;&lt;li&gt;Worker communicates back and forth with the cache tier&lt;/li&gt;&lt;li&gt;Cache tier communicates back and forth with the origin fetch servers&lt;/li&gt;&lt;li&gt;Origin fetch servers communicates back and forth with the origin&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Each tier manages multiple requests independently. Like a &lt;a href=&quot;https://cloud.google.com/pubsub/docs/overview&quot;&gt;pub/sub queue&lt;/a&gt;. Actual connections are on both ends but completely independent. Because of all these layers, we can simply remove the worker completely as just using a CDN implements things like HTTP/2, keep-alive, and Brotli. CDNs can fix a lot of broken origin behaviors just by simply being in the path, which is exactly what happens in my use case mentioned in this blog post.&lt;/p&gt;&lt;p&gt;If there are any folks from Cloudflare reading this blog and you happen to have a diagram of all the above layers, that would be super helpful to see, so please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;share it with me&lt;/a&gt;. I had a look around the web and most diagrams just seem to display a Worker as a single box full of hidden tricks. It would be great to look at all the independent layers involved to get more of an idea of how they work together.&lt;/p&gt;&lt;h2 id=&quot;the-example-worker&quot;&gt;The example worker&lt;/h2&gt;&lt;p&gt;So even though the use case I mention doesn&#39;t require a worker (only a CDN), I&#39;m going to leave this section in as an example of how a Cloudflare worker works. For those of you familiar with &lt;a href=&quot;https://developers.google.com/web/fundamentals/primers/service-workers&quot;&gt;Service Workers&lt;/a&gt;, a Cloudflare Worker is basically the &#39;edge&#39; version of &lt;a href=&quot;https://w3c.github.io/ServiceWorker/&quot;&gt;this API&lt;/a&gt;, with some limitations due to the environment not being the browser. With a little JavaScript you can write a fully programmable network proxy, allowing us to control how network requests from our pages are handled.&lt;/p&gt;&lt;p&gt;The example code run in the worker below is actually fairly straightforward:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// this is the site we are proxying&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; site &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;www.example.com&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// do stuff when the &#39;fetch&#39; event occurs&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;fetch&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;respondWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;handleRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Respond to the request
 * @param {Request} request
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// x-host header added by WebPageTest when using overrideHost&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; xhost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;x-host&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// if x-host header not found, abort&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;xhost&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;x-host header missing&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// set our hostname to that listed in the xhost header when using overrideHost in WPT&lt;/span&gt;
  url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; xhost&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// store the original response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; originalResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// create a new response to be modified (response properties are immutable)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; originalResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// simple example of how you would add a custom header to the response&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// response.headers.set(&quot;x-custom-header&quot;, &quot;header-value&quot;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// return the modified response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in WebPageTest we add the following script:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;overrideHost www.example.com example-modifier.nooshu.workers.dev
navigate www.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This script above is saying: when you come across any asset requested from &lt;code&gt;www.example.com&lt;/code&gt;, route it through our Cloudflare Worker (&lt;code&gt;example-modifier.nooshu.workers.dev&lt;/code&gt;) so the request / response can be modified accordingly. If you are interested in what else you can do with Cloudflare Workers, I highly recommend reading the blog post &lt;a href=&quot;https://andydavies.me/blog/2020/09/22/exploring-site-speed-optimisations-with-webpagetest-and-cloudflare-workers/&quot;&gt;&#39;Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers&#39;&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;what-difference-does-it-make-to-web-performance&quot;&gt;What difference does it make to web performance&lt;/h2&gt;&lt;p&gt;Now that we have a CDN sitting between the browser and the origin, we have the following setups available that we can examine using WebPageTest:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Original config (HTTP/1.1 + &lt;code&gt;connection: close&lt;/code&gt; header)&lt;/li&gt;&lt;li&gt;HTTP/2 transformed by the CDN (&lt;code&gt;connection&lt;/code&gt; ignored or removed)&lt;/li&gt;&lt;li&gt;HTTP/1.1 transformed by the CDN (HTTP/1.1 + &lt;code&gt;connection: keep-alive&lt;/code&gt; header)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: To connect to the CDN using HTTP/1.1, you can simply use the &lt;code&gt;--disable-http2&lt;/code&gt; command line option in Chromium, so it drops back to HTTP/1.1.&lt;/p&gt;&lt;p&gt;So let&#39;s see what difference it makes:&lt;/p&gt;&lt;h3 id=&quot;page-waterfall-http-1-1-keep-alive&quot;&gt;Page waterfall (HTTP/1.1 + keep-alive)&lt;/h3&gt;&lt;p&gt;In the waterfall chart below we can immediately see the difference this makes verses the chart shown at the start of the post. Instead of 9 separate TCP connections to download 10 assets, we see 4. A reduction of 56%.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;This waterfall chart shows 2 TCP connections for 10 page assets.&quot; decoding=&quot;async&quot; height=&quot;257&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/8vGrHQE94w-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/8vGrHQE94w-300.webp 300w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/8vGrHQE94w-600.webp 600w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/8vGrHQE94w-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;page-waterfall-http-2&quot;&gt;Page waterfall (HTTP/2)&lt;/h3&gt;&lt;p&gt;While we are examining waterfalls, we may as well look at a H2 version for comparison. HTTP/2&#39;s ability to multiplex streams over a single connection has reduced the TCP connections down to 2. The other TCP connection is the &lt;code&gt;anonymous&lt;/code&gt; connection needed to download the web fonts.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;This h2 waterfall chart shows 4 TCP connections for 10 page assets.&quot; decoding=&quot;async&quot; height=&quot;257&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/mX0eMz0qFZ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/mX0eMz0qFZ-300.webp 300w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/mX0eMz0qFZ-600.webp 600w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/mX0eMz0qFZ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;timings-comparison&quot;&gt;Timings comparison&lt;/h3&gt;&lt;p&gt;But where it all really matters is in the page timings. Here we can see a comparison chart with all 3 configurations visualised:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Comparison chart showing the difference in performance of the different setups.&quot; decoding=&quot;async&quot; height=&quot;815&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/hrahpeyYB0-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/hrahpeyYB0-300.webp 300w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/hrahpeyYB0-600.webp 600w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/hrahpeyYB0-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;From the graphs you can clearly see that the original config (HTTP/1.1 + &lt;code&gt;connection: close&lt;/code&gt;) is much worse for performance in almost all metrics.&lt;/p&gt;&lt;h3 id=&quot;visually-progress-comparison&quot;&gt;Visually progress comparison&lt;/h3&gt;&lt;p&gt;From the visual progress graph below we get an idea of how a user with &lt;em&gt;perceive&lt;/em&gt; the performance of the website. It&#39;s easy to see from the graph that HTTP/2 is the winner. By rendering &lt;em&gt;something&lt;/em&gt; to the viewport almost half a second quicker than our modified version of HTTP/1.1, and 1.1 seconds quicker than the original config! It&#39;s also worth noting that with the original config the page is rendered in a single big bang. From 0% to 98% visually complete over what looks like a single frame. Where as H2 and H/1.1 + &lt;code&gt;keep-alive&lt;/code&gt; build the page more incrementally. This is essentially giving a user something to look at on their device sooner. This is always a good idea as it shows that something is actually happening.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Line graph showing the visual progress of each of the setups. H2 first, modified H1.1 second, with the original setup last.&quot; decoding=&quot;async&quot; height=&quot;360&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/UvS3054QIw-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/UvS3054QIw-300.webp 300w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/UvS3054QIw-600.webp 600w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/UvS3054QIw-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;testing&quot;&gt;Testing&lt;/h2&gt;&lt;p&gt;If you are wondering how you test to see if a server is configured with &lt;code&gt;connection: close&lt;/code&gt; enabled, there are a couple of quick methods:&lt;/p&gt;&lt;h3 id=&quot;devtools&quot;&gt;DevTools&lt;/h3&gt;&lt;p&gt;Both Chrome and Firefox DevTools allow you to view response headers as separate columns in the &#39;Network&#39; tab:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The DevTools network tab allows you to add new columns that display response headers.&quot; decoding=&quot;async&quot; height=&quot;380&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/t8BqD5Ouk4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/t8BqD5Ouk4-300.webp 300w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/t8BqD5Ouk4-600.webp 600w, https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/t8BqD5Ouk4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So if you see a row with &#39;HTTP/1.1&#39; as the protocol, and &#39;close&#39; under the connection column, then you know the server config isn&#39;t optimal.&lt;/p&gt;&lt;h3 id=&quot;curl&quot;&gt;Curl&lt;/h3&gt;&lt;p&gt;Or if you are looking for a way to test this via the command line then you can always use &lt;code&gt;curl&lt;/code&gt;. Simply update the server URL and run the following command:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sIX&lt;/span&gt; GET https://www.example.com &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Connection|HTTP/&#39;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Output:&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# HTTP/1.1 200 OK&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Connection: close&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It will extract the information for you and output it to the terminal window.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Now I admit this blog post hasn&#39;t actually &#39;fixed&#39; the issue at the origin unless you plan on dropping a CDN in front of it (you should seriously consider doing this). But what it has done is give you actual performance data you can use to &#39;build a case&#39; to get it fixed. If you report back to a business and / or management with graphs that show that by making a small change on the server both web performance and server load will improve. You could even use it to push for enabling HTTP/2 if a considerable improvement is seen, or even the use of a CDN.&lt;/p&gt;&lt;p&gt;It&#39;s amazing what a difference a single header can make when it comes to web performance. There are probably so many &#39;legacy&#39; server configs running across the web with this exact issue: where just a small modification to the config will make an enormous difference. So if you have a server that you know is still running HTTP/1.1, why not do your users (and your server) a favour and check you don&#39;t have &lt;code&gt;connection: close&lt;/code&gt; included in the response headers.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;01/11/20: Initial post published.&lt;/li&gt;&lt;li&gt;02/11/20: Update to mention that RFC 2616 was obsoleted by RFC 7230, which includes updated connection concurrency guidance. Thanks &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;02/11/20: Update to the post after a conversation with &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;. This version now calls out where my mental modal of Cloudflare Workers was incorrect and adds a bit more details on how it all works! Thanks (again) Pat!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Using jq with the WebPageTest API</title>
    <link href="https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/" />
    <updated>2020-10-09T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/</id>
    <content type="html">&lt;p&gt;I&#39;ve written quite a few blog posts on this blog &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;about WebPageTest&lt;/a&gt; (WPT). The reason for this is it tends to be the first tool I (and I&#39;m sure many) look to when they want to debug web performance issues with a website. Underneath its retro looking user interface (UI) is an incredibly powerful data collection tool for you to examine and analyse. Now what you may not realise is that all the data you can see in the UI, is also available via the WebPageTest API.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: All the &lt;code&gt;jq&lt;/code&gt; scripts from this post &lt;a href=&quot;https://github.com/Nooshu/wpt-jq-data-scripts&quot;&gt;can be found in this wpt-jq-data-scripts repo&lt;/a&gt;, along with a link to the &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1aTfGyGWh0586GaGI7YbOVqdQBITVzNewV9nXyrRxDfU/edit#gid=657404292&quot;&gt;example graphs in Google Sheets that can be found here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;accessing-the-api&quot;&gt;Accessing the API&lt;/h2&gt;&lt;p&gt;Accessing the API is very simple. You can either enter the URL &lt;code&gt;https://www.webpagetest.org/jsonResult.php?test=[TESTID]&lt;/code&gt; in your browser, or follow the &#39;View JSON result&#39; link on a tests summary page (see below):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The link to the JSON from the test runs is easily accessible at the top right of the WebPageTest summary page.&quot; decoding=&quot;async&quot; height=&quot;332&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PrJIem-K6y-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PrJIem-K6y-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PrJIem-K6y-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PrJIem-K6y-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Follow this link in Firefox and you will get a loverly inbuilt JSON viewer with a filter tool to quickly find what you are looking for. But if you happen to be a Chrome user you&#39;re out of luck, as you will simply get the raw JSON output on the screen. But there are &lt;a href=&quot;https://chrome.google.com/webstore/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa&quot;&gt;JSON formatter extensions on the Chrome Web Store&lt;/a&gt; you can install should you want this functionality.&lt;/p&gt;&lt;h2 id=&quot;the-json-blob&quot;&gt;The JSON blob&lt;/h2&gt;&lt;p&gt;So what comes out of the API in terms of data, and how much data are we talking about? Frankly it&#39;s a lot of data! The JSON blob contains detailed information about every single test run, and all of the individual file requests (including cookie and header information) for each run too. So you can imagine that as the number of runs increases (to give you a more stable median), the file size increases dramatically. For example, a single test run using Chrome for the homepage of this blog produces a JSON blob that is approximately 520KB in size (uncompressed / unminified). Taking it to the other extreme, with the GDS private instance of WebPageTest I&#39;ve run a number of tests with 200+ test runs. The JSON blob from these tests is around 50 MB (uncompressed / unminified). So if you are looking for a great way to stress test your JSON viewer or parser, the WebPageTest API is an excellent way to do it!&lt;/p&gt;&lt;h2 id=&quot;bringing-in-jq&quot;&gt;Bringing in jq&lt;/h2&gt;&lt;p&gt;So the output from the WebPageTest API can be pretty huge. Packed full of interesting data waiting to be analysed and explored. But how do you do that in a quick and easy way? That&#39;s where &lt;a href=&quot;https://stedolan.github.io/jq/&quot;&gt;jq comes in&lt;/a&gt;. So what is jq? I think the jq author Stephen Dolan explains it best:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;jq is like &lt;code&gt;sed&lt;/code&gt; for JSON data - you can use it to slice and filter and map and transform structured data with the same ease that &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt; and friends let you play with text.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Basically jq is an incredibly powerful tool for manipulating JSON data. It can be used to extract basic data, or even fully restructure it if required. When I say it is a tool, that&#39;s actually doing it a huge disservice. jq is in fact a &lt;a href=&quot;https://stackoverflow.com/a/7320&quot;&gt;Turing Complete&lt;/a&gt; programming language. Proof of its turing completeness can be &lt;a href=&quot;https://github.com/MakeNowJust/bf.jq&quot;&gt;seen here&lt;/a&gt;, with a &lt;a href=&quot;http://en.wikipedia.org/wiki/Brainfuck&quot;&gt;Brainfuck&lt;/a&gt; interpreter written in jq.&lt;/p&gt;&lt;p&gt;So how can we use jq with the WebPageTest API to simplify our workflow and extract that data we need? That&#39;s what the rest of this blog post will be talking about.&lt;/p&gt;&lt;h2 id=&quot;the-basics&quot;&gt;The basics&lt;/h2&gt;&lt;p&gt;So first, the absolute basics. For readers who already have experience using jq, you can probably skip this part. But I&#39;m always one for being inclusive and covering first principles in blog posts. Downloading / installing is fairly simple, and there are plenty of ports available with instructions on the &lt;a href=&quot;https://stedolan.github.io/jq/download/&quot;&gt;download page&lt;/a&gt;. Once installed we can use &lt;code&gt;curl&lt;/code&gt; to grab our JSON blob like so:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://www.webpagetest.org/jsonResult.php?test&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;WPT_TEST_ID&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; test-data.json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we are saving our test data to a &lt;code&gt;test-data.json&lt;/code&gt; file on the local machine. Once done, we can read it using jq:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token string&quot;&gt;&#39;.&#39;&lt;/span&gt; test-data.json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This command will print the whole contents of the JSON file to the screen. The &lt;code&gt;&#39;.&#39;&lt;/code&gt; is known as the identity filter. It takes the input, and pipes it directly through to the output. This is an excellent way to pretty print a set of JSON data:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token string&quot;&gt;&#39;.&#39;&lt;/span&gt; test-data.json &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; pretty-test-data.json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above method works, but it&#39;s very long-winded. Thankfully &lt;code&gt;jq&lt;/code&gt; has the ability to either read a file, or it can take its input from &lt;a href=&quot;https://www.howtogeek.com/435903/what-are-stdin-stdout-and-stderr-on-linux/&quot;&gt;stdin&lt;/a&gt;. So the above commands can be simplified to:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://www.webpagetest.org/jsonResult.php?test&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;WPT_TEST_ID&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; jq &lt;span class=&quot;token string&quot;&gt;&#39;.&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; pretty-test-data.json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we are passing the output from the (silenced with &lt;code&gt;-s&lt;/code&gt;) &lt;code&gt;curl&lt;/code&gt; command into jq. jq is then pretty printing it to a file on the computer.&lt;/p&gt;&lt;p&gt;The above commands are the basis for the rest of the commands listed in this post.&lt;/p&gt;&lt;h2 id=&quot;what-data-is-in-the-api&quot;&gt;What data is in the API&lt;/h2&gt;&lt;p&gt;So let&#39;s assume we don&#39;t have a JSON viewer to dig down into the data. So what can we do to help us understand what data the API contains? There are a couple of jq commands that can do that:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot; data-gist=&quot;ff463b3cbc95ffe75da16224cb217fd5&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# query the API, pipe into `jq`&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://www.webpagetest.org/jsonResult.php?test&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;WPT_TEST_ID&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.data | path(..) | map(tostring) | join (&quot;/&quot;)&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; full-schema.txt

&lt;span class=&quot;token comment&quot;&gt;# example output&lt;/span&gt;
fvonly
successfulFVRuns
average
average/firstView
average/firstView/minify_total
average/firstView/responses_200
average/firstView/testStartOffset
average/firstView/bytesOut
average/firstView/gzip_savings
average/firstView/requestsFull
average/firstView/start_epoch
average/firstView/cpu.MarkLoad
average/firstView/connections
average/firstView/cpu.EventDispatch
&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Examining this filter:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;.data&lt;/code&gt; - navigate down into the data object&lt;/li&gt;&lt;li&gt;&lt;code&gt;path(..)&lt;/code&gt; - recursively decent into the large structure and extracting all the paths&lt;/li&gt;&lt;li&gt;&lt;code&gt;map(tostring)&lt;/code&gt; - map the array into a new array and convert paths to strings&lt;/li&gt;&lt;li&gt;&lt;code&gt;join (&quot;/&quot;)&lt;/code&gt; - join each string together with a forward slash&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We then output this result to a file. What this has now created is a high-level schema as to how the WebPageTest API is structured. If you are interested in the full output of this command you can find the &lt;a href=&quot;https://gist.github.com/Nooshu/ff463b3cbc95ffe75da16224cb217fd5&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;top-level-structure&quot;&gt;Top level structure&lt;/h3&gt;&lt;p&gt;Examining the API closely a pattern emerges. At the top level you have some basic test run data and a set of objects with (much) more data:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;average - contains data about the average for each metric across all runs&lt;/li&gt;&lt;li&gt;standardDeviation - contains data about the standard deviation of each metric across all runs&lt;/li&gt;&lt;li&gt;median - contains data about the median run (depending on median metric that has been selected)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Delving a level deeper we find each of these has the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;firstView - data from the first view of the page&lt;/li&gt;&lt;li&gt;repeatView (if selected in the GUI) - data from the repeat view of the page (useful for examining caching between page navigation)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And within each of these we come to a huge amount of data that WebPageTest has gathered from the selected browser when loading the page. And this same data structure can be found within each of the runs objects at the top level:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;runs - each of the individual run data is contained within here&lt;/li&gt;&lt;li&gt;runs/1 - data about run 1&lt;/li&gt;&lt;li&gt;runs/2 - data about run 2&lt;/li&gt;&lt;li&gt;runs/n - data about run n...&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;inner-data-structure&quot;&gt;Inner data structure&lt;/h3&gt;&lt;p&gt;We can use another jq filter to extract information about the inner data structure within the average, standardDeviation, median, and individual run numbers:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot; data-gist=&quot;8ae70d8b248a1128e2a58ac3b2625a30&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.data.median.firstView | to_entries | def parse_entry: {&quot;key&quot;: .key, &quot;value&quot;: .value | type}; map(parse_entry) | sort_by(.key) | from_entries&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; inner-structure.json

&lt;span class=&quot;token comment&quot;&gt;# example output&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;Colordepth&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;,
  &lt;span class=&quot;token string&quot;&gt;&quot;Dpi&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;,
  &lt;span class=&quot;token string&quot;&gt;&quot;Images&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;,
  &lt;span class=&quot;token string&quot;&gt;&quot;LastInteractive&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;,
  &lt;span class=&quot;token string&quot;&gt;&quot;LayoutShifts&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt;,
  &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Examining this filter:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;.data.median.firstView&lt;/code&gt; - navigate down into the median firstView data&lt;/li&gt;&lt;li&gt;&lt;code&gt;to_entries&lt;/code&gt; - convert an object into an array of objects with a key and a value&lt;/li&gt;&lt;li&gt;&lt;code&gt;def parse_entry&lt;/code&gt; - define a jq function to rewrite the object into key then examine the type of value it is&lt;/li&gt;&lt;li&gt;&lt;code&gt;map(parse_entry)&lt;/code&gt; - create a new array, running each object through this function&lt;/li&gt;&lt;li&gt;&lt;code&gt;sort_by(.key)&lt;/code&gt; - sort result alphabetically by the key name&lt;/li&gt;&lt;li&gt;&lt;code&gt;from_entries&lt;/code&gt; - convert the array back into an object&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Then we output the final modified JSON to a file to be used where needed. The full output of this command can be seen in &lt;a href=&quot;https://gist.github.com/Nooshu/8ae70d8b248a1128e2a58ac3b2625a30&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;this Gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;problematic-keys&quot;&gt;Problematic keys&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: So &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; has come to the rescue (again) and quickly solved this issue. If you append &lt;code&gt;normalizekeys=1&lt;/code&gt; to a JSON response, the key names will now be normaized, stripping away any non-alphanumeric characters. For the API this means any &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, and &lt;code&gt;_&lt;/code&gt; characters will be removed. I&#39;m leaving in the section below for future reference.&lt;/p&gt;&lt;p&gt;It&#39;s worth mentioning at this point that the naming of some of the keys coming from the API can be problematic depending on how they are being used. There are many keys that contain a &lt;code&gt;.&lt;/code&gt; which may not sound too bad, but if you are extracting the data using JavaScript, it causes issues. For example, to access values in a JavaScript object you may often use the dot notation:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; simple_example &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;connectivity&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Cable&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;loadTime&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;887&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;firstImagePaint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;627&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Accessing values&lt;/span&gt;
simple_example&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connectivity &lt;span class=&quot;token comment&quot;&gt;// &quot;Cable&quot;&lt;/span&gt;
simple_example&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;firstImagePaint &lt;span class=&quot;token comment&quot;&gt;// 627&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But look what happens when one of our keys contains dots:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; simple_example &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;connectivity&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Cable&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;loadTime&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;887&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;firstImagePaint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;627&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;cpu.v8.compile&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;21&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Accessing values&lt;/span&gt;
simple_example&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cpu&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;v8&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;compile &lt;span class=&quot;token comment&quot;&gt;// &quot;cpu is undefined&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The JavaScript is looking inside the &lt;code&gt;simple_example&lt;/code&gt; object and looking for the &lt;code&gt;cpu&lt;/code&gt; object, then for the &lt;code&gt;v8&lt;/code&gt; object, until it finds the &lt;code&gt;compile&lt;/code&gt; key. Obviously this chain doesn&#39;t exist as &lt;code&gt;cpu.v8.compile&lt;/code&gt; is a single key name, so the JavaScript fails.&lt;/p&gt;&lt;p&gt;There are also another set of keys that can cause issues too: some of the keys include &lt;code&gt;-&lt;/code&gt; in the name. This is again an issue when using the dot notation to try to extract the value. The presence of &lt;code&gt;-&lt;/code&gt; in the key is seen as a subtraction expression, e.g. &lt;code&gt;data.median.firstView.score_keep-alive&lt;/code&gt; is seen as &lt;code&gt;data.median.firstView.score_keep - alive&lt;/code&gt;, which of course fails. There is a fix for this issue in JavaScript. You can use the array syntax to look up the values:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; simple_example &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;score_keep-alive&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;cpu.v8.compile&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;21&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Accessing values&lt;/span&gt;
simple_example&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;score_keep-alive&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 1&lt;/span&gt;
simple_example&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;cpu.v8.compile&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 21&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But wouldn&#39;t it be great if we could get jq to clean the data for us, so we don&#39;t need to even worry about it? Thankfully we can with this filter:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token string&quot;&gt;&#39;walk( if type == &quot;object&quot; then with_entries( if .key | contains(&quot;-&quot;) then .key |= sub(&quot;-&quot;;&quot;&quot;) else . end) else . end) | walk( if type == &quot;object&quot; then with_entries( if .key | contains(&quot;.&quot;) then .key |= gsub(&quot;&#92;&#92;.&quot;;&quot;&quot;) else . end) else . end)&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s a bit of a beast, but it is basically walking through the data examining the keys. If they contain a &lt;code&gt;-&lt;/code&gt; or a &lt;code&gt;.&lt;/code&gt; then it is replaced with nothing.&lt;/p&gt;&lt;p&gt;NOTE: This isn&#39;t the most efficient filter since it is walking through the data twice. If anyone has any pointers on how to reduce this down to a single &lt;code&gt;walk()&lt;/code&gt; function call, please &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;scenarios&quot;&gt;Scenarios&lt;/h2&gt;&lt;p&gt;Now that we&#39;ve taken a closer look at the data available inside the API, let&#39;s look at a few jq commands we can run to extract the data we want.&lt;/p&gt;&lt;h3 id=&quot;extracting-first-and-third-party-domains&quot;&gt;Extracting first and third party domains&lt;/h3&gt;&lt;p&gt;This isn&#39;t as hard as it sounds since the API returns a &lt;code&gt;domains&lt;/code&gt; object with each domain visited in the test and some connection information about the domain too. Using jq we can easily extract a list of all the domains. Note the use of the &lt;code&gt;--raw-output&lt;/code&gt; option (or &lt;code&gt;-r&lt;/code&gt;), which stops the resulting output being formatted as JSON. In this example we don&#39;t want quoted strings, we want them as a simple list.&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.data.median.firstView.domains | to_entries[] | .key&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; domains-list.txt&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or if you only wanted first-party domains (assuming &#39;nytimes.com&#39; is the website being tested):&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.data.median.firstView.domains | to_entries[] | select(.key | contains(&quot;nytimes.com&quot;) ) | .key&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; first-party-domains-list.txt&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or what about the inverse, if you wanted all third-party domains:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.data.median.firstView.domains | to_entries[] | select(.key | contains(&quot;nytimes.com&quot;) | not ) | .key&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; third-party-domains-list.txt&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So where might this be useful? Well WebPageTest has the ability to block requests to certain domains, so you could quickly block any / all third-party domains by copy / pasting the result of this command into the GUI. If you are wanting to block all third-party requests there&#39;s an easier way using WebPageTest scripting (&lt;code&gt;blockDomainsExcept&lt;/code&gt;) that &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; covers in his &#39;&lt;a href=&quot;https://andydavies.me/blog/2018/02/19/using-webpagetest-to-measure-the-impact-of-3rd-party-tags/&quot;&gt;Measuring the Impact of 3rd-Party Tags With WebPageTest&lt;/a&gt;&#39; blog post. But this method gives you a way to gradually add / remove domains from the list and retest the impact of each if required.&lt;/p&gt;&lt;h3 id=&quot;plotting-connection-setup-metrics-across-runs&quot;&gt;Plotting connection setup metrics across runs&lt;/h3&gt;&lt;p&gt;Here we&#39;ll look at plotting the values from a large number of test runs related to the connection setup time (similar to the graphs I created in &lt;a href=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/#tls-12-vs-tls-13&quot;&gt;this blog post&lt;/a&gt;). So we are looking for the following values from the API:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;DNS start &amp; end (&lt;code&gt;runs.number.firstView.requests.0.dns_start&lt;/code&gt;, &lt;code&gt;runs.number.firstView.requests.0.dns_end&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Connect start &amp; end (&lt;code&gt;runs.number.firstView.requests.0.connect_start&lt;/code&gt;, &lt;code&gt;runs.number.firstView.requests.0.connect_end&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;SSL start &amp; end (&lt;code&gt;runs.number.firstView.requests.0.ssl_start&lt;/code&gt;, &lt;code&gt;runs.number.firstView.requests.0.ssl_end&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Note: I realise the API comes with &lt;code&gt;dns_ms&lt;/code&gt;, &lt;code&gt;connect_ms&lt;/code&gt;, &lt;code&gt;ssl_ms&lt;/code&gt;, but in the tests I&#39;m looking at there are a lot of 0 values for this key, so I will calculate it manually.&lt;/p&gt;&lt;p&gt;Our plan here is to extract only the data listed above and output into a comma-separated values (CSV) format. That way we can easily import the data into any graphing tool and graph the data. Here&#39;s the command for this (version 1):&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;[&quot;DNS Start&quot;, &quot;DNS End&quot;, &quot;Connect Start&quot;, &quot;Connect End&quot;, &quot;SSL Start&quot;, &quot;SSL End&quot;],(.data.runs | to_entries[].value.firstView.requests[] | select(.final_base_page == true) | [.dns_start, .dns_end, .connect_start, .connect_end, .ssl_start, .ssl_end]) | @csv&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; csv-output.csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is a long command, so let&#39;s break it down a little:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;[&quot;DNS Start&quot;, &quot;DNS End&quot;, &quot;Connect Start&quot;, &quot;Connect End&quot;, &quot;SSL Start&quot;, &quot;SSL End&quot;]&lt;/code&gt; - these are the headers for each of the columns in the resulting CSV file&lt;/li&gt;&lt;li&gt;&lt;code&gt;.data.runs | to_entries[].value.firstView.requests[]&lt;/code&gt; - drill down into the runs data and convert each run into a set of data objects with a key and a value (and remove them from the wrapping array). Then pull out the runs individual requests from the first view and unwrap from the array.&lt;/li&gt;&lt;li&gt;&lt;code&gt;select(.final_base_page == true)&lt;/code&gt; - here we are filtering to find the request that has come from the HTML of the page (that will contain the connection information we need)&lt;/li&gt;&lt;li&gt;&lt;code&gt;[.dns_start, .dns_end, .connect_start, .connect_end, .ssl_start, .ssl_end]&lt;/code&gt; - construct a new array only containing the data we will need in the CSV file&lt;/li&gt;&lt;li&gt;&lt;code&gt;| @csv&lt;/code&gt; - pipe the final array wrapped data into the CSV formatter&lt;/li&gt;&lt;li&gt;&lt;code&gt;&gt; csv-output.csv&lt;/code&gt; - output to a file&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This gives us a CSV file with 6 columns, and from those columns we can calculate the time each of the DNS, Connect, SSL stages took. But we can actually get &lt;code&gt;jq&lt;/code&gt; to do that calculation for us:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;[&quot;DNS&quot;, &quot;Connect&quot;, &quot;SSL&quot;],(.data.runs | to_entries[].value.firstView.requests[] | select(.final_base_page == true) | [(.dns_end - .dns_start), (.connect_end - .connect_start), (.ssl_end - .ssl_start)]) | @csv&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; csv-output.csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above command is very similar to version 1, only now we have got &lt;code&gt;jq&lt;/code&gt; to do some of the calculation leg work for us, as it can do the basic arithmetic when the new array of data is being constructed. So no need to do it in our spreadsheet / graphing tool now.&lt;/p&gt;&lt;p&gt;It&#39;s worth mentioning that we&#39;ve just reduced a 46MB JSON file with 201 test runs down to a 2KB CSV file by discarding all the information we weren&#39;t interested in.&lt;/p&gt;&lt;p&gt;Importing the data into Google Sheets, it is now possible to create a graph that look like this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Plotting the DNS, connect, and SSL times using Google Sheets gives an idea as to how stable the test runs were during the WebPageTest testing.&quot; decoding=&quot;async&quot; height=&quot;396&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/F3MTrSDgqK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/F3MTrSDgqK-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/F3MTrSDgqK-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/F3MTrSDgqK-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now I don&#39;t know about you, but the thought of having to type out those 289 characters to run that command doesn&#39;t sound like much fun. So let&#39;s look at how we can simplify the workflow.&lt;/p&gt;&lt;h3 id=&quot;jq-command-reuse&quot;&gt;&lt;code&gt;jq&lt;/code&gt; command reuse&lt;/h3&gt;&lt;p&gt;There&#39;s a really useful bit of functionality where &lt;code&gt;jq&lt;/code&gt; can be used in a &lt;a href=&quot;https://en.wikipedia.org/wiki/Shebang_(Unix)&quot;&gt;shebang&lt;/a&gt; line. Using this method it is possible to hide the &lt;code&gt;jq&lt;/code&gt; code inside a separate file, then point your JSON at the file and out pops the data in the format we need! So if we take the complicated looking filter above, and create a &lt;code&gt;connection-metrics.jq&lt;/code&gt; file (&lt;a href=&quot;https://gist.github.com/Nooshu/f8b1eee5a805657fdcd71aff6b2d88eb&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;gist here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;):&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token important shebang&quot;&gt;#!/usr/bin/env jq -rMf&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Filter to extract DNS, Connect, SSL metrics from WebPageTest run JSON&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Headers for resulting CSV&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;DNS&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;Connect&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;SSL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;,
	&lt;span class=&quot;token comment&quot;&gt;# drill down into the runs data&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
	.data.runs
	&lt;span class=&quot;token comment&quot;&gt;# convert the run data into an object and drill down into the run request data&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; to_entries&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;.value.firstView.requests&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;# only select the request that corresponds to the HTML page (so it has DNS,Connect,SSL data)&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; select&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.final_base_page &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;# build an array of the resulting data we want in the CSV&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.dns_end - .dns_start&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.connect_end - .connect_start&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.ssl_end - .ssl_start&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;# pass to the CSV formatter&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; @csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Notice how we can now easily comment the filter and split it over multiple lines for better readability. Save this to a file, make sure the file can be executed (&lt;code&gt;chmod +x connection-metrics.jq&lt;/code&gt;), then point it at your WebPageTest JSON like so:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://www.webpagetest.org/jsonResult.php?test&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;WPT_TEST_ID&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; ./connection-metrics.jq &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; result-output.csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or for a JSON file on your local machine:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;./connection-metrics.jq wpt-api-input.json &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; result-output.csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Both of these commands produce a CSV output like below, that can be easily imported into any program that happens to support CSV&#39;s.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&quot;DNS&quot;,&quot;Connect&quot;,&quot;SSL&quot;
37,29,45
32,30,45
33,29,45
...
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;use-as-a-template&quot;&gt;Use as a template&lt;/h3&gt;&lt;p&gt;So the great thing about the above example is it can now be used as a template for extracting other values from each of the WebPageTest runs. The command has already drilled down to the run level data after all. So what about if we extract and plot some other metrics? For example lets look at:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Time to First Byte (&lt;code&gt;TTFB&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;First Contentful Paint (&lt;code&gt;firstContentfulPaint&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Start Render (&lt;code&gt;render&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;DOM Complete (&lt;code&gt;domComplete&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Fully Loaded (&lt;code&gt;fullyLoaded&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We modify the above example script to find the new metrics (seen in &lt;a href=&quot;https://gist.github.com/Nooshu/11ecfacdd98f5be12867649564063d91&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;this gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;), then run it over our data set. This then gives us a graph that looks like this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Plotting the more page level metrics in Google sheets. This time using a line chart.&quot; decoding=&quot;async&quot; height=&quot;404&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/I7NfOv5GDj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/I7NfOv5GDj-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/I7NfOv5GDj-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/I7NfOv5GDj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So over the 201 test runs it&#39;s clear to see that the TTFB was very stable, with DOM Complete and Fully loaded second in terms of stability. FCP and start render are almost identical and jump around a lot. But there&#39;s an issue: there&#39;s a huge spike in the results which is clearly an error, and it breaks the scale of the graph. Now of course we could look through all of the data manually and remove it, but lets get &lt;code&gt;jq&lt;/code&gt; to do it for us. After all if we ever had a data set with lots of these occurrences, automating the task is going to save us time.&lt;/p&gt;&lt;p&gt;First we need to find a way to pinpoint the broken test run. The most stable metric is TTFB, which we have a fairly good idea about what this value should be (~2000ms), so we can use it to filter out any large differences from the data. To do this we simply add a &lt;code&gt;| select(.TTFB &amp;lt; 2200)&lt;/code&gt; into the filter. With this we are only allowing run results through that have a TTFB of less than 2.2 seconds. Here&#39;s an updated &lt;a href=&quot;https://gist.github.com/Nooshu/ceac9b8ab43b27c2e2f27630369b5589&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; with the minor code changes.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Filtering out the results that are obvious errors gives us a much better looking graph.&quot; decoding=&quot;async&quot; height=&quot;425&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/CgIp7ouzLo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/CgIp7ouzLo-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/CgIp7ouzLo-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/CgIp7ouzLo-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see from the graph above, the vertical axis is a lot more applicable to our overall results, thus giving us a better idea of how the metrics fluctuate over time.&lt;/p&gt;&lt;h3 id=&quot;examining-cls-information&quot;&gt;Examining CLS information&lt;/h3&gt;&lt;p&gt;There&#39;s been a lot of talk recently about Cumulative Layout Shifts (CLS), since it is now considered part of Googles &lt;a href=&quot;https://web.dev/vitals/&quot;&gt;Core Web Vitals&lt;/a&gt;. In the future a poor CLS result could actually hurt your search engine page rank (at least with Google). Thankfully WebPageTest monitors a page for layout shifts and gives detailed information about when they happen and the score for each shift. We can use this information to plot the layout shifts for each run, and see how they vary between them:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.data.runs | to_entries[].value.firstView | [.LayoutShifts[].score] | @csv&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; cls-scores.csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is a relatively simple filter:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;.data.runs | to_entries[].value.firstView&lt;/code&gt; - drill down into the runs data and convert each run into a set of data objects with a key and a value (and remove from the array). Then extract the runs first view data.&lt;/li&gt;&lt;li&gt;&lt;code&gt;[.LayoutShifts[].score]&lt;/code&gt; - look at all the layout shift information, unwrap from the array and pull out each score, wrap in an array for the CSV formatting (CSV&#39;s require an array an an input)&lt;/li&gt;&lt;li&gt;&lt;code&gt;@csv&lt;/code&gt; - pipe the final array wrapped data into the CSV formatter&lt;/li&gt;&lt;li&gt;&lt;code&gt;&gt; cls-scores.csv&lt;/code&gt; - output to a file&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Plotting this data in Google Sheets gives you a graph that looks like this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Individual CLS data for each run plotted on a bar graph for CNN.com. The difference between the runs is quite staggering.&quot; decoding=&quot;async&quot; height=&quot;357&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/VfnceI71ZA-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/VfnceI71ZA-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/VfnceI71ZA-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/VfnceI71ZA-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The site tested above is the old web performance community favorite: CNN.com. 21 runs using Chrome on a cable connection. Each column on the x-axis shows an individual run. The y-axis shows the cumulative CLS score for that run. Each of the colours is a separate &#39;shift&#39; that occurred during page load. What strikes me about this graph is the difference between each of the runs, with the lowest cumulative score of around 0.04, and the highest just under 0.8! If Google is going to be factoring CLS into search rankings (presumably from user RUM data), I&#39;d personally want this score to be a little more stable!&lt;/p&gt;&lt;p&gt;This graph could be used by a team as a prioritisation exercise. The blue column is large and prominent in each of the runs, so this would be worth investigating as a priority to see if this shift can be reduced.&lt;/p&gt;&lt;h3 id=&quot;more-examples-to-follow&quot;&gt;More examples to follow...&lt;/h3&gt;&lt;p&gt;As I come up with other examples of data to extract from the API using &lt;code&gt;jq&lt;/code&gt;, I will post them here. If you have a handy &lt;code&gt;jq&lt;/code&gt; snippet you are willing to share please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;, full credit will be given to you.&lt;/p&gt;&lt;h2 id=&quot;tools-to-help-manipulate-json-data-with-jq&quot;&gt;Tools to help manipulate JSON data with &lt;code&gt;jq&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;So how do you manipulate megabytes of JSON data quickly? Because, I have to be honest there&#39;s a lot of trial and error with &lt;code&gt;jq&lt;/code&gt; (at least for me). Oftentimes what I think will happen with a particular filter, doesn&#39;t. So the ability to tweak filters and see the result quickly is key to using &lt;code&gt;jq&lt;/code&gt;. And as a &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code&lt;/a&gt; user there are a number of extensions available to optimise &lt;code&gt;jq&lt;/code&gt; workflows. The fact that WebPageTest outputs such large JSON blobs makes it quite a resource intensive use case for these extensions. So let&#39;s examine how well they work.&lt;/p&gt;&lt;h3 id=&quot;visual-code-jq-playground&quot;&gt;Visual Code jq playground&lt;/h3&gt;&lt;p&gt;Visual Code jq playground is the most feature complete extension of the three that I&#39;ve looked at. It has great documentation and examples, and also has the ability to show the results in an editor window or the &#39;Output&#39; tab. It managed to return the manipulated results from a 50 MB JSON blob really quickly.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Visual Code jq playground has lots of great features, like the option for where the results are output as seen in the image.&quot; decoding=&quot;async&quot; height=&quot;593&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PFJxoTeYch-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PFJxoTeYch-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PFJxoTeYch-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/PFJxoTeYch-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The major downside that I have found with this extension is that it only works on file types with a &lt;code&gt;.jq&lt;/code&gt; extension. So when you have a huge JSON blob from WPT, this needs to be renamed to &lt;code&gt;blob.jq&lt;/code&gt;. Once done you type your filter directly above the JSON (as seen in the image). You can then show the result in the console, or another editor window. I&#39;m not a fan of both of these workflow requirements. If this extension could be filtered via a separate input box, and it worked on &lt;code&gt;.json&lt;/code&gt; files, then this extension would be almost perfect. Find the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=davidnussio.vscode-jq-playground&quot;&gt;Visual Code jq playground here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;vscode-jq&quot;&gt;vscode-jq&lt;/h3&gt;&lt;p&gt;At first it took me a little while to figure out how to use this extension as the documentation is non-existent. You need to open the &#39;Command Pallet..&#39; then type &lt;code&gt;&gt;jq&lt;/code&gt;, at which point you will be given an empty input box to enter your filter string. Press &#39;Enter&#39; and the result will be displayed in the &#39;Output&#39; panel at the bottom of the window (see image below).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;vscode-jq outputs the results at the bottom of the screen. It is quick, but the space at the bottom is very limited.&quot; decoding=&quot;async&quot; height=&quot;638&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/YJGioHzOW8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/YJGioHzOW8-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/YJGioHzOW8-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/YJGioHzOW8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I tested the plugin with very large JSON files, all the way up to 50 MB and it worked perfectly. It was also very quick to return a result considering the size of some of the files. There are a couple of downsides though: The &#39;output&#39; tab at the bottom of the window is very limited in terms of vertical size. And as JSON files tend to spread over thousands of lines this can be quite frustrating. Second, there&#39;s no JSON syntax highlighting in the &#39;output&#39; tab, so it can be quite hard to read the output. And finally, the history of previous filters isn&#39;t saved. Considering the trial and error aspect of &lt;code&gt;jq&lt;/code&gt; filters, this is a real shame. Having to retype the whole filter again for a minor change means debugging is slower than it needs to be. All in all, I love the speed and JSON size capacity, it&#39;s just missing a few features from my workflow to be perfect. Find the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=dandric.vscode-jq&quot;&gt;vscode-jq extension here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;jq-live-view&quot;&gt;jq Live View&lt;/h3&gt;&lt;p&gt;This extension looked really promising. Upon opening a JSON file you get a small &#39;jq&#39; icon in the top right of the window which you click to bring up the filter input. Input your filter, press &#39;enter&#39; and a new file window pops up with the result. It also remembers your history of filters which is a nice touch.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;jq live view VS Code extension is simple to use. Screen splits in 2 with the resulting JSON displayed on the right hand side.&quot; decoding=&quot;async&quot; height=&quot;547&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/ieguT8ymgg-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/ieguT8ymgg-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/ieguT8ymgg-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/ieguT8ymgg-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Unfortunately it comes with a major flaw when it comes to working with WebPageTest JSON blobs. I could only get it to work with files that were less than 1 MB in size. The error &lt;code&gt;stdout maxBuffer length exceeded with large files&lt;/code&gt; raised its ugly head with files larger than 1 MB. To put that into context, passing this site&#39;s homepage through WebPageTest with 3 runs creates a 950 KB blob (which worked). Upping that to 5 runs creates a 1.4 MB blob (which failed). I&#39;m going to investigate and see if there is a way to fix this issue, as I really like the extension and think it is almost perfect. Quick and easy to use, just not recommended for WPT blobs at the moment! Find the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ldd-vs-code.jq-preview&quot;&gt;jq Live View extension here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;jq-play-a-playground-for-jq-1-6&quot;&gt;jq&gt;play - A playground for jq 1.6&lt;/h3&gt;&lt;p&gt;To be clear, this isn&#39;t an extension for VS Code, it is a separate website created by the author of &lt;code&gt;jq&lt;/code&gt; that allows you to manipulate JSON in (almost) real time. It gives you a number of pointers below the main panels as to what manipulation options are available. But be warned: clicking on one of these will clear your current filter text!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;jq play has a clean and simple interface for tweaking your filters.&quot; decoding=&quot;async&quot; height=&quot;409&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/LIuABSqy9e-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/LIuABSqy9e-300.webp 300w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/LIuABSqy9e-600.webp 600w, https://nooshu.com/blog/2020/10/09/using-jq-with-the-webpagetest-api/LIuABSqy9e-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It&#39;s a fantastic website for quickly seeing what a filter does to pasted JSON code. But it does come with a few limitations: The JSON file size limit is 10 MB, so no super massive JSON blobs allowed. There&#39;s also no input delay on the filter input text box, so while you are typing you will likely see compilation errors due to an incomplete filter. This can be very annoying, especially when filtering a large amount of data, as it takes a number of seconds to complete. A setting to toggle this functionality on and off and replace it with a submit button would solve this issue. So if you do decide to use this tool I highly recommend configuring your filter on a JSON blob with a low number of runs first, then transfer this over to the &lt;code&gt;jq&lt;/code&gt; command on the command-line.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;So in this blog post we&#39;ve looked at the WebPageTest API, figured out how to reduce the amount of data to something more manageable using &lt;code&gt;jq&lt;/code&gt;, and looked over a few examples as to how &lt;code&gt;jq&lt;/code&gt; can be used to extract specific web performance data for further examination. Finally we looked at a few tools to help with the writing of &lt;code&gt;jq&lt;/code&gt; filters when manipulating JSON data. I hope you found this post useful. It really has only scratched the surface of what is possible with &lt;code&gt;jq&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If you have any go to filters of your own, or any feedback on the ones written here please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;. Thanks for reading.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;09/10/20: Initial post published.&lt;/li&gt;&lt;li&gt;09/10/20: Post updated with details about the new &lt;code&gt;normalizekeys=1&lt;/code&gt; parameter &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; has added to WebPageTest to strip out non-alphanumeric characters from the JSON response. Thanks Pat!&lt;/li&gt;&lt;li&gt;14/10/20: Added link to the &lt;a href=&quot;https://github.com/Nooshu/wpt-jq-data-scripts&quot;&gt;&#39;wpt-jq-data-scripts&#39;&lt;/a&gt; repo with all the scripts and more for people to use. Feedback and ideas welcome!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Why you should be testing your 404 pages web performance</title>
    <link href="https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/" />
    <updated>2020-08-25T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/</id>
    <content type="html">&lt;p&gt;So some recent web performance monitoring work for GOV.UK piqued my interest in the web performance of 404 pages. In this post I&#39;m going to go into some of my thoughts around this and the finer details of existing 404 pages on the web today.&lt;/p&gt;&lt;h2 id=&quot;anatomy-of-a-404-page&quot;&gt;Anatomy of a 404 page&lt;/h2&gt;&lt;p&gt;So before I start, let&#39;s get the basics out of the way. What is a 404? Well I&#39;d hazard a guess that the 404 status code is the most well known of &lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_HTTP_status_codes&quot;&gt;all the HTTP status codes&lt;/a&gt;. If you were to ask a random person on the street what a 404 is, I&#39;d bet many could guess as to what it is. Maybe something along the lines of &quot;it&#39;s when the website is broken&quot;, or &quot;the page is broken&quot;.&lt;/p&gt;&lt;p&gt;Let&#39;s take a look at what the Internet Engineering Task Force (IETF) RFC 7231 has to say &lt;a href=&quot;https://tools.ietf.org/html/rfc7231#section-6.5.4&quot;&gt;about the 404 status code&lt;/a&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The 404 (Not Found) status code indicates that the origin server did not find a current representation for the target resource or is not willing to disclose that one exists.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So saying the website or page is broken is close, but it isn&#39;t always about &#39;pages&#39;, as the rfc mentions the word &#39;resource&#39;. The user will most likely see a 404 as a broken page. A browser on the other hand could be seeing 404&#39;s for a huge number of reasons, and in many cases a user won&#39;t even realise this. I can&#39;t stress this point enough: &lt;strong&gt;404 responses aren&#39;t just caused by a user mistyping a URL&lt;/strong&gt;, there&#39;s a whole lot more to it than that. I&#39;ll back this claim up with data later in the post.&lt;/p&gt;&lt;h3 id=&quot;what-decides-what-is-sent-in-a-404-response&quot;&gt;What decides what is sent in a 404 response&lt;/h3&gt;&lt;p&gt;When a browser requests a resource from the server, and the server doesn&#39;t have it available, what decides what is sent back to the browser? Well multiple representations of a resource can sit on the same URL. The process for selecting which resource is the best representation to be used and sent back to the browser is called &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation&quot;&gt;content negotiation&lt;/a&gt;. The MDN documentation explains it well. The key part is:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The server uses [HTTP headers] as hints and an internal algorithm chooses the best content to serve to the client. The algorithm is server-specific and not defined in the standard.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So the browser gives the server as much information as it can about the type of content it supports and what it is expecting (as a hint), then it is the decision of the particular server&#39;s algorithm exactly what is in the response. And this is where the selection happens. It then depends on how the server is configured as to if a custom error page is served (e.g. site branding, more natural languages, site CSS and JS), or if the servers default &#39;404 Not Found page&#39; is used.&lt;/p&gt;&lt;h2 id=&quot;collecting-and-refining-the-data&quot;&gt;Collecting and refining the data&lt;/h2&gt;&lt;p&gt;In terms of 404 status data, thankfully there&#39;s an awesome project that has been running for a number of years called the &lt;a href=&quot;https://httparchive.org/&quot;&gt;HTTP Archive&lt;/a&gt;, which tracks how the web is built. Every month, &lt;a href=&quot;https://httparchive.org/reports/state-of-the-web#numUrls&quot;&gt;millions of URLs&lt;/a&gt; are &#39;crawled&#39; and a huge amount of data is captured and stored that can then be used by the web community to learn about the state of the web today, and how it is changing over time. This data just so happens to include error data too! So we can take a look at the current state of 404 pages across the web in great detail.&lt;/p&gt;&lt;p&gt;Using a fairly straightforward SQL query we can pull out the response body size of all the 404 pages in the HTTPArchive as of June 2020:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;#standardSQL
select status, respBodySize, reqBodySize, url
from httparchive.summary_requests.2020_06_01_mobile
where status = 404
order by respBodySize desc
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Big thank you for &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for the query and pulling the data from the archive. Just to give you an idea of how much data comes back: it gives you a CSV file containing 3,382,962 rows, and is around 290MB uncompressed! Given the maximum file size for a CSV in Google Sheets is 20MB, and there is a cell limit of 5 million cells, we&#39;re going to need to reduce the amount of data by a lot at some point in this post!&lt;/p&gt;&lt;h3 id=&quot;full-dataset-breakdown&quot;&gt;Full dataset breakdown&lt;/h3&gt;&lt;p&gt;Before I refine the dataset to make it more manageable, let&#39;s gather some high-level information about where all these 404 errors are coming from. First thing to do is to de-duplicate the data, making sure any repeated URLs are removed. For many sites you&#39;re going to get the same 404 repeated across multiple pages. Doing this removes 443,080 rows from the CSV, so not a bad start! You can &lt;a href=&quot;https://docs.google.com/spreadsheets/d/190LCHz5xWKXSA-EI1p6A3xOFeNci8Drr-M3YsZVhFtM/edit?usp=sharing&quot;&gt;find a Google Sheet&lt;/a&gt; with my basic analysis of the full dataset if you&#39;d prefer to view it there.&lt;/p&gt;&lt;p&gt;First let&#39;s see the file types that are missing which are causing the 404 errors:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Filetype&lt;/th&gt;&lt;th&gt;Number of occurrences&lt;/th&gt;&lt;th&gt;% of total&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;*.ico&lt;/td&gt;&lt;td&gt;639,344&lt;/td&gt;&lt;td&gt;21.75&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.jpg&lt;/td&gt;&lt;td&gt;618,335&lt;/td&gt;&lt;td&gt;21.03&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.png&lt;/td&gt;&lt;td&gt;526,360&lt;/td&gt;&lt;td&gt;17.90&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.js&lt;/td&gt;&lt;td&gt;298,193&lt;/td&gt;&lt;td&gt;10.14&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.css&lt;/td&gt;&lt;td&gt;148,884&lt;/td&gt;&lt;td&gt;5.06&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.gif&lt;/td&gt;&lt;td&gt;144,576&lt;/td&gt;&lt;td&gt;4.92&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.woff2&lt;/td&gt;&lt;td&gt;54,758&lt;/td&gt;&lt;td&gt;1.86&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.woff&lt;/td&gt;&lt;td&gt;53,202&lt;/td&gt;&lt;td&gt;1.81&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Unsurprisingly image files are the main cause of 404 errors. But the top filetype is &lt;code&gt;*.ico&lt;/code&gt;. Any guesses as to why this is? Winner gets the prize of one lovely &lt;code&gt;favicon.ico&lt;/code&gt; to download.&lt;/p&gt;&lt;p&gt;What about if we look for common files in these 404 errors:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Filename&lt;/th&gt;&lt;th&gt;Number of occurrences&lt;/th&gt;&lt;th&gt;% of total&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;favicon.ico&lt;/td&gt;&lt;td&gt;632,384&lt;/td&gt;&lt;td&gt;21.51&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ajax-loader.gif&lt;/td&gt;&lt;td&gt;19,855&lt;/td&gt;&lt;td&gt;0.68&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;favicon.png&lt;/td&gt;&lt;td&gt;18,247&lt;/td&gt;&lt;td&gt;0.62&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;manifest.json&lt;/td&gt;&lt;td&gt;11,411&lt;/td&gt;&lt;td&gt;0.39&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;site.webmanifest&lt;/td&gt;&lt;td&gt;4,783&lt;/td&gt;&lt;td&gt;0.16&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;sw.js&lt;/td&gt;&lt;td&gt;1,859&lt;/td&gt;&lt;td&gt;0.06&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spacer.gif&lt;/td&gt;&lt;td&gt;1,375&lt;/td&gt;&lt;td&gt;0.05&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Favicon.ico&lt;/td&gt;&lt;td&gt;295&lt;/td&gt;&lt;td&gt;0.01&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;So I think we&#39;ve confirmed the answer as to why the &lt;code&gt;*.ico&lt;/code&gt; filetype has such a large number of occurrences. That&#39;s 21.51% of the unique URLs listed in the full dataset are caused by missing favicons!&lt;/p&gt;&lt;p&gt;Finally let&#39;s look for common URL patterns that are causing 404 errors. These are just a few that I pulled out that I found quite interesting:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;URL Pattern&lt;/th&gt;&lt;th&gt;Number of occurrences&lt;/th&gt;&lt;th&gt;% of total&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&quot;/wp-content/uploads/&quot;&lt;/td&gt;&lt;td&gt;375,123&lt;/td&gt;&lt;td&gt;12.76&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/wp-content/themes/&quot;&lt;/td&gt;&lt;td&gt;136,303&lt;/td&gt;&lt;td&gt;4.64&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;apis.google.com&quot;&lt;/td&gt;&lt;td&gt;71,264&lt;/td&gt;&lt;td&gt;2.42&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/wp-content/plugins/&quot;&lt;/td&gt;&lt;td&gt;65,967&lt;/td&gt;&lt;td&gt;2.24&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;cdn.shopify.com&quot;&lt;/td&gt;&lt;td&gt;42,871&lt;/td&gt;&lt;td&gt;1.46&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;fontawesome-webfont&quot;&lt;/td&gt;&lt;td&gt;33,916&lt;/td&gt;&lt;td&gt;1.15&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/undefined&quot;&lt;/td&gt;&lt;td&gt;22,482&lt;/td&gt;&lt;td&gt;0.76&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/null&quot;&lt;/td&gt;&lt;td&gt;9,540&lt;/td&gt;&lt;td&gt;0.32&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;.gov.uk&quot;&lt;/td&gt;&lt;td&gt;971&lt;/td&gt;&lt;td&gt;0.03&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;.nhs.uk&quot;&lt;/td&gt;&lt;td&gt;551&lt;/td&gt;&lt;td&gt;0.02&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/Array&quot;&lt;/td&gt;&lt;td&gt;470&lt;/td&gt;&lt;td&gt;0.02&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;[object%20Object]&quot;&lt;/td&gt;&lt;td&gt;292&lt;/td&gt;&lt;td&gt;0.01&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/NaN&quot;&lt;/td&gt;&lt;td&gt;153&lt;/td&gt;&lt;td&gt;0.01&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;The top two URL patterns are related to WordPress, which to be honest isn&#39;t very surprising, considering according to the &lt;a href=&quot;https://almanac.httparchive.org/en/2019/&quot;&gt;2019 Web Almanac&lt;/a&gt; the most popular CMS (by far) &lt;a href=&quot;https://almanac.httparchive.org/en/2019/cms#fig-5&quot;&gt;is WordPress&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;refined-dataset-breakdown&quot;&gt;Refined dataset breakdown&lt;/h3&gt;&lt;p&gt;So the full dataset with 3,382,962 rows is far too big to manipulate and analyse (for me using Google Sheets), so moving forwards I decided to reduce the original dataset by:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;removing any URLs with a response body size of less that 100KB&lt;/li&gt;&lt;li&gt;again de-duplicated the remaining urls to make sure they were unique&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This reduced 3,382,962 rows down to a much more manageable 11,621 rows. Why 100KB? Good question. It just seemed like a fairly round number which gave a suitably large number of results remaining. I personnel think 100KB for a 404 page is still quite large (but much more on this later). If you are interested in the resulting raw data, you can find the Google Sheet &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1dapOBnmVTKS8pr9hSO2VkZ6cKWRw88hevIb6KJCWkmI/edit?usp=sharing&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;NOTE: For readers curious about how to access HTTPArchive data, I highly recommend reading &#39;&lt;a href=&quot;https://github.com/HTTPArchive/httparchive.org/blob/master/docs/gettingstarted_bigquery.md&quot;&gt;Getting Started Accessing the HTTP Archive with BigQuery&lt;/a&gt;&#39; written by &lt;a href=&quot;https://twitter.com/paulcalvano&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Paul Calvano&lt;/a&gt;, and &#39;&lt;a href=&quot;https://timkadlec.com/remembers/2019-12-10-using-bigquery-without-breaking-the-bank/&quot;&gt;Using BigQuery without breaking the bank&lt;/a&gt;&#39; by &lt;a href=&quot;https://twitter.com/tkadlec&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tim Kadlec&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So for the top 11,621 URLs (in terms of response size) that were 404 errors, what file types are missing that are causing the errors:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Filetype&lt;/th&gt;&lt;th&gt;Number of occurrences&lt;/th&gt;&lt;th&gt;% of total&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;*.png&lt;/td&gt;&lt;td&gt;2,201&lt;/td&gt;&lt;td&gt;18.94&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.jpg&lt;/td&gt;&lt;td&gt;2,120&lt;/td&gt;&lt;td&gt;18.24&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.js&lt;/td&gt;&lt;td&gt;1,962&lt;/td&gt;&lt;td&gt;16.88&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.ico&lt;/td&gt;&lt;td&gt;1,836&lt;/td&gt;&lt;td&gt;15.80&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.css&lt;/td&gt;&lt;td&gt;533&lt;/td&gt;&lt;td&gt;4.59&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.gif&lt;/td&gt;&lt;td&gt;392&lt;/td&gt;&lt;td&gt;3.37&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.json&lt;/td&gt;&lt;td&gt;348&lt;/td&gt;&lt;td&gt;2.99&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;*.woff&lt;/td&gt;&lt;td&gt;285&lt;/td&gt;&lt;td&gt;2.45&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Icon files are no longer top of the list in the refined dataset, but images and scripts are.&lt;/p&gt;&lt;p&gt;What about if we again look for patterns in common files causing these 404 errors:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Filename&lt;/th&gt;&lt;th&gt;Number of occurrences&lt;/th&gt;&lt;th&gt;% of total&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;favicon.ico&lt;/td&gt;&lt;td&gt;1,757&lt;/td&gt;&lt;td&gt;15.12&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;manifest.json&lt;/td&gt;&lt;td&gt;259&lt;/td&gt;&lt;td&gt;2.23&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;favicon.png&lt;/td&gt;&lt;td&gt;230&lt;/td&gt;&lt;td&gt;1.98&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;site.webmanifest&lt;/td&gt;&lt;td&gt;112&lt;/td&gt;&lt;td&gt;0.96&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ajax-loader.gif&lt;/td&gt;&lt;td&gt;94&lt;/td&gt;&lt;td&gt;0.81&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;serviceworker.js&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;0.10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;sw.js&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;0.07&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Favicon.ico&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;0.06&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;manifest.webmanifest&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;0.06&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spacer.gif&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;So even in our refined dataset the &lt;code&gt;favicon.ico&lt;/code&gt; file creates real problems!&lt;/p&gt;&lt;p&gt;Finally what about common URL patterns causing the 404 errors:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;URL Pattern&lt;/th&gt;&lt;th&gt;Number of occurrences&lt;/th&gt;&lt;th&gt;% of total&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&quot;/wp-content/uploads/&quot;&lt;/td&gt;&lt;td&gt;2,284&lt;/td&gt;&lt;td&gt;19.65&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/wp-content/themes/&quot;&lt;/td&gt;&lt;td&gt;1,294&lt;/td&gt;&lt;td&gt;11.14&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/wp-content/plugins/&quot;&lt;/td&gt;&lt;td&gt;1,116&lt;/td&gt;&lt;td&gt;9.60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/undefined&quot;&lt;/td&gt;&lt;td&gt;255&lt;/td&gt;&lt;td&gt;2.19&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;fontawesome-webfont&quot;&lt;/td&gt;&lt;td&gt;166&lt;/td&gt;&lt;td&gt;1.43&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/null&quot;&lt;/td&gt;&lt;td&gt;33&lt;/td&gt;&lt;td&gt;0.28&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/Array&quot;&lt;/td&gt;&lt;td&gt;23&lt;/td&gt;&lt;td&gt;0.20&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;.gov.uk&quot;&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;0.02&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;apis.google.com&quot;&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;cdn.shopify.com&quot;&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;.nhs.uk&quot;&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;[object%20Object]&quot;&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;/NaN&quot;&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Again, the top entries are all related to the WordPress CMS. A Google Sheet with my basic analysis of the refined dataset can be &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1AsSNLBo5RDEInJk9DS9lNKlol2ZOd_QUXsxkMQVq8U0/edit?usp=sharing&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;response-sizes&quot;&gt;Response sizes&lt;/h2&gt;&lt;p&gt;A really important aspect of these 404 responses I&#39;ve only briefly touched on so far (other than the 100KB limit) is the actual response body size that is being sent to a clients browser. When a user visits a site&#39;s 404 page, either directly or unseen via the browser requesting a resource that doesn&#39;t exist on the server, the server is going to respond with a 404 status code and the associated 404 page. Looking at the refined dataset gives you some pretty staggering examples of 404 response bodies that are quite frankly huge. There are 118 examples of 404 pages that are 1MB+. 17 examples where the response body size is 2MB+. And the award for the largest 404 response body size is a whopping 6.7MB!&lt;/p&gt;&lt;h3 id=&quot;huge-404-page-waterfall&quot;&gt;Huge 404 page waterfall&lt;/h3&gt;&lt;p&gt;So what does a WebPageTest waterfall chart look like for one of these sites that has a huge 404 page response? I&#39;ve picked the 2nd largest on the list for the moment as it is a very basic direct example. I will examine the first place website later in the post, as it comes with quite a few quirks! To force WebPageTest to run a test on a 404 page I&#39;ve had to follow the instructions &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-test-a-404-pages-web-performance&quot;&gt;I have written here&lt;/a&gt;, as if you don&#39;t WebPageTest will simply abort the test (it is giving a 404 error response after all, so it&#39;s to be expected).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see WPT being forced to run a test for 20 seconds, and the page is still yet to finish loading!&quot; decoding=&quot;async&quot; height=&quot;378&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/4OF298jq36-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/4OF298jq36-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/4OF298jq36-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/4OF298jq36-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In this particular instance the HTML makes up 85.4% of the total page weight, or 7.5MB on its own! This looks to be mainly inline CSS within the page. After forcing the test to run for 20 seconds the page still hadn&#39;t fully loaded. My guess is that there&#39;s some broken server code or a plug-in that has gone off the rails and is injecting all this CSS multiple times and increasing the page weight by megabytes! The fact that I forced the test to run for so long, and it had only just finished loading suggests to me that the size logged in the HTTP Archive data is actually smaller than it should be!&lt;/p&gt;&lt;p&gt;That is one huge 404 page! And given that this is a page that ultimately a user doesn&#39;t actually want to be on, that&#39;s a lot of wasted time, data, and device resources!&lt;/p&gt;&lt;h2 id=&quot;examining-the-breakdowns&quot;&gt;Examining the breakdowns&lt;/h2&gt;&lt;p&gt;Now that we have the broken down what is causing the 404 error data, what conclusions can be drawn from it?&lt;/p&gt;&lt;h3 id=&quot;favicons&quot;&gt;Favicons&lt;/h3&gt;&lt;p&gt;21.5% of the unique URLs in the complete data set came from a missing &lt;code&gt;favicon.ico&lt;/code&gt;. Refining the data down to the top 11,621 URLs with the largest response bodies reveals that 1757 (or 15.1%) URLs on that list show the same issue. It just so happens that the largest 404 page in the whole data set is being triggered because of a missing &lt;code&gt;favicon.ico&lt;/code&gt; on the server. By default &lt;a href=&quot;https://en.wikipedia.org/wiki/Favicon#How_to_use&quot;&gt;a browser automatically looks&lt;/a&gt; for the &lt;code&gt;favicon.ico&lt;/code&gt; in the websites root directory (if a &lt;code&gt;&amp;lt;link rel=&quot;icon&quot;&gt;&lt;/code&gt; isn&#39;t present). If no icon is found, a server will respond with a 404 page. In the case of the largest 404 page a 6.7MB response from the server is sent to the client. This is on top of any data actually needed by the browser to render the page! All the time this is happening the user has no idea. It&#39;s not like they see a broken image. The page is just incredibly slow to load. And to top it all off this happens for every page on the site (as the browser is still looking for that elusive &lt;code&gt;favicon.ico&lt;/code&gt; it wants).&lt;/p&gt;&lt;p&gt;You can see this happening via a WebPageTest waterfall on a cable connection using Firefox. And yes, you are reading that correct, 17.3 seconds to load the 404 response, amounting to 44 seconds for the full page load!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Favicon triggering a huge 404 response from the server. That&#39;s an additional 6.7MB on top of the page response in Firefox!&quot; decoding=&quot;async&quot; height=&quot;297&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/5ASRoqujY3-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/5ASRoqujY3-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/5ASRoqujY3-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/5ASRoqujY3-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The impact of missing favicons on web performance &lt;a href=&quot;https://love2dev.com/blog/web-performance-tip-make-sure-you-have-a-favicon/&quot;&gt;has been written about before&lt;/a&gt;, and under certain conditions can cause you &lt;a href=&quot;https://allinthehead.com/retro/359/how-a-missing-favicon-broke-my-app-for-chrome-users&quot;&gt;hours of debugging&lt;/a&gt;. So do yourself and your users a favour, as a bare minimum stick the most basic &lt;code&gt;favicon.ico&lt;/code&gt; at your server root.&lt;/p&gt;&lt;h3 id=&quot;wordpress-requests&quot;&gt;Wordpress requests&lt;/h3&gt;&lt;p&gt;The &quot;/wp-content/uploads/&quot; URLs are understandable in many cases. A content author uploads an image, maybe modifies the post or the image is renamed / deleted from the server, thus creating the 404 error.&lt;/p&gt;&lt;p&gt;It&#39;s the &quot;/wp-content/themes/&quot; and &quot;/wp-content/plugins/&quot; URLs that really worry me. WordPress has an excellent upgrade mechanism for both plugins and themes: simply click a button and it does it all for you. Great! But what you are essentially doing is then trusting that the developers of &lt;em&gt;every&lt;/em&gt; plugin and theme you use on your site aren&#39;t now injecting a broken resource link into your pages! If they do, you aren&#39;t likely to notice it unless you check the browser console, it breaks functionality in some way, or you happen to be monitoring your server logs for 404 responses. But when that broken resource is injected into every page, and a 404 status code and page is sent, that&#39;s when it could effect web performance. And as you can see from both sets of data this isn&#39;t a rare occurrence, it is happening a lot for WordPress sites!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A broken WordPress plugin is causing a 404 and making a user download an extra 288KB.&quot; decoding=&quot;async&quot; height=&quot;324&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/LhSD-ZywmW-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/LhSD-ZywmW-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/LhSD-ZywmW-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/LhSD-ZywmW-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above example we see a page trying to load a broken WordPress plugin called WaveSurfer-WP, only the JavaScript file is missing causing the browser to download an additional 288KB!&lt;/p&gt;&lt;h3 id=&quot;javascript&quot;&gt;JavaScript&lt;/h3&gt;&lt;p&gt;Both datasets show a high percentage of broken JavaScript requests that are generating 404 errors. But it&#39;s actually worse than that. At least if a whole JavaScript file fails to load you are likely to notice it. But what about if you are using JavaScript to generate URLs for a particular component on your site. It then downloads assets from your server in a very Ajax fashion, but the JavaScript doing this fails in some way. This isn&#39;t hypothetical, we can actually see it happening in the HTTP Archive data in these URL patterns:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&quot;/undefined&quot;&lt;/li&gt;&lt;li&gt;&quot;/null&quot;&lt;/li&gt;&lt;li&gt;&quot;/Array&quot;&lt;/li&gt;&lt;li&gt;&quot;/NaN&quot;&lt;/li&gt;&lt;li&gt;&quot;[object%20Object]&quot;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These are all URLs being generated using JavaScript which are failing for some reason. And in doing so they are making a request to the server for a resource that doesn&#39;t exist. At which point the server responds with a 404 status and the corresponding page page. So unless you have a rigorous set of tests to catch these edge cases in your JavaScript, these 404&#39;s could be occurring and you&#39;d have very little knowledge of them happening. The user certainly isn&#39;t likely to check their browser console, so unless it breaks the site completely, they may not even know it is happening.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Broken JS implementation is causing a 404 download of almost 300KB&quot; decoding=&quot;async&quot; height=&quot;114&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/1uiP8JwHpK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/1uiP8JwHpK-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/1uiP8JwHpK-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/1uiP8JwHpK-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above example we see some broken JavaScript requesting a resource from &#39;/undefined&#39;, and the browser receives a 294KB response back.&lt;/p&gt;&lt;h3 id=&quot;standard-files&quot;&gt;Standard files&lt;/h3&gt;&lt;p&gt;Outside of the usual &lt;code&gt;favicon.ico&lt;/code&gt; file, a whole range of files appear in many of the 404&#39;s logged, and they are all manually inserted by developers:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&quot;favicon.png&quot;&lt;/li&gt;&lt;li&gt;&quot;ajax-loader.gif&quot;&lt;/li&gt;&lt;li&gt;&quot;manifest.json&quot;&lt;/li&gt;&lt;li&gt;&quot;site.webmanifest&quot;&lt;/li&gt;&lt;li&gt;&quot;manifest.webmanifest&quot;&lt;/li&gt;&lt;li&gt;&quot;sw.js&quot;&lt;/li&gt;&lt;li&gt;&quot;serviceworker.js&quot;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Proving mistakes do happen. Even though a developer has added the &lt;code&gt;&amp;lt;link rel=&quot;manifest&quot; href=&quot;/manifest.webmanifest&quot;&gt;&lt;/code&gt; to the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;, the actual manifest file has been forgotten about. Or maybe it was added but then deleted in a future release by mistake. Who knows. Again in both instances causing a 404 response on all pages it sits on (maybe all of them!).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A request for a favicon.png causes a 285KB response from the server. Response even comes back as a 200, even though it is a 404.&quot; decoding=&quot;async&quot; height=&quot;338&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/rJAb9-KWYp-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/rJAb9-KWYp-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/rJAb9-KWYp-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/rJAb9-KWYp-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above example the &lt;code&gt;favicon.png&lt;/code&gt; file doesn&#39;t exist on the server, so it returns a 285KB 404 page. But it also sends the page with a 200 response code, with a 404 response body. This is known as a &#39;&lt;a href=&quot;https://en.wikipedia.org/wiki/HTTP_404#Soft_404_errors&quot;&gt;soft 404&lt;/a&gt;&#39;, and can be the result of incorrectly configured server software.&lt;/p&gt;&lt;h3 id=&quot;presentational-assets&quot;&gt;Presentational assets&lt;/h3&gt;&lt;p&gt;I was actually quite surprised at how many CSS and font files (WOFF2, WOFF, TTF) were causing 404 errors in the datasets. Of all the assets that are likely to display presentational issues (and therefore likely be noticeable to both developers and users alike), these would be it. But it could be that these CSS and font files are only being used for particular pages or site components, so maybe hidden away in depths of the site. Or if you have a good CSS font stack configured, the use of a web font may not always be noticeable (which begs the question why load it in the first place). In any event, these filetypes appear a huge number of times across the full dataset, so it&#39;s quite a common occurrence.&lt;/p&gt;&lt;p&gt;In the example below a missing CSS file triggers a 270KB 404 page response:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A broken CSS file can trigger a huge 404 page response, 270KB in this instance.&quot; decoding=&quot;async&quot; height=&quot;214&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/wboA2Kn0vT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/wboA2Kn0vT-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/wboA2Kn0vT-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/wboA2Kn0vT-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Ultimately with each of the five examples of potential causes of 404&#39;s listed above, there&#39;s a chance that an unoptimised 404 response will cost the user dearly, both in terms of web performance and also device resource and data usage.&lt;/p&gt;&lt;h2 id=&quot;what-about-testing&quot;&gt;What about testing?&lt;/h2&gt;&lt;p&gt;So here&#39;s the thing about synthetic monitoring and automated performance testing of 404 pages: it&#39;s actually almost impossible to do with many services at the moment (if you know of any, please &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;). The reason being is that many synthetic performance testing tools are built upon either &lt;a href=&quot;https://developers.google.com/web/tools/lighthouse&quot;&gt;Google Lighthouse&lt;/a&gt; or &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;. As an example, let&#39;s see what happens with these tools if we try to audit a 404 page:&lt;/p&gt;&lt;h3 id=&quot;lighthouse&quot;&gt;Lighthouse&lt;/h3&gt;&lt;p&gt;Running a Lighthouse audit on a 404 page causes the tool to abort the process with no data being captured about the page. This is fully understandable really, it is an error page after all! But it&#39;s a shame there isn&#39;t an option to override this implementation detail and force the collection of data if a 404 status code is encountered.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Google Lighthouse aborts on encountering a 404 status code and no data is captured.&quot; decoding=&quot;async&quot; height=&quot;757&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/L9ncI4kNXM-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/L9ncI4kNXM-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/L9ncI4kNXM-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/L9ncI4kNXM-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;webpagetest&quot;&gt;WebPageTest&lt;/h3&gt;&lt;p&gt;This is the default setup for WebPageTest, and the waterfall that is generated if you point it at a 404 page. Now there is a way to force WebPageTest to capture data from a page &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-test-a-404-pages-web-performance&quot;&gt;when it encounters a 404 status code&lt;/a&gt;. It&#39;s a bit of a hack but it does work.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;WebPageTest default settings return a very empty looking waterfall, with no assets outside the standard HTML being captured.&quot; decoding=&quot;async&quot; height=&quot;212&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/2rIRetV7tk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/2rIRetV7tk-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/2rIRetV7tk-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/2rIRetV7tk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;browser-devtools&quot;&gt;Browser DevTools&lt;/h3&gt;&lt;p&gt;Your best bet for examining the asset sizes for your 404 pages at the moment is to resort to your browser developer tools. Take a look in the network tab and see what is being loaded, and approximately how long it is taking. You could even try &lt;a href=&quot;https://github.com/sitespeedio/throttle&quot;&gt;throttling your network connection&lt;/a&gt; to observe just how long it takes for the 404 page to be loaded and rendered.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using Firefox developer tools to examine the responses of a 404 page.&quot; decoding=&quot;async&quot; height=&quot;476&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/NEj3bd3WM8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/NEj3bd3WM8-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/NEj3bd3WM8-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/NEj3bd3WM8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;404-caching&quot;&gt;404 Caching&lt;/h2&gt;&lt;p&gt;Using the browser developer tools it is possible examine and optimise the caching strategy used on your 404 page. There&#39;s two types of caching to consider when it comes to 404 pages: CDN cache and local browser cache.&lt;/p&gt;&lt;h3 id=&quot;cdn-cache&quot;&gt;CDN cache&lt;/h3&gt;&lt;p&gt;The thing about hitting a 404 page is that there&#39;s a high possibility that this request is going to be sent straight to your origin server. But if you place a CDN in front of your origin, you have the chance to cache the 404 page response on the CDN. This is a good idea since without it, hitting a 404 page could be used as a DoS attack vector, by forcing the origin server to respond to every 404 request rather than letting the CDN respond with a cached version.&lt;/p&gt;&lt;p&gt;Check the documentation for your specific CDN to see how they handle caching of 404 pages by default, as each CDN has their own strategy for caching 4xx status codes received from an origin server.&lt;/p&gt;&lt;h3 id=&quot;local-browser-cache&quot;&gt;Local browser cache&lt;/h3&gt;&lt;p&gt;Should a browser cache the HTML page and assets when it encounters a 404 page? Caching and cache correctness related to 4xx responses is actually specifically mentioned in &lt;a href=&quot;https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1&quot;&gt;rfc 2616 - Hypertext Transfer Protocol -- HTTP/1.1&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;A correct cache MUST respond to a request with the most up-to-date response held by the cache that is appropriate to the request which meets [one of] the following conditions: It is an appropriate 304 (Not Modified), 305 (Proxy Redirect), or error (4xx or 5xx) response message.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So there&#39;s nothing technically stopping you from setting &lt;code&gt;Cache-Control&lt;/code&gt; headers for your 404 page and associated assets and requesting that a browser cache them.&lt;/p&gt;&lt;h3 id=&quot;design-considerations-and-caching&quot;&gt;Design considerations and caching&lt;/h3&gt;&lt;p&gt;Now you may be asking: why on earth would I want to cache 404 page assets? Most users will likely visit a 404 page and then immediately hit the browser back button.&lt;/p&gt;&lt;p&gt;That will be true in many cases, but think about it this way: the assets have already been downloaded so why not request to cache them. Then if you can offer a user an easy way to get to the rest of your site (e.g. homepage link, search functionality, popular articles call to action), the 404 page could actually be used to warm the cache for the rest of the site visit. This initial failure could be converted into a web performance win, should a user decide to explore the rest of the site.&lt;/p&gt;&lt;p&gt;Users encountering your 404 page should be factored into your user journey planning, because at some point they are going to see it. So take the time to consider how users can recover from their mistake (or the mistake of another person e.g. mistyping a URL in a blog post).&lt;/p&gt;&lt;h2 id=&quot;possible-solutions&quot;&gt;Possible solutions&lt;/h2&gt;&lt;p&gt;So there are a couple of possible solutions to help fix issues with 404 pages on your site:&lt;/p&gt;&lt;h3 id=&quot;favicon-and-monitoring&quot;&gt;Favicon and monitoring&lt;/h3&gt;&lt;p&gt;First is make sure you have a &lt;code&gt;favicon.ico&lt;/code&gt; file at the web root of your server. It doesn&#39;t even need to be of anything, it could simply be transparent. Just as long as it is present and the server can serve it if it is requested. Next step is setup monitoring so you can see where 404&#39;s are occurring on your site so they can be fixed. This can be done using a &lt;a href=&quot;https://www.matthewedgar.net/find-and-fix-404-errors/#tools&quot;&gt;variety of methods&lt;/a&gt;, from Web log analysis to the Google Search Console.&lt;/p&gt;&lt;h3 id=&quot;correct-server-setup&quot;&gt;Correct server setup&lt;/h3&gt;&lt;p&gt;But you could take it further. Ideally you only want to be serving HTML to the browser when it expects a HTML response. And for all other responses serve a small error payload. How this is setup depends on the web server or CDN being used. Listed below are example configs for &lt;a href=&quot;https://www.nginx.com/&quot;&gt;NGINX&lt;/a&gt; and &lt;a href=&quot;https://httpd.apache.org/&quot;&gt;Apache&lt;/a&gt;, where as a default we are setting the custom error page, then for specific locations we are serving the small 404 response (assuming the files don&#39;t exist).&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# NGINX&lt;/span&gt;
location ~* &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;.&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ico&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;jpg&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;jpeg&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;png&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gif&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;svg&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;js&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;css&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;swf&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;eot&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;ttf&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;otf&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;woff&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;woff2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;$ &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    error_page &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt; /small_404_response.txt&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
error_page  &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt; /custom_404.html&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# ApacheConf&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Location ~ &lt;span class=&quot;token string&quot;&gt;&quot;(&#92;.ico|&#92;.jpg|&#92;.jpeg|&#92;.png|&#92;.gif|&#92;.svg|&#92;.js|&#92;.css|&#92;.swf|&#92;.eot|&#92;.ttf|&#92;.otf|&#92;.woff|&#92;.woff2)$&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
	ErrorDocument &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;404 Not Found&quot;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;/Location&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
ErrorDocument &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt; /custom_404_page.html&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can complete the same task using various different methods in Apache and NGINX, the above is just one example.&lt;/p&gt;&lt;p&gt;You can actually see this in action by looking at how Facebook has their servers setup.&lt;/p&gt;&lt;p&gt;Here we aren&#39;t matching against a particular location (e.g. &lt;code&gt;https://www.facebook.com/test.&lt;/code&gt;). Because of this a full HTML response is being served:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using Firefox developer tools to examine the responses of a 404 page.&quot; decoding=&quot;async&quot; height=&quot;217&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/aeRAgRy9o5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/aeRAgRy9o5-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/aeRAgRy9o5-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/aeRAgRy9o5-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;But if we request a location that matches a particular file extension (e.g. &lt;code&gt;https://www.facebook.com/test.jpg&lt;/code&gt;) the 404 response is much smaller:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using Firefox developer tools to examine the responses of a 404 page.&quot; decoding=&quot;async&quot; height=&quot;106&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/XkWjesG8sk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/XkWjesG8sk-300.webp 300w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/XkWjesG8sk-600.webp 600w, https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/XkWjesG8sk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;A 72 byte response saying &#39;Not Found&#39; then a link back to Facebook.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Assuming that an optimised 404 page is only required because users will mistype a URL in their browser is short-sighted. As the HTTP Archive data has shown, there are many other reasons why a user may encounter a 404 response (even if they have no idea they actually are!). The web performance impact of a users browser loading an unoptimised 404 page can be huge, and it can have a real impact on their experience of your whole site. All it takes is a forgotten file or misplaced &lt;code&gt;;&lt;/code&gt; in some JavaScript, and your users could be encountering it.&lt;/p&gt;&lt;p&gt;Automated web performance monitoring of 404 pages is very difficult at the moment, as most tools don&#39;t capture data when encountering a 404 status code. So it&#39;s likely you will need to check your 404 page manually using a browsers developer tools. I&#39;d love to see this change in the future and see tools implement some form of &#39;404 mode&#39; for testing 404 pages. It&#39;s really important that users aren&#39;t penalised for a mistake (either theirs or that of another person).&lt;/p&gt;&lt;p&gt;So why not check the state of your sites 404 page right now? &lt;a href=&quot;https://twitter.com/nhoizey/status/1298610618187100160&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Check your server logs&lt;/a&gt; so spot where 404&#39;s are happening across your site. Because I guarantee at some point a user is going to encounter one, even if they don&#39;t realise it.&lt;/p&gt;&lt;h3 id=&quot;june-2024-update&quot;&gt;June 2024 update&lt;/h3&gt;&lt;p&gt;As of &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/commit/9832e9636be55d21af3c5365c73ac0eb489f551e&quot;&gt;October 2023&lt;/a&gt; this functionality was available in Lighthouse by using the &lt;code&gt;ignoreStatusCode&lt;/code&gt; option. And as of the 5th June this is also available in &lt;a href=&quot;https://pagespeed.web.dev/&quot;&gt;PageSpeed Insights&lt;/a&gt;. See a sample 404 report &lt;a href=&quot;https://pagespeed.web.dev/analysis/http-www-example-com-dir/fqxhjfrjz3?form_factor=mobile&quot;&gt;here&lt;/a&gt;. For more information on this change see &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/issues/10493&quot;&gt;issue 10493&lt;/a&gt; in the &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/&quot;&gt;Google Chrome Lighthouse GitHub repository&lt;/a&gt;. Many thanks to &lt;a href=&quot;https://toot.cafe/@paul_irish&quot;&gt;Paul Irish&lt;/a&gt; and the whole of the Google Lighthouse team for adding this feature!&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;25/08/20: Initial post published.&lt;/li&gt;&lt;li&gt;26/08/20: Minor adjustments / clarifications, thanks &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt;. This is why you always get someone else to proofread before publishing!&lt;/li&gt;&lt;li&gt;27/08/20: Added information about &#39;soft 404s&#39; and the &#39;What decides what is sent in a 404 response&#39; section. Horizontal scrolling of wide tables is now possible across the whole site (Thanks to &lt;a href=&quot;https://twitter.com/edwardhorsford&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Ed Horsford&lt;/a&gt; for reporting)&lt;/li&gt;&lt;li&gt;28/08/20: Added a section about possible solutions related to 404 page monitoring, and serving a smaller response depending on the location the client requests. Thanks &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/paulcalvano&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Paul Calvano&lt;/a&gt; for the feedback.&lt;/li&gt;&lt;li&gt;07/07/24: Added an update from &lt;a href=&quot;https://toot.cafe/@paul_irish&quot;&gt;Paul Irish&lt;/a&gt; about support for 404&#39;s in Lighthouse and PageSpeed Insights. Thanks Paul!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>A &#39;Vary&#39; unusual waterfall</title>
    <link href="https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/" />
    <updated>2020-08-12T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/</id>
    <content type="html">&lt;p&gt;So as I&#39;ve mentioned a &lt;a href=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/&quot;&gt;few times before&lt;/a&gt; on this blog, we (GDS) use &lt;a href=&quot;https://speedcurve.com&quot;&gt;SpeedCurve&lt;/a&gt; for our synthetic web performance monitoring. We&#39;ve been using it for a while, but in a very ad-hoc fashion. Recently I finally got round to setting up a whole set of monitoring on our services to let us know when things change (both positive and negative). So back in April an alert appeared for one of our services: &lt;a href=&quot;https://www.notifications.service.gov.uk/&quot;&gt;GOV.UK Notify&lt;/a&gt;. Seemingly overnight without warning the font size in Chrome jumped from 157KB, to 331KB. That&#39;s a whopping 110% increase!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Notify font size doubled overnight without warning. Graph shows the steep increase.&quot; decoding=&quot;async&quot; height=&quot;348&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/nCqPcbA0Y_-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/nCqPcbA0Y_-300.webp 300w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/nCqPcbA0Y_-600.webp 600w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/nCqPcbA0Y_-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Checking in with the team, nothing had changed with the code that could cause this for quite some time. Delving into the details a little further we could see that the number of font requests had jumped from 5 before, to 8 after. Very unusual considering no code had changed to trigger it.&lt;/p&gt;&lt;h2 id=&quot;the-waterfall&quot;&gt;The waterfall&lt;/h2&gt;&lt;p&gt;One of the great features about SpeedCurve is the ability to jump directly into the underlying test data, since it is all powered by &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;, it&#39;s a tool I&#39;m a &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;little more familiar with&lt;/a&gt;. But the waterfall I was presented with was one I&#39;d never seen before:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The unusual waterfall from the WebPageTest results driving the SpeedCurve graphs. Shows 4 font downloads rather than 2, with the first 2 requests failing.&quot; decoding=&quot;async&quot; height=&quot;295&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/LJjf8P_88z-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/LJjf8P_88z-300.webp 300w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/LJjf8P_88z-600.webp 600w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/LJjf8P_88z-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;On first sight the browser is requesting both WOFF2 and WOFF fonts at the same time. Looking a little closer requests 6 and 7 are very strange. Both are requests by the browser for the WOFF2 fonts. On examination you can see the server attempting to send the browser (Chrome) the font files. This is indicated by the dark red bands (known as chunks). But the strange thing is the independent, unconnected bands. It looked as though the server sends some data, stops for 10-20ms, then tries again. Eventually the sending of data fails, the fonts are not received and the browser moves on.&lt;/p&gt;&lt;h2 id=&quot;the-fallback-woff-fonts&quot;&gt;The fallback WOFF fonts&lt;/h2&gt;&lt;p&gt;The intriguing part for me is looking at where the &#39;fallback&#39; WOFF fonts are requested by the browser:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Notice how the WOFF font request starts just as the browser receives WOFF2 data.&quot; decoding=&quot;async&quot; height=&quot;295&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/CreDk5eVds-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/CreDk5eVds-300.webp 300w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/CreDk5eVds-600.webp 600w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/CreDk5eVds-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice how just as the browser receives the first WOFF2 font data it immediately initiates the request for the equivalent WOFF fonts. My interpretation of this is that as soon as the browser sees the WOFF2 data, it knows there is an issue and that it can&#39;t use the data, so it immediately requests the fallback. This happens for both WOFF2 fonts on the page. Assuming my interpretation is correct, once the WOFF2 files have been requested they will be using up bandwidth, and if the browser can&#39;t use the data they will be wasting that bandwidth. By the time the browser cancels the font requests (HTTP/2 only), the fonts may have already arrived (as seen in the waterfall above).&lt;/p&gt;&lt;h2 id=&quot;historic-data&quot;&gt;Historic data&lt;/h2&gt;&lt;p&gt;Thankfully we&#39;d had SpeedCurve setup on a few Notify URLs for approximately 6 months, so we had a large amount of data to look back on. In examining historic data about the font size, an interesting pattern emerged:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Viewing historic data it could be seen that the font size for Notify was up and down like a yo-yo.&quot; decoding=&quot;async&quot; height=&quot;348&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/0zOLpcAhps-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/0zOLpcAhps-300.webp 300w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/0zOLpcAhps-600.webp 600w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/0zOLpcAhps-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This font size &#39;change&#39; had happened at least 25 times in the past 6 months, then mysteriously reset itself. It&#39;s also possible to see in the data a few &#39;half jumps&#39;, where only one of the WOFF2 font files had the issue, and the other downloaded fine. I concluded from this that it wasn&#39;t anything the team were doing, this was something else much more random.&lt;/p&gt;&lt;p&gt;But what really triggered the investigation was that after the 21st April, the font size became permanently stuck (as can be seen to the right of the graph).&lt;/p&gt;&lt;h2 id=&quot;more-investigation&quot;&gt;More investigation&lt;/h2&gt;&lt;p&gt;Chatting to the team further they mentioned something quite intriguing. While developing the site they occasionally ran into seemingly random console errors. They&#39;d pop-up for a single load, then immediately disappear after a refresh, meaning they had no chance to debug the issue or consistently reproduce it to allow them to fix it:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Chrome browser console error referred to a CORS error about Access-Control-Allow-Origin.&quot; decoding=&quot;async&quot; height=&quot;234&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/sW3PpIxnwf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/sW3PpIxnwf-300.webp 300w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/sW3PpIxnwf-600.webp 600w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/sW3PpIxnwf-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see above, it&#39;s a Cross-Origin Resource Sharing (CORS) error related to the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header on both WOFF2 fonts. This gave us the first indication as to what the issue was.&lt;/p&gt;&lt;h2 id=&quot;so-what-changed&quot;&gt;So what changed?&lt;/h2&gt;&lt;p&gt;Another great point about SpeedCurve, since it is all built upon WebPageTest, under all the useful graphs you can delve into an incredible amount of information about the page load process. Given that it&#39;s possible to look at the point before the issue happened, then the point after it happened, it&#39;s possible to compare test results and look for clues. Since it looked to be CORS related the obvious place to look was the response headers for the WOFF2 files. This is what a &lt;a href=&quot;https://gist.githubusercontent.com/Nooshu/4706427b4e182a9a6215ad6d6ef565c6/raw/81c7e7dfb7819b3976b8f966075b6e4924e7af27/success.txt&quot;&gt;successful response&lt;/a&gt; looked like, and here&#39;s the &lt;a href=&quot;https://gist.githubusercontent.com/Nooshu/4706427b4e182a9a6215ad6d6ef565c6/raw/81c7e7dfb7819b3976b8f966075b6e4924e7af27/failed.txt&quot;&gt;failed response&lt;/a&gt;, both for the same file. Just a few initial observations when comparing the headers:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Both responses came from the same CDN point of presence (PoP): &lt;code&gt;LHR3-C1&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;code&gt;content-length&lt;/code&gt; and the &lt;code&gt;etag&lt;/code&gt; values are both exactly the same, so the binary file itself hasn&#39;t changed (byte-for-byte identical since a &lt;a href=&quot;https://en.wikipedia.org/wiki/HTTP_ETag#Strong_and_weak_validation&quot;&gt;strong ETag validator&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;The &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;last-modified&lt;/code&gt;, and &lt;code&gt;age&lt;/code&gt; headers &lt;strong&gt;have&lt;/strong&gt; changed.&lt;/li&gt;&lt;li&gt;The failed response is missing all &lt;code&gt;access-control-*&lt;/code&gt; headers.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The &lt;code&gt;access-control-*&lt;/code&gt; headers are CORS headers. So CORS rears its ugly head again. The &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;last-modified&lt;/code&gt;, and &lt;code&gt;age&lt;/code&gt; headers indicate that although the file itself may not have changed (in terms of bytes), the surrounding metadata has. And given the &lt;code&gt;last-modified&lt;/code&gt; value was now &lt;code&gt;Tue, 21 Apr 2020 14:32:51 GMT&lt;/code&gt;, the date our issue appeared, I&#39;d guess they are related!&lt;/p&gt;&lt;h2 id=&quot;caching-on-a-cdn&quot;&gt;Caching on a CDN&lt;/h2&gt;&lt;p&gt;So GOV.UK Notify hosts all of their static assets on the Amazon CloudFront content delivery network (CDN). In order to minimise latency for users, CDN&#39;s will cache requests on their own servers. This takes the load off a website&#39;s origin server, and allows a CDN to serve assets to a user from a PoP that is geographically closest to them. This is because lower latency generally equals &lt;a href=&quot;https://hpbn.co/building-blocks-of-tcp/#slow-start&quot;&gt;better web performance&lt;/a&gt;. But there&#39;s a popular saying in Computer Science:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Caching is a difficult problem to solve. Getting something into cache is easy, but making sure that it is still an accurate representation of the original at all times is difficult! So what if that&#39;s what has happened here. The version stored in the CDN cache is in some way broken and it is being served to the client, and the client is rejecting it. This actually happens a lot, sometimes by accident, other times used maliciously as a security exploit. This is known as web cache poisoning. The theory being that by injecting compromised code into a CDN&#39;s cache, it could then be served up to all users. If you are interested in finding out a lot more about this topic I highly recommend reading &#39;&lt;a href=&quot;https://portswigger.net/research/practical-web-cache-poisoning&quot;&gt;Practical Web Cache Poisoning&lt;/a&gt;&#39; by &lt;a href=&quot;https://twitter.com/albinowax&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;James Kettle&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Just to be clear, I don&#39;t think what happened is malicious in any way, shape, or form. It just highlights that this could be the issue, and gives it a name for further investigation. A quick Google for &#39;CloudFront no Access-Control headers&#39; very quickly brings you to this &lt;a href=&quot;https://serverfault.com/a/856948&quot;&gt;incredibly detailed post on Serverfault&lt;/a&gt; all about it. Reading through that post quickly leads you to a possible solution: the &lt;code&gt;vary&lt;/code&gt; header.&lt;/p&gt;&lt;h2 id=&quot;the-vary-header&quot;&gt;The &lt;code&gt;vary&lt;/code&gt; header&lt;/h2&gt;&lt;p&gt;So let&#39;s briefly chat about the &lt;code&gt;vary&lt;/code&gt; header. CDN&#39;s use what is called a cache key to check to see if the request being made for a file and the file that already exists in the CDN cache is the file the browser is expecting. For that reason you want your cache key for a particular resource to be unique. That way you can avoid cache collision, where an outdated version of a file is served to a user. Default cache key settings differ depending on the CDN being used, but they can often be customised and configured by a developer. For example, the default cache key could be the complete Uniform Resource Identifier (URI) for a particular request. So say we have a file already cached on a CDN, &lt;code&gt;https://nooshu.github.io/images/fake-image.jpg&lt;/code&gt; and we make a request for it using the following &lt;a href=&quot;https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#URI_references&quot;&gt;URI reference&lt;/a&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Request for &lt;code&gt;https://nooshu.github.io/images/fake-image.jpg&lt;/code&gt; = match&lt;/li&gt;&lt;li&gt;Request for &lt;code&gt;http://nooshu.github.io/images/fake-image.jpg&lt;/code&gt; = no match (HTTP, not HTTPS)&lt;/li&gt;&lt;li&gt;Request for &lt;code&gt;/images/fake-image.jpg&lt;/code&gt; = no match (Missing scheme and authority)&lt;/li&gt;&lt;li&gt;Request for &lt;code&gt;https://nooshu.github.io/images/fake-image.jpg?cachebuser=123&lt;/code&gt; = no match (URI queries matter)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;What the &lt;code&gt;vary&lt;/code&gt; header allows you to do is add specific client request headers to also be &#39;keyed&#39; by the cache. So for example, if you wanted to make sure a CDN is always serving up the correct version of a file depending on its compression type, you would specify &lt;code&gt;vary: Accept-Encoding&lt;/code&gt; in the response. The CDN would then examine a future clients &lt;code&gt;Accept-Encoding&lt;/code&gt; header to determine which file to serve from its cache. If the client requests uncompressed, it serves an uncompressed version from cache. If the client requests a gzipped version, it gets a gzipped version from cache. If a version being requested doesn&#39;t exist in the CDN cache, it will go back to the origin server for a copy (then save it for the next client who requests it).&lt;/p&gt;&lt;h3 id=&quot;vary-origin&quot;&gt;&lt;code&gt;vary&lt;/code&gt; origin&lt;/h3&gt;&lt;p&gt;So what is the &lt;code&gt;vary: origin&lt;/code&gt; response header doing? It is saying to the client that &quot;when I serve a file to you, I&#39;m taking into account the &lt;code&gt;origin&lt;/code&gt; header from which the request is being made from.&quot; So for example, if two requests are being made for the same file but the origin header is different for each request, multiple versions of the same file will now exist in the CDN cache. Should another client come along and make the request from the same origin, they get the version of the file associated with that origin only.&lt;/p&gt;&lt;p&gt;Font&#39;s (for reasons we won&#39;t go into) &lt;a href=&quot;https://www.w3.org/TR/css-fonts-4/#font-fetching-requirements&quot;&gt;need the &lt;code&gt;access-control-allow-origin&lt;/code&gt; header&lt;/a&gt; to be set when loaded as part of a page. This can either be a wildcard, or the request origin reflected back. &lt;code&gt;origin&lt;/code&gt; headers are only sent with CORS and &lt;code&gt;POST&lt;/code&gt; requests, so the CORS request to the WOFF2 fonts that sit on the GOV.UK Notify assets domain already contains an &lt;code&gt;origin&lt;/code&gt; header. So what would happen if we didn&#39;t set up the CDN to consider this &lt;code&gt;origin&lt;/code&gt; header in the request when caching font files on the CDN? Well the CDN could be serving a version of the font files (including headers) to a client that is missing vital information it needs to use them e.g. missing CORS headers (&lt;code&gt;access-control-*&lt;/code&gt;).&lt;/p&gt;&lt;h2 id=&quot;so-what-exactly-happened-with-the-cdn-cache&quot;&gt;So what exactly happened with the CDN cache?&lt;/h2&gt;&lt;p&gt;Well that&#39;s the million-dollar (pound) question isn&#39;t it. And unfortunately &lt;em&gt;why&lt;/em&gt; exactly this was happening and &lt;em&gt;what&lt;/em&gt; triggering it is still a complete mystery to me. But the theory I&#39;m going with is this:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;em&gt;something&lt;/em&gt; triggered the CDN cache to be cleared, meaning the WOFF2 requests no longer existed in the cache&lt;/li&gt;&lt;li&gt;A request was made by &lt;em&gt;someone&lt;/em&gt; for the WOFF2 fonts that primed the cache with a non-CORS version of the font response e.g. by someone directly navigating to the file (a navigation request is non-CORS)&lt;/li&gt;&lt;li&gt;Because the the &lt;code&gt;Origin&lt;/code&gt; isn&#39;t being considered as a cache key (&lt;code&gt;Vary: Origin&lt;/code&gt;), this non-CORS version is then served back to the Chrome client without the correct &lt;code&gt;access-control-*&lt;/code&gt; headers&lt;/li&gt;&lt;li&gt;No CORS headers set on the font response is going to cause issues, leading to the WOFF2 font not being used by Chrome&lt;/li&gt;&lt;li&gt;The browser immediately requests the fallback WOFF fonts which are then used (assuming they are cached correctly)&lt;/li&gt;&lt;li&gt;SpeedCurve sees the additional font requests (even though the fonts weren&#39;t actually used) and it updates its graphs accordingly&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&quot;cache-persistence&quot;&gt;Cache persistence&lt;/h3&gt;&lt;p&gt;Another strange thing I noticed was that this issue was easily reproducible on the public instance of WebPageTest. All you needed to do was select the &#39;London, UK - EC2 - Chrome&#39; instance. But when I selected any other instance it didn&#39;t show the issue. I found this interesting because SpeedCurve run their own &lt;a href=&quot;https://speedcurve.com/blog/faster-ec2-testing-agents-for-speedcurve/&quot;&gt;WebPageTest instances on Amazon EC2&lt;/a&gt;. How is it that 2 separate WebPageTest instances on EC2 are displaying the same issue, but other instances are fine. On examining the response headers it is easy to see why:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;SpeedCurve instance (failed response): &lt;code&gt;x-amz-cf-pop: LHR3-C1&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Public EC2 instance (failed response): &lt;code&gt;x-amz-cf-pop: LHR3-C1&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Public non-EC2 instance (successful request): &lt;code&gt;x-amz-cf-pop: LHR61-C2&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The two EC2 instances (both in London) were receiving responses from the same CloudFront edge node (&lt;code&gt;LHR3-C1&lt;/code&gt;), whereas the other public non-EC2 instance was receiving the request from another edge node (&lt;code&gt;LHR61-C2&lt;/code&gt;). And this is what was happening every single day when the test was run. The SpeedCurve instance was hitting the same edge node which had the poisoned cache, hence the reason why the graph was constantly showing the issue. Same Chrome browser, same EC2 instance every day with the same poisoned cache (with long-lived cache headers &lt;code&gt;max-age=315360000, immutable&lt;/code&gt;) is going to show the same issue (unless the CDN cache is cleared).&lt;/p&gt;&lt;h2 id=&quot;re-examining-the-waterfall&quot;&gt;Re-examining the waterfall&lt;/h2&gt;&lt;p&gt;Now that we have a bit more of an idea about what is going on, lets re-examine the original waterfall to pinpoint what is happening:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Annotated waterfall, with exactly what is happening (see following paragraph)&quot; decoding=&quot;async&quot; height=&quot;146&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/WeAJNexjYa-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/WeAJNexjYa-300.webp 300w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/WeAJNexjYa-600.webp 600w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/WeAJNexjYa-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;First the WOFF2 font files are requested by the browser after a new TCP connection is established&lt;/li&gt;&lt;li&gt;A very thin slither of data is received (most likely the headers)&lt;/li&gt;&lt;li&gt;The browser immediately cancels the request as there are no required CORS headers. This explains the &#39;empty space&#39; between the data.&lt;/li&gt;&lt;li&gt;The browser immediately requests the next fonts in the &lt;code&gt;@font-face&lt;/code&gt; declaration (requests 8 &amp; 9)&lt;/li&gt;&lt;li&gt;The browser still receives chunks of WOFF2 data because this data was already in transit over the network or sitting in one of many buffers along the way.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;It never ceases to amaze me the amount of information you can get from a waterfall chart once you know what to look for!&lt;/p&gt;&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;&lt;p&gt;Well after all the discussion above the actual solution is very simple (in theory). Add &lt;code&gt;vary: Origin&lt;/code&gt; to the response for the web fonts and purge CloudFront cache on every node. Unfortunately it&#39;s not always that simple. Looking at the &lt;a href=&quot;https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#ResponseCustomRemovedHeaders&quot;&gt;CloudFront documentation&lt;/a&gt; on how it handles &lt;code&gt;vary&lt;/code&gt; headers, they have specific rules around what is automatically removed. But it is &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#headers&quot;&gt;still possible&lt;/a&gt;. Once merged and deployed the issue was fixed:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Notify font size doubled overnight without warning. With the fix deployed and the cache cleared it drops right back down.&quot; decoding=&quot;async&quot; height=&quot;348&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/20nHSYNHpp-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/20nHSYNHpp-300.webp 300w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/20nHSYNHpp-600.webp 600w, https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/20nHSYNHpp-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now admittedly this isn&#39;t the most exciting graph ever, but it does show that it immediately fixed the issue, bringing the number of font requests and the total font size down to the correct value after being stuck there for a number of months. And from all the discussion above, this should now have solved the issue for good. No more random CORS errors for Chrome users and potential wasted bytes on fonts that couldn&#39;t actually be used.&lt;/p&gt;&lt;h2 id=&quot;learnings&quot;&gt;Learnings&lt;/h2&gt;&lt;p&gt;So there are a couple of learnings here:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Make sure you setup monitoring and alerting for your website across a number of different browser configurations. Without SpeedCurve graphs to inspect and investigate, we would still be trying to reproduce the random CORS error in the browser.&lt;/li&gt;&lt;li&gt;When making a CORS request for fonts, make sure the &lt;code&gt;vary: Origin&lt;/code&gt; header is set.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;On point 2, it is worth mentioning that this issue is such a common occurrence it is specifically mentioned in the &lt;a href=&quot;https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches&quot;&gt;Fetch specification&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Well that was a lot of investigation work for such a small change, but it just goes to show how difficult it is to debug and fix &lt;a href=&quot;https://en.wikipedia.org/wiki/There_are_known_knowns&quot;&gt;unknown unknowns&lt;/a&gt;. Without fully understanding the detailed ins and outs of caching on a CDN, and how CORS requests for fonts are &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=855713#c4&quot;&gt;handled within Chrome&lt;/a&gt;, how would a developer ever know that simply adding the &lt;code&gt;vary: Origin&lt;/code&gt; header will solve this random error message that occasionally pops up in the browser console.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;12/08/20: Initial post published. Thank you to the GOV.UK Notify team developers (&lt;a href=&quot;https://twitter.com/quis&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Chris&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/tombaromba&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Tom&lt;/a&gt;, and &lt;a href=&quot;https://twitter.com/ThisIsDMcDonald&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;David&lt;/a&gt;) for their time investigating this issue with me and rolling out the fix. Big thank you to &lt;a href=&quot;https://twitter.com/gaelmetais&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Gaël Métais&lt;/a&gt; for spotting the original issue, and &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for answering lots of my questions!&lt;/li&gt;&lt;li&gt;13/08/20: Fix a mistake I made around the ACAO header not accepting a wildcard for fonts. Thanks to &lt;a href=&quot;https://twitter.com/ericlaw&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Eric Lawrence&lt;/a&gt; for flagging!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Measuring and examining TLS 1.3, IPv4, and IPv6 performance</title>
    <link href="https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/" />
    <updated>2020-07-30T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/</id>
    <content type="html">&lt;p&gt;Earlier in the year we enabled Transport Layer Security (TLS) 1.3 on the Fastly point of presence (POPs) for GOV.UK. Fastly have been gradually rolling out a whole set of improvements to the cache nodes in their POPs. This includes a new &lt;a href=&quot;https://h2o.examp1e.net/&quot;&gt;h2o&lt;/a&gt; TLS architecture, which was required for enabling TLS 1.3. I believe with this upgrade it also paves the way for enabling &lt;a href=&quot;https://www.fastly.com/quic-http-3&quot;&gt;HTTP/3 &amp; QUIC&lt;/a&gt; in the future, which should &lt;a href=&quot;https://www.fastly.com/blog/how-http3-and-quic-help-long-tail-connections&quot;&gt;improve performance on connections that suffer from high packet loss&lt;/a&gt; (e.g. unstable, rural connections). A win-win all round! So assuming you are using a modern browser (and aren&#39;t stuck behind a proxy that forces a TLS downgrade), you should see something like this when you next visit GOV.UK:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;GOV.UK now supports TLS 1.3, as can be seen in the certificate information in Firefox.&quot; decoding=&quot;async&quot; height=&quot;609&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/xLScl0ShuU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/xLScl0ShuU-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/xLScl0ShuU-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/xLScl0ShuU-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;TLS 1.3 is the latest version of the Transport Layer Security cryptographic protocol, and it offers a &lt;a href=&quot;https://www.fastly.com/blog/tls13-available&quot;&gt;number of improvements&lt;/a&gt; over previous versions, including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;improved security by removing insecure or less secure ciphers (as well as insecure features)&lt;/li&gt;&lt;li&gt;improved performance due to a reduction in the number of round trips (RTT) to establish a secure connection&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Improved security and web performance, sign me up! So when TLS 1.3 was enabled on the UK POPs for GOV.UK on 15th April, I was a little disappointed to not see anything reflected in our synthetic web performance tests in &lt;a href=&quot;https://speedcurve.com/&quot;&gt;SpeedCurve&lt;/a&gt;, as I&#39;d expected to see a change in the Time to First Byte (TTFB) metric. I thought it may be related to &lt;a href=&quot;https://tools.ietf.org/html/rfc5077&quot;&gt;TLS session resumption&lt;/a&gt;, but WebPageTest gives details about this in a pages HTML &#39;Raw Details&#39; tab, and they are all &lt;code&gt;&quot;tls_resumed&quot;: &quot;False&quot;&lt;/code&gt;. But these results are only from synthetic testing on a limited set of devices and connections, so that&#39;s probably why. I&#39;d guess if we were capturing Real User Monitoring (RUM) data over the same time period, we would have most likely have spotted it. So after reading &lt;a href=&quot;https://twitter.com/chendo&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Chendo&#39;s&lt;/a&gt; blog post &lt;a href=&quot;https://chen.do/tls-v1-3-performance-compared-to-tls-v1-2/&quot;&gt;TLS 1.3 performance compared to TLS 1.2&lt;/a&gt;, it got me thinking: how could I replicate these tests for GOV.UK and verify (purely for my own sake) that TLS 1.3 does in fact offer a web performance improvement for our users. And does IPv4 vs IPv6 have any effect on the results (IPv6 is enabled for GOV.UK). The rest of this blog post goes into how I did this, what was involved and what were the results.&lt;/p&gt;&lt;h2 id=&quot;ways-of-measuring-connection-time&quot;&gt;Ways of measuring connection time&lt;/h2&gt;&lt;p&gt;The first step in this process is being able to collect data about the connection timings. There are a few ways of measuring the time taken for TCP connections to be established. Here are a few I looked into:&lt;/p&gt;&lt;h3 id=&quot;time-command&quot;&gt;Time Command&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;time&lt;/code&gt; command is what Chendo used in the blog post I referred to earlier. The command isn&#39;t specific to timing network activity. In fact it can be used to time any command used on the command line. It&#39;s incredibly simple to use, just prepend &lt;code&gt;time&lt;/code&gt; to the command you wish to time:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-version&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# output: 0.16s user 0.04s system 109% cpu 0.186 total&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above is the output from Zsh: 0.16s CPU time was spent in user mode. 0.04s spent in kernel mode. The task got 109% of the CPU time, and took a total of 0.186 seconds to run. There&#39;s a whole heap of customisations possible which can be seen on the &lt;a href=&quot;https://man7.org/linux/man-pages/man1/time.1.html&quot;&gt;commands man-page&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;curl&quot;&gt;cURL&lt;/h3&gt;&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; is used in command lines or scripts to transfer data. It&#39;s open source and pretty much used everywhere, from cars to mobile phones. The number of options available on its &lt;a href=&quot;https://curl.haxx.se/docs/manpage.html&quot;&gt;man-page is huge&lt;/a&gt;. If you need to transfer data with URLs, then cURL is the tool for you. Thankfully we will only need a very small number of settings: those related to connection timing. &lt;a href=&quot;https://twitter.com/josephscott&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Joseph Scott&lt;/a&gt; wrote an &lt;a href=&quot;https://blog.josephscott.org/2011/10/14/timing-details-with-curl/&quot;&gt;excellent blog post&lt;/a&gt; back in 2011 about how to extract detailed connection timing information using cURL, which can then be used to find things like the TTFB or TLS negotiation time. Joseph has even created a very simple shortcut called &lt;a href=&quot;https://github.com/josephscott/shell/blob/master/bin/curlt&quot;&gt;curlt&lt;/a&gt; that outputs the timing information and response headers for a particular connection.&lt;/p&gt;&lt;h3 id=&quot;httpstat&quot;&gt;HTTPStat&lt;/h3&gt;&lt;p&gt;&lt;code&gt;httpstat&lt;/code&gt; is essentially a visualisation plugin built upon &lt;code&gt;curl&lt;/code&gt;. It takes the &lt;code&gt;curl&lt;/code&gt; connection timing statistics and visualises them in an easy to digest format. &lt;a href=&quot;https://github.com/reorx/httpstat&quot;&gt;Very simple to install&lt;/a&gt; and also outputs all the response headers too, so a very handy tool for basic debugging.&lt;/p&gt;&lt;p&gt;One of the advantages of using cURL on its own is that you have fine-grained control of the output from the command. So for example, if you want to write a large number of results to a file as JSON or a CSV, then this is entirely possible. Given this option, this is the route I decided to take when measuring TLS 1.3 performance.&lt;/p&gt;&lt;h2 id=&quot;testing-performance&quot;&gt;Testing performance&lt;/h2&gt;&lt;p&gt;So there were a few specific points I wanted to cover in testing:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;what&#39;s the impact of the distance from a user to the server when it comes to latency and TLS negotiation time&lt;/li&gt;&lt;li&gt;semi-automate a way to collect a &#39;large&#39; amount of data related to connection metrics&lt;/li&gt;&lt;li&gt;could I spot any difference in connection metrics when using IPv6 or IPv4&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The last point is only really because my ISP has IPv6 available (and is enabled), as does GOV.UK. So I was just curious if anything could be seen in the resulting data when using IPv6 vs IPv4. Does IPv6 offer web performance advantages? There are a &lt;a href=&quot;https://blog.sucuri.net/2016/08/ipv4-vs-ipv6-performance-comparison.html&quot;&gt;number&lt;/a&gt; of &lt;a href=&quot;https://blogs.infoblox.com/ipv6-coe/can-ipv6-really-be-faster-than-ipv4-part-1/&quot;&gt;posts&lt;/a&gt; out &lt;a href=&quot;https://blogs.infoblox.com/ipv6-coe/can-ipv6-really-be-faster-than-ipv4-part-2/&quot;&gt;there&lt;/a&gt; that &lt;a href=&quot;https://teamarin.net/2019/06/25/why-is-ipv6-faster/&quot;&gt;compare performance&lt;/a&gt;. Some seem to suggest there is, others not.&lt;/p&gt;&lt;h3 id=&quot;collecting-results&quot;&gt;Collecting results&lt;/h3&gt;&lt;p&gt;As mentioned before I&#39;m using cURL to capture detailed timing information. I captured a set of data which could then be plotted on a graph for each of the connection scenarios. So lets go through how this is done step by step.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Capture a single request / response&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Using the method Joseph mentions in his &lt;a href=&quot;https://blog.josephscott.org/2011/10/14/timing-details-with-curl/&quot;&gt;blog post&lt;/a&gt;, it is very simple to capture detailed timing information from a URL. Once the &lt;code&gt;curl-format.txt&lt;/code&gt; file has been &lt;a href=&quot;https://gist.github.com/Nooshu/731f2df621a880d6df3d377cb967ea49&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;created&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;, run the following command:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-w&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@curl-format.txt&quot;&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-o&lt;/span&gt; /dev/null &lt;span class=&quot;token variable parameter&quot;&gt;-s&lt;/span&gt; https://www.gov.uk&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The output gives the response headers, as well as the detailed timing information that looks like this:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;time_namelookup&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;      &lt;span class=&quot;token number&quot;&gt;0.045055&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;time_connect&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;         &lt;span class=&quot;token number&quot;&gt;0.058824&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;time_appconnect&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;      &lt;span class=&quot;token number&quot;&gt;0.107569&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;time_pretransfer&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;     &lt;span class=&quot;token number&quot;&gt;0.109468&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;time_redirect&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;        &lt;span class=&quot;token number&quot;&gt;0.000000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;time_starttransfer&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;token number&quot;&gt;0.127577&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property string-property&quot;&gt;&quot;time_total&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;           &lt;span class=&quot;token number&quot;&gt;0.130754&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;What do each of these values mean? They are detailed on the &lt;a href=&quot;https://curl.haxx.se/docs/manpage.html&quot;&gt;cURL man-page&lt;/a&gt;, but I&#39;ve also pulled together a quick visualisation to help you understand it too:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Visualisation of the cURL timings metrics&quot; decoding=&quot;async&quot; height=&quot;443&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7CYP6Lnu1c-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7CYP6Lnu1c-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7CYP6Lnu1c-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7CYP6Lnu1c-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It&#39;s important to remember that many of these metrics are relative to the start of the connection, not the end of the previous activity. This is solved with a little simple data manipulation in the spreadsheets (see &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1EaBjG9scj1FSbUD9BmXuXrKMQNz1UzR9uPyUvPmT1Ao/edit#gid=326484325&quot;&gt;results&lt;/a&gt; for more info on this).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Capture multiple request/responses over time&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Now that we can capture a single response from a URL, what about if we wanted to capture timing information over a period of time? This is where a simple bit of shell script comes in handy. Note that the &lt;code&gt;curl_format&lt;/code&gt; has been moved into the shell script for ease.&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token important shebang&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# create `timing-data.sh` and make it executable (chmod +x timing-data.sh)&lt;/span&gt;

&lt;span class=&quot;token variable assign-left&quot;&gt;curl_format&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;{
	&quot;time_dns&quot;:      	%{time_namelookup},
	&quot;time_connect&quot;:         %{time_connect},
	&quot;time_appconnect&quot;:      %{time_appconnect},
	&quot;time_pretransfer&quot;:     %{time_pretransfer},
	&quot;time_ttfb&quot;:   		%{time_starttransfer},
	&quot;time_total&quot;:   		%{time_total}
}&#39;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# used as headers for the CSV file&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;[&quot;DNS&quot;,&quot;Connect&quot;,&quot;SSL&quot;,&quot;Wait&quot;,&quot;TTFB&quot;,&quot;Total Time&quot;]&#39;&lt;/span&gt;

&lt;span class=&quot;token variable assign-left&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;521&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;((&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;total&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-w&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$curl_format&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-k&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;--compressed&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-o&lt;/span&gt; /dev/null &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$@&lt;/span&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;#space the timings out slightly&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above script is used like so:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;timing-data.sh https://www.gov.uk --tls-max &lt;span class=&quot;token number&quot;&gt;1.2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The script goes off and captures a large number of timing results (in this case 521). I&#39;ve omitted &lt;code&gt;time_redirect&lt;/code&gt; from the above script as I know this will always be 0 in my case, but you could leave it in if you are using on a URL with redirects. The above test took approximately 10 minutes to run. Here I&#39;m capping the TLS version to 1.2 (&lt;code&gt;--tls-max 1.2&lt;/code&gt;), as this allows us to compare different versions. Without this cap, cURL would use the highest version supported by the server, which is 1.3 for &lt;code&gt;www.gov.uk&lt;/code&gt;. So the script above works, but it isn&#39;t capturing the results to a file, or converting it to a format that can be easily imported into a spreadsheet and plotted. It comes out as one huge string of timing info in a JSON object. Time to bring in &lt;a href=&quot;https://stedolan.github.io/jq/&quot;&gt;jq&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://stedolan.github.io/jq/&quot;&gt;jq&lt;/a&gt; is a Swiss army knife for manipulating JSON data on the command line. You can easily transform JSON data into any structure you need:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;timing-data.sh https://www.gov.uk --tls-max &lt;span class=&quot;token number&quot;&gt;1.2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; jq &lt;span class=&quot;token variable parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;[.[]] | @csv&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here I&#39;m taking the JSON output from the cURL response and piping it into &lt;code&gt;jq&lt;/code&gt;. I&#39;m then constructing an array made up of each element in the input (the timing values). Then lastly I&#39;m piping the array into the &lt;code&gt;@csv&lt;/code&gt; formatter to output the values as a CSV. Once converted to a CSV, the data can be easily imported into a spreadsheet (e.g. Google Sheets) for further manipulation and graphing.&lt;/p&gt;&lt;p&gt;Lastly, lets make sure we write all this manipulated data to a file so we can actually use it. A final bit of shell script can do this for us:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# final script&lt;/span&gt;
timing-data.sh https://www.gov.uk --tls-max &lt;span class=&quot;token number&quot;&gt;1.2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; jq &lt;span class=&quot;token variable parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;[.[]] | @csv&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; output-test-results.csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we redirect the output to a file. The file is created automatically if it doesn&#39;t exist.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Capture multiple locations&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The script is now capturing a large amount of connection timing data from a URL, but we have no control over the server cURL is looking at to get this data from. As mentioned above, I&#39;d like to see how a servers geographic location (CDN POP) effects TLS and connection negotiation speed. Thankfully cURL comes with a useful &lt;code&gt;--resolve&lt;/code&gt; option that allows us to do this:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# final script&lt;/span&gt;
timing-data.sh https://www.gov.uk &lt;span class=&quot;token variable parameter&quot;&gt;--resolve&lt;/span&gt; www.gov.uk:443:&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;another_edge_node_ip&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; --tls-max &lt;span class=&quot;token number&quot;&gt;1.2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; jq &lt;span class=&quot;token variable parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;[.[]] | @csv&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; output-test-results.csv&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we are asking cURL to resolve any secure requests for &lt;code&gt;www.gov.uk&lt;/code&gt; to a different IP address. So instead of allowing the CDN to choose the closest geographic location for our request (most likely London for me), we can tell it to request it from elsewhere. For example Sydney, or Vancouver. Why those locations? Well they are 10,585 and 4,663 miles away respectively. Therefore the &lt;a href=&quot;https://hpbn.co/primer-on-latency-and-bandwidth/#speed-of-light-and-propagation-latency&quot;&gt;latency from these locations&lt;/a&gt; will be much higher e.g. higher RTT. So it will be much easier to see the difference between TLS 1.2 and 1.3 (and indeed IPv4 and IPv6 if any can be seen) due to the greater distance to travel.&lt;/p&gt;&lt;p&gt;Of course this is all very contrived, as the very purpose of using a CDN is to minimise the latency between a user and the server, thus giving better web performance. To do this CDN&#39;s use technologies like &lt;a href=&quot;https://www.keycdn.com/support/anycast&quot;&gt;Anycast&lt;/a&gt;, Unicast, Broadcast, and Multicast. The details of which I won&#39;t go into here (because I don&#39;t fully understand them myself!) But this &lt;code&gt;--resolve&lt;/code&gt; method is a great way to test the TLS negotiation improvements in 1.3.&lt;/p&gt;&lt;p&gt;So now we have a method of capturing a large amount of connection timing data from an origin, and we can store it in a format that is easily imported into a graphing tool. For graphing I&#39;m using Google Sheets, as it has lots of options available and is easily sharable.&lt;/p&gt;&lt;h2 id=&quot;testing&quot;&gt;Testing&lt;/h2&gt;&lt;p&gt;In terms of testing I chose three of Fastly&#39;s POPs. UK (London), Canada (Vancouver - YVR), Australia (Sydney - SYD). London to my home is obviously very close (short distance), my home to Vancouver is 4,600 miles (medium distance), and my home to Sydney is 10,500 miles (long distance). For each one I would request the GOV.UK homepage, but force the use of a particular POP. I also forced the use of either IPv4 or IPv6 with the &lt;code&gt;--resolve&lt;/code&gt; option. Both the IPv4 and the IPv6 tests were run concurrently for a particular TLS version, so as to keep the network conditions consistent. Each &lt;code&gt;cURL&lt;/code&gt; command was run 521 times, with a 0.3 second delay between each run (so as to elongate the testing period). Each test took approximately 10 - 15 minutes to complete, with no other significant activity happening over my home network during the runs (so as to minimise last-mile interference).&lt;/p&gt;&lt;p&gt;NOTE: For those wanting to know how IPv4 or IPv6 is chosen in practical terms, there&#39;s a whole RFC called &lt;a href=&quot;https://tools.ietf.org/html/rfc8305&quot;&gt;&#39;Happy Eyeballs Version 2&#39;&lt;/a&gt; dedicated to it. TL;DR: A DNS server provides an A record (IPv4) and an AAAA record (IPv6) for the particular URL you requesting. These are sent at &lt;a href=&quot;https://tools.ietf.org/html/rfc8305#section-3&quot;&gt;&lt;em&gt;almost&lt;/em&gt; the same time&lt;/a&gt;. Of the two DNS queries, the one that completes first is chosen to be the protocol that is used for the connection. But it&#39;s not quite that simple though, as IPv6 is given two advantages:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;the IPv6 AAAA record is sent first before the A record&lt;/li&gt;&lt;li&gt;if the IPv4 A record is received by the client first, there will be a recommended wait of 50ms to allow the AAAA record to complete&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This is essentially trying to migrate internet traffic towards IPv6 (assuming it is available on the clients connection and at the server).&lt;/p&gt;&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;&lt;p&gt;Before I get into the results let me provide you with links so you can interpret them for yourself should you wish to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1EaBjG9scj1FSbUD9BmXuXrKMQNz1UzR9uPyUvPmT1Ao/edit#gid=326484325&quot;&gt;IPv4 TLS 1.2 vs IPv4 TLS 1.3 summary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1EaBjG9scj1FSbUD9BmXuXrKMQNz1UzR9uPyUvPmT1Ao/edit#gid=345823576&quot;&gt;IPv6 TLS 1.2 vs IPv6 TLS 1.3 summary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1EaBjG9scj1FSbUD9BmXuXrKMQNz1UzR9uPyUvPmT1Ao/edit#gid=1496755554&quot;&gt;IPv4 TLS 1.2 vs IPv6 TLS 1.2 summary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1EaBjG9scj1FSbUD9BmXuXrKMQNz1UzR9uPyUvPmT1Ao/edit#gid=683610603&quot;&gt;IPv4 TLS 1.3 vs IPv6 TLS 1.3 summary&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Both the data and larger versions of the graphs can be found in other sheets found on those links. I&#39;ve also compiled the timing difference between TLS versions and protocol versions at the different percentiles, both for the TLS negotiation and the total time (right of the graphs).&lt;/p&gt;&lt;h3 id=&quot;tls-1-2-vs-tls-1-3&quot;&gt;TLS 1.2 vs TLS 1.3&lt;/h3&gt;&lt;p&gt;This is an easy one to call. TLS 1.3 beats 1.2 in all scenarios, by a fairly substantial set of percentages too. Usually between 20-50% improvement at the median TLS negotiation time for both IPv4 and IPv6. It is very easy to see this by looking at the graphs. The purple &#39;SSL handshake&#39; is always a lot thinner with TLS 1.3, meaning it took less time to negotiate the secure connection. This in turn leads onto a significant total time reduction in nearly all results. Usually a reduction between 10-30%. Not a bad saving considering all you need to do is &quot;enable it&quot;!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;See the improvement in SSL negotiation from TLS 1.2 to 1.3 over IPv4, with the purple band on the graph being significantly reduced.&quot; decoding=&quot;async&quot; height=&quot;304&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/yknga16-eo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/yknga16-eo-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/yknga16-eo-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/yknga16-eo-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;See the improvement in SSL negotiation from TLS 1.2 to 1.3 over IPv6, with the purple band on the graph being significantly reduced.&quot; decoding=&quot;async&quot; height=&quot;304&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/lOV_j7_jUB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/lOV_j7_jUB-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/lOV_j7_jUB-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/lOV_j7_jUB-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now this isn&#39;t ground breaking news, as it is &lt;a href=&quot;https://www.cloudflare.com/learning-resources/tls-1-3/&quot;&gt;completely expected&lt;/a&gt;. This fact is touted as one of the advantages of enabling TLS 1.3. But I&#39;m very glad to be able to verify that this is the case for GOV.UK. So users with a browser that supports TLS 1.3 should see a significant improvement in TTFB under many circumstances.&lt;/p&gt;&lt;p&gt;But wait, there&#39;s more! TLS 1.3 also supports something called 0-RTT, or &lt;a href=&quot;https://blog.cloudflare.com/introducing-0-rtt/&quot;&gt;Zero Round Trip Time Resumption&lt;/a&gt;. This basically allows a client that has visited a website previously, to pick up where they left off. No need for a new TLS negotiation, one was established before, so use that. When in use I&#39;d expect the purple in the graphs above to disappear. That means all the time needed to establish a secure connection and start sending data will be removed. That&#39;s a pretty impressive web performance boost (when it eventually becomes better supported).&lt;/p&gt;&lt;h3 id=&quot;ipv4-vs-ipv6&quot;&gt;IPv4 vs IPv6&lt;/h3&gt;&lt;p&gt;TLS 1.3 is quicker than 1.2, that is plain to see. But IPv4 vs IPv6 isn&#39;t as clear cut. As mentioned, both IPv4 and IPv6 tests were run at the same time so as the traffic at the time was consistent.&lt;/p&gt;&lt;p&gt;Comparing TLS 1.2 over IPv4 and IPv6 (negotiation speed, total time):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;UK to UK, IPv6 was marginally slower (5%, 3% at median)&lt;/li&gt;&lt;li&gt;UK to Vancouver, IPv6 was marginally slower (5%, 5% at median)&lt;/li&gt;&lt;li&gt;UK to Sydney, IPv6 was marginally quicker (7%, 4% at median)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;Comparison graph of 521 connections from UK to UK. TLS 1.2 over IPv6 vs IPv4 was slightly slower when connecting to UK.&quot; decoding=&quot;async&quot; height=&quot;304&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/T5U0LM71Nn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/T5U0LM71Nn-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/T5U0LM71Nn-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/T5U0LM71Nn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Comparison graph of 521 connections from UK to Vancouver. TLS 1.2 over IPv6 vs IPv4 was slightly slower when connecting to Vancouver.&quot; decoding=&quot;async&quot; height=&quot;303&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SyG-rFceDA-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SyG-rFceDA-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SyG-rFceDA-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SyG-rFceDA-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Comparison graph of 521 connections from UK to Sydney. TLS 1.2 over IPv6 vs IPv4 was slightly quicker when connecting to Sydney.&quot; decoding=&quot;async&quot; height=&quot;304&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/ptK1NztTLx-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/ptK1NztTLx-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/ptK1NztTLx-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/ptK1NztTLx-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Comparing TLS 1.3 over IPv4 and IPv6 (negotiation speed, total time):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;UK to UK, IPv6 was significantly quicker (20%, 4% at median)&lt;/li&gt;&lt;li&gt;UK to Vancouver, IPv6 was marginally slower (4%, 5% at median)&lt;/li&gt;&lt;li&gt;UK to Sydney, IPv6 was marginally quicker (4%, 2% at median)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;Comparison graph of 521 connections from UK to UK. TLS 1.3 over IPv6 vs IPv4 was significantly quicker when connecting to UK.&quot; decoding=&quot;async&quot; height=&quot;303&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/Kz9Hj0OV3y-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/Kz9Hj0OV3y-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/Kz9Hj0OV3y-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/Kz9Hj0OV3y-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Comparison graph of 521 connections from UK to Vancouver. TLS 1.3 over IPv6 vs IPv4 was slightly slower when connecting to Vancouver.&quot; decoding=&quot;async&quot; height=&quot;303&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7J0I6xjIoq-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7J0I6xjIoq-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7J0I6xjIoq-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/7J0I6xjIoq-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Comparison graph of 521 connections from UK to Sydney. TLS 1.3 over IPv6 vs IPv4 was slightly quicker when connecting to Sydney.&quot; decoding=&quot;async&quot; height=&quot;303&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/XgHQT1YCD--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/XgHQT1YCD--300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/XgHQT1YCD--600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/XgHQT1YCD--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So what&#39;s going on here? Well, in all honesty the sample size of.. just me, isn&#39;t going to answer this question. It&#39;s important to understand that with IPv4 and IPv6, you essentially have two independent connections to the internet. So you have two different paths from client to server. Although the tests were run at the same time, the path that each was running over are separate. Meaning that each path:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;may have different bandwidth available&lt;/li&gt;&lt;li&gt;may have more congestion than the other&lt;/li&gt;&lt;li&gt;may have more packet dropouts than the other&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These are just a few considerations. &lt;a href=&quot;https://twitter.com/apenwarr&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Avery Pennarun&lt;/a&gt; explains it very well in his excellent &lt;a href=&quot;https://tailscale.com/blog/two-internets-both-flakey/#failover&quot;&gt;&#39;IPv4, IPv6, and a sudden change in attitude&#39;&lt;/a&gt; blog post. So it was an interesting experiment (for me), but I wouldn&#39;t rely on the accuracy of the IPv4 vs IPv6 results. They were accurate for my connection at the point in time I measured them, but in no way are they conclusive. It&#39;s also worth remembering that IPv6 adoption is still &lt;a href=&quot;https://www.google.com/intl/en/ipv6/statistics.html#tab=ipv6-adoption&quot;&gt;considerably low&lt;/a&gt; considering it became a &lt;a href=&quot;https://www.ietf.org/&quot;&gt;IETF&lt;/a&gt; Draft Standard back in December 1998. There are also huge swathes of the world that simply &lt;a href=&quot;https://www.google.com/intl/en/ipv6/statistics.html#tab=per-country-ipv6-adoption&quot;&gt;haven&#39;t adopted it yet&lt;/a&gt;. I guess now that globally we have &lt;a href=&quot;https://www.ripe.net/publications/news/about-ripe-ncc-and-ripe/the-ripe-ncc-has-run-out-of-ipv4-addresses&quot;&gt;run out of IPv4 addresses&lt;/a&gt;, there should be some pressure to move adoption forwards.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;So in this blog post I&#39;ve covered a range of topics, from how to collect data using &lt;code&gt;curl&lt;/code&gt; and converting this into a format that can be plotted (CSV), all the way to IPv4 vs IPv6. Results show that enabling TLS 1.3 is a good idea. It offers more security and better performance for your users. It&#39;s also worth noting that TLS 1.3 will be a requirement to use the QUIC transport layer network protocol in the future. This will pave the way to HTTP/3. And once 0-RTT becomes more prevalent, for repeat website visits the purple on the graphs displayed above will disappear completely. Even faster connections for all (at least for those that use a browser that supports it anyway).&lt;/p&gt;&lt;h2 id=&quot;update-using-google-analytics-server-connection-time&quot;&gt;Update: Using Google Analytics server connection time&lt;/h2&gt;&lt;p&gt;After publishing this article I got chatting to &lt;a href=&quot;https://twitter.com/rnebhwani&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rockey Nebhwani&lt;/a&gt; who suggested looking at Google Analytics (GA) data to see if I could spot the change. I always tend to forget about the performance timing data that GA captures. It has some limitations (sample size &lt;a href=&quot;https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings#sampling_considerations&quot;&gt;being unfixable&lt;/a&gt; and its use of averages), but if you have no other data sources then it&#39;s better than nothing! Turns out it&#39;s very easy to spot where the change occurred when looking over 5 months of data:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using GA data it is possible to see the server connection time drop when TLSv1.3 was enabled on the Fastly POPs.&quot; decoding=&quot;async&quot; height=&quot;371&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SukXoY2glI-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SukXoY2glI-300.webp 300w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SukXoY2glI-600.webp 600w, https://nooshu.com/blog/2020/07/30/measuring-tls-13-ipv4-ipv6-performance/SukXoY2glI-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Admittedly it&#39;s not dropped a huge amount, only 10ms or so. But considering this is the average time of heavily sampled data (14,302,686 page views sent sample data over 5 months), it shows that users are seeing an improvement. For those interested in how you find this data in GA: Behavior &gt; Site Speed &gt; Overview &gt; Avg. Server Connection Time.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;30/07/20: Initial post published. Thanks go out to &lt;a href=&quot;https://twitter.com/josephscott&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Joseph Scott&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/programmingart&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Robin Marx&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/chendo&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Chendo&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/SimmerVigor&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Lucas Pardue&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt;, and &lt;a href=&quot;https://twitter.com/radumicu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Radu Micu&lt;/a&gt; for pointing me to a tonne of resources and answering lots of questions for this post.&lt;/li&gt;&lt;li&gt;30/07/20: Updated TLS 1.3 advantages to mention insecure ciphers have been removed in 1.3, rather than new more secure ones added (Thanks Jamie H).&lt;/li&gt;&lt;li&gt;05/08/20: Added graph of GA data (Avg. Server Connection Time) that shows the when TLSv1.3 was enabled (Thanks &lt;a href=&quot;https://twitter.com/rnebhwani&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rockey Nebhwani&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Bringing HTTP/2 to GOV.UK - Resources</title>
    <link href="https://nooshu.com/blog/2020/07/14/bringing-http2-to-govuk-resources/" />
    <updated>2020-07-14T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/07/14/bringing-http2-to-govuk-resources/</id>
    <content type="html">&lt;p&gt;So at the start of February 2020 we permanently enabled HTTP/2 on GOV.UK (that feels like a long time ago now!). I spoke about this at &lt;a href=&quot;https://ldnwebperf.org/&quot;&gt;London Web Performance&lt;/a&gt;, read a whole lot about HTTP/2 (and surrounding topics), and produced some work of my own in the process. Without all this, it would have been very difficult to justify the permanent change, as it wasn&#39;t as simple as just &quot;turning it on&quot;.&lt;/p&gt;&lt;p&gt;So in an effort to help others in the same position, and to fully document the process, I&#39;ve pulled together a post with as much information as I can remember about the work. It was inspired by &lt;a href=&quot;https://twitter.com/36degrees&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Oliver Byford&lt;/a&gt; and his excellent &#39;&lt;a href=&quot;https://obyford.com/posts/inclusive-forms/&quot;&gt;Inclusive forms: Anatomy of a (fictional) GOV.UK service&lt;/a&gt;&#39; blog post.&lt;/p&gt;&lt;h2 id=&quot;london-web-performance&quot;&gt;London Web Performance&lt;/h2&gt;&lt;p&gt;My &lt;a href=&quot;https://ldnwebperf.org/&quot;&gt;LdnWebPerf&lt;/a&gt; presentation and associated slides:&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; src=&quot;https://www.youtube.com/embed/U21rbXJ5V2I?si=XqUxVYmdkUUiXkI0&quot; title=&quot;YouTube video player&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/presentation/d/1Ao3bFnKo2xnunNQRG2tXi_MeSIJV7mdnH2v9IuE9qfk/edit?usp=sharing&quot;&gt;Slides: Bringing HTTP/2 to GOV.UK&lt;/a&gt; - Matt Hobbs, March 2020&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;internal-research&quot;&gt;Internal research&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/document/d/1LLK-QqokIRpDuDNSV01hvl4WuKjNOnLEJKtXIrXMKPo/edit?usp=sharing&quot;&gt;Initial Research: Bringing HTTP/2 to GOV.UK&lt;/a&gt; - Matt Hobbs, October 2018&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/document/d/16i1mRQfaywhvQDTRlQ_z2dt2Vx0LZeVavpft3o6pyK8/edit?usp=sharing&quot;&gt;Initial Data: Performance testing HTTP/2 data&lt;/a&gt; - Matt Hobbs, October 2018&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1UnekjaPdxSwazpvb4AArKoXqrhT4E3iMxdw86yN2dv8/edit?usp=sharing&quot;&gt;Data comparison Sheet: HTTP/2 results before / after&lt;/a&gt; - Matt Hobbs, October 2018&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/document/d/1eSipC8es6ASkLmptr4TDO_edVAO8QRQjJ2q4VLXKe5g/edit#heading=h.30j0zll&quot;&gt;Initial Analysis: Was switching to HTTP/2 a good thing?&lt;/a&gt; - Matt Hobbs, October 2018&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1fvQFsvnCOqBpRbQaB9tHw3NP6DqiSOVYPUh2nYZdINw/edit?usp=sharing&quot;&gt;Initial research: summary sheets&lt;/a&gt; - Matt Hobbs, October 2018&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;gov-uk-fixed&quot;&gt;GOV.UK Fixed!&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://docs.google.com/document/d/1uSyDrCkCsTa04N_0Dppx6Vh7y5j0sXS901S_xeky0YY/edit?usp=sharing&quot;&gt;Bringing HTTP/2 to GOV.UK - First technical draft&lt;/a&gt; - Matt Hobbs, February 2020&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/blob/ffef3c558769a7930a729d722ee7211c33e3549a/rfc-115-enabling-http2-on-govuk.md&quot;&gt;GOV.UK RFC-115: Enabling HTTP/2 on GOV.UK&lt;/a&gt; - Matt Hobbs, January 2020&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://technology.blog.gov.uk/2020/07/02/speeding-up-gov-uk-with-http-2/&quot;&gt;Blog: Speeding up GOV.UK with HTTP/2&lt;/a&gt; - Matt Hobbs, July 2020&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.gov.uk/government/case-studies/how-gds-improved-govuks-frontend-performance-with-http2&quot;&gt;Case study: How GDS improved GOV.UK&#39;s frontend performance with HTTP/2&lt;/a&gt; - Matt Hobbs, July 2020&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;subresource-integrity-sri&quot;&gt;Subresource Integrity (SRI)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/&quot;&gt;Blog post: HTTP/2 and Subresource Integrity don&#39;t always get on&lt;/a&gt; - Matt Hobbs, December 2019&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/alphagov/govuk-rfcs/blob/3a682960071e7f946d9ecef843d82db4c938f86a/rfc-114-sri-enable-http2.md&quot;&gt;GOV.UK RFC-114: Changing SRI to allow for HTTP/2 to be enabled on GOV.UK&lt;/a&gt; - Matt Hobbs, December 2019&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/SRI/&quot;&gt;Specification: Subresource Integrity &lt;/a&gt;- W3C Recommendation, June 2016&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;http-2-prioritisation&quot;&gt;HTTP/2 prioritisation&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/andydavies/http2-prioritization-issues&quot;&gt;Repo: Tracking HTTP/2 Prioritization Issues&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/pmeenan/http2priorities/tree/master/stand-alone&quot;&gt;Repo: Stand-alone HTTP/2 prioritization test&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/better-http-2-prioritization-for-a-faster-web/&quot;&gt;Blog: Better HTTP/2 Prioritization for a Faster Web&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;, May 2019&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ct5MvtmL1NM&quot;&gt;Video: HTTP/2 Prioritization - Velocity 2019&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;, July 2019&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;http-2-connection-coalescing&quot;&gt;HTTP/2 connection coalescing&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/&quot;&gt;Blog post: HTTP/2 connection coalescing&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/bagder&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Daniel Stenberg&lt;/a&gt;, August 2016&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Connection_management_in_HTTP_1.x#Domain_sharding&quot;&gt;Docs: Connection management in HTTP/1.x: Domain sharding&lt;/a&gt; - MDN web docs&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;cross-origin-resource-sharing-cors&quot;&gt;Cross-Origin Resource Sharing (CORS)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&quot;&gt;Docs: Cross-Origin Resource Sharing (CORS)&lt;/a&gt; - MDN web docs&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.infoq.com/presentations/cors/&quot;&gt;Video: Demystifying CORS: It&#39;s Simpler Than You Think&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/chimericdream&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Bill Parrott&lt;/a&gt;, November 2017&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://httptoolkit.tech/will-it-cors/&quot;&gt;Tool: Will it CORS?&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;learning-materials&quot;&gt;Learning materials&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=haDK_ChZ5zQ&quot;&gt;Video: Revisiting HTTP/2 - LDNWebPerf&lt;/a&gt; - Hooman Beheshti, May 2018&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/&quot;&gt;Blog post: Improving Perceived Performance With the Link Rel=preconnect HTTP Header&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;, March 2019&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.manning.com/books/http2-in-action&quot;&gt;Book: HTTP/2 in Action&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt;, March 2019&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://medium.com/bbc-design-engineering/http-2-is-easy-just-turn-it-on-34baad2d1fb1&quot;&gt;Blog post: HTTP/2 is easy, just turn it on...&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/tdp_org&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Neil Craig&lt;/a&gt;, November 2017&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://kamranahmed.info/blog/2016/08/13/http-in-depth/&quot;&gt;Blog: HTTP-in-depth - Journey to HTTP/2&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/kamranahmedse&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Kamran Ahmed&lt;/a&gt;, August 2016&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://almanac.httparchive.org/en/2019/http2&quot;&gt;Web Almanac: Part IV Chapter 20 - HTTP/2&lt;/a&gt; - &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt;, November 2019&lt;/li&gt;&lt;/ul&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;13/07/20: Initial post published.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Example synthetic web performance monitoring dashboards using SpeedCurve</title>
    <link href="https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/" />
    <updated>2020-06-17T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://www.gov.uk/government/organisations/government-digital-service&quot;&gt;Government Digital Service (GDS)&lt;/a&gt; have been rolling out &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Performance/Rum-vs-Synthetic#Synthetic_Monitoring&quot;&gt;synthetic web performance monitoring&lt;/a&gt; across all GOV.UK applications (all 13 of them), and the other services run by by GDS. Using &lt;a href=&quot;https://speedcurve.com/&quot;&gt;SpeedCurve&lt;/a&gt; we can now set a whole bunch of performance budgets and alerts that let us know via email and Slack, when a particular threshold has been crossed in the metrics we are monitoring. In rolling this out to all teams, I found myself creating dashboards with a consistent set of graphs. The reason for this method was to:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;try and extract as much useful information from the synthetic tests as possible&lt;/li&gt;&lt;li&gt;&#39;standardise&#39; the graphs across teams so I could document and talk about what each meant easily&lt;/li&gt;&lt;li&gt;compare and contrast performance between each of the applications / services&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;During this process I created a document for all teams to read with an explanation of the graphs and metrics involved. Instead of keeping it locked away in a Google Document, I&#39;ve decided to adapt it into a blog post. By doing so I&#39;m hoping it will:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;help others understand the power of the dashboard and graphs you can create using SpeedCurve&lt;/li&gt;&lt;li&gt;help others by giving example dashboards and graphs they can copy and adapt&lt;/li&gt;&lt;li&gt;validate my thoughts and understanding of the graphs and metrics with other people in the web performance community&lt;/li&gt;&lt;li&gt;gather feedback from others to find out if there&#39;s anything I&#39;m missing&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;browser-and-devices-we-are-testing-on&quot;&gt;Browser &amp; devices we are testing on&lt;/h2&gt;&lt;p&gt;Synthetic web performance testing gives you fine grain control of the conditions for which the tests are run. Certain browsers and settings have been selected to test against, primarily based off our Google Analytics (GA) data.&lt;/p&gt;&lt;p&gt;Browsers I&#39;ve currently selected to test with are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Chrome Desktop (Cable connection)&lt;/li&gt;&lt;li&gt;Internet Explorer 11 (Cable connection)&lt;/li&gt;&lt;li&gt;Firefox (Cable connection)&lt;/li&gt;&lt;li&gt;Samsung Galaxy S4 (3G connection)&lt;/li&gt;&lt;li&gt;iPhone 5 (2G connection)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;chrome&quot;&gt;Chrome&lt;/h3&gt;&lt;p&gt;Chrome is the 2nd most popular browser used on GOV.UK (after Safari on iOS). It makes up ~30% of our users browser usage. Users tend to use both the desktop and mobile version. It is an obvious choice for inclusion in testing.&lt;/p&gt;&lt;h3 id=&quot;internet-explorer-11&quot;&gt;Internet Explorer 11&lt;/h3&gt;&lt;p&gt;This is the last Internet Explorer (IE) version we &lt;a href=&quot;https://technology.blog.gov.uk/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/&quot;&gt;officially support&lt;/a&gt; according to the &lt;a href=&quot;https://www.gov.uk/service-manual/technology/designing-for-different-browsers-and-devices#browsers-to-test-in&quot;&gt;Service Manual&lt;/a&gt;. Looking at the last 3 months GA data from GOV.UK, I can see it makes up 99.2% of IE usage. Again, an obvious choice for inclusion in testing.&lt;/p&gt;&lt;h3 id=&quot;firefox&quot;&gt;Firefox&lt;/h3&gt;&lt;p&gt;Firefox is currently only used by 1.48% of GOV.UK users over the past 3 months (me included). This does make it a candidate for not being included. I&#39;m personally keeping it in as it is the only non-WebKit based browser on our list, and it is always good to diversify testing, as browser engines &lt;a href=&quot;https://caniuse.com/#compare=firefox+77,chrome+83&quot;&gt;differ&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;samsung-galaxy-s4&quot;&gt;Samsung Galaxy S4&lt;/h3&gt;&lt;p&gt;Simulated specifications for this mobile are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Connection: Mobile 3G&lt;/li&gt;&lt;li&gt;Viewport: 360x640&lt;/li&gt;&lt;li&gt;CPU: 3 x slowdown&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Many of the tests that I chose to run are using this simulated Samsung S4 mobile phone. The results returned are set at the 95th percentile (p95). The reasons for this are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The S4 is a low specification &#39;legacy&#39; device, with limited CPU and RAM.&lt;/li&gt;&lt;li&gt;It is a device that is currently used by many users of GOV.UK (~45,000 users in the past 3 months, or 0.04%)&lt;/li&gt;&lt;li&gt;A 3G connection is fairly standard and expected in the UK for many users.&lt;/li&gt;&lt;li&gt;The 95th percentile gives essentially the worst case scenario (i.e. 95% of users are going to have a &lt;strong&gt;better&lt;/strong&gt; experience than this). By setting the bar so low, any performance improvements we can make at this level will likely improve performance for every device setup.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;2g-mobile-effective-iphone-5-se&quot;&gt;2G Mobile (effective iPhone 5/SE)&lt;/h3&gt;&lt;p&gt;This is a custom browser profile that I have created in the SpeedCurve settings. Specifications for this simulated mobile are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Browser: Chrome&lt;/li&gt;&lt;li&gt;Dimensions: 320x568&lt;/li&gt;&lt;li&gt;Pixel ratio: 2&lt;/li&gt;&lt;li&gt;Connection: Mobile 2G&lt;/li&gt;&lt;li&gt;CPU: 4 x slowdown&lt;/li&gt;&lt;li&gt;User Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B137 Safari/601.1&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This phone is currently used by ~87,000 GOV.UK users over the past 3 months (or 0.08%).&lt;/p&gt;&lt;h2 id=&quot;example-dashboard-1-a-gov-uk-application&quot;&gt;Example dashboard 1 - A GOV.UK application&lt;/h2&gt;&lt;p&gt;Now that we understand the test devices, let&#39;s look at the whole &#39;standard&#39; dashboard used for each GOV.UK application:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of a dashboard used for every GOV.UK application. We have Web Vitals, Lighthouse scores, core metrics on a Galaxy S4 on a 3G connection, Font size, CSS Size and JavaScript size.&quot; decoding=&quot;async&quot; height=&quot;405&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/KtSnaxyjui-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/KtSnaxyjui-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/KtSnaxyjui-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/KtSnaxyjui-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Each application runs a particular set of pages on GOV.UK, so it&#39;s good to get a general overview of how well the application is delivering pages to a user. So let&#39;s delve into what each graph is telling us.&lt;/p&gt;&lt;h3 id=&quot;web-vitals-iphone-5-2g-95th-percentile&quot;&gt;Web Vitals - iPhone 5 - 2G - 95th Percentile&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;The Web Vitals graph, showing JS Total Blocking Time, Cumulative Layout Shift, and Largest Contentful Paint.&quot; decoding=&quot;async&quot; height=&quot;463&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/aA_B3O9VLn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/aA_B3O9VLn-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/aA_B3O9VLn-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/aA_B3O9VLn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Web Vitals is a marketing name for a set of metrics that are soon to become very important in the Search Engine Optimisation (SEO) world. Google published a &lt;a href=&quot;https://webmasters.googleblog.com/2020/05/evaluating-page-experience.html&quot;&gt;blog post&lt;/a&gt; recently stating that in 2021 its search ranking algorithm will start to use the scores from these metrics to influence a page&#39;s rank. The metrics they have chosen to use are user-centric and ensure that a page must:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Load quickly (Largest Contentful Paint)&lt;/li&gt;&lt;li&gt;Be interactive within a reasonable time (JS Total Blocking Time for Synthetic testing, First Input delay for RUM)&lt;/li&gt;&lt;li&gt;minimise jarring interstitials that affect visual stability (Cumulative Layout Shift)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These metrics have been chosen so as to help start ranking pages based on their overall usability for a user. Pages that score poorly in these areas will be penalised.&lt;/p&gt;&lt;p&gt;Note: An iPhone 5 on a 2G connection has been used in this instance, as when using the Samsung Galaxy S4 device, it doesn&#39;t seem to support Cumulative Layout Shift. So this selection gives an idea of the result of these metrics on a low-specification device, on a very slow connection.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;JS Total Blocking Time (TBT)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;According to the &lt;a href=&quot;https://web.dev/vitals/&quot;&gt;Core Web Vitals&lt;/a&gt;, First Input Delay (FID) is the metric that should be used for indicating if a page is interactive for a user. Unfortunately, this metric is only available via Real User Monitoring (RUM) testing, and not available in synthetic testing. An alternative synthetic metric is available to measure page interactivity. It is called Total Blocking Time.&lt;/p&gt;&lt;p&gt;Total Blocking Time measures some of the activity on the browsers main JavaScript thread. Too much activity on the main thread can cause a page to not respond to user input, causing confusion and frustration for users. Any script that takes over 50ms to execute is known as a &lt;a href=&quot;https://web.dev/long-tasks-devtools/&quot;&gt;&#39;long task&#39;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So the &#39;total&#39; in TBT is a little misleading. It in fact doesn&#39;t measure the total as you would expect (initial navigation to fully interactive): In reality TBT:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;only tracks the long tasks between First Contentful Paint (FCP) and Time to Interactive (TTI)&lt;/li&gt;&lt;li&gt;only tracks time &lt;strong&gt;over&lt;/strong&gt; the initial 50ms (the threshold for a &#39;long task&#39;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;TBT is the sum of the scripts after these two criteria has been applied. So TBT is great for understanding your Lighthouse score, but there are other ways to track scripts with long tasks on your page. There&#39;s a whole &lt;a href=&quot;https://speedcurve.com/blog/javascript-long-tasks/&quot;&gt;post here&lt;/a&gt; from &lt;a href=&quot;https://twitter.com/MarkZeman&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Mark Zeman&lt;/a&gt; which talks all about it if you want to know more.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Cumulative Layout Shift (CLS)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Cumulative Layout Shift measures the visual stability of a page. The visual stability of a page is affected by late displaying elements, causing the whole page to &#39;shift&#39;. This is especially problematic on mobile devices, and sometimes causes users to click on buttons and adverts they didn&#39;t intend to.&lt;/p&gt;&lt;p&gt;A brief explanation as to how it works: when rendered elements move during page load, they are labeled as &#39;unstable&#39;. A score is calculated by taking the distance that the unstable elements have moved in the viewport (this is the distance fraction), and multiplying it by the surface area of the viewport that&#39;s affected by unstable elements moving (this is the impact fraction).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Cumulative layout shift diagram explaining the distance fraction (how far an element moved down the viewport) and the impact fraction (the area this elements move impacted in the viewport)&quot; decoding=&quot;async&quot; height=&quot;529&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/DBkzW_L4gs-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/DBkzW_L4gs-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/DBkzW_L4gs-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/DBkzW_L4gs-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The cumulative layout shift score is the sum of the above calculations for all unstable elements on a page that move around during the page load. Diagram credit goes to the &lt;a href=&quot;https://calibreapp.com/blog/new-generation-of-performance-metrics&quot;&gt;Calibre app team&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Largest Contentful Paint (LCP)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This metric measures the loading performance of a page. It does this by reporting on the render time of the largest content element visible within the viewport. A LCP of 2.5 seconds or less is considered good, 2.5 - 4 seconds needs improvement, 4+ seconds equates to a poor user experience.&lt;/p&gt;&lt;h3 id=&quot;lighthouse-scores&quot;&gt;Lighthouse Scores&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Graph showing the result of Google Lighthouse scores over time.&quot; decoding=&quot;async&quot; height=&quot;455&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/ak_G2ZtcED-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/ak_G2ZtcED-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/ak_G2ZtcED-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/ak_G2ZtcED-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://developers.google.com/web/tools/lighthouse&quot;&gt;Google Lighthouse&lt;/a&gt; is a tool built into every version of Chrome DevTools. It is a set of best practice audits that can be run against any website via the DevTools &#39;Audit&#39; tab. The results give you a score out of 100 for the following areas:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Performance&lt;/li&gt;&lt;li&gt;Accessibility&lt;/li&gt;&lt;li&gt;Best Practice&lt;/li&gt;&lt;li&gt;SEO (omitted from our tests)&lt;/li&gt;&lt;li&gt;Progressive Web App (omitted from our tests)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We aren&#39;t tracking SEO and PWA scores since they are a lower priority for our services.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Lighthouse Performance Score&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The lighthouse performance score gives you an idea of how well your service is performing. Scoring is based on a range of metrics that are weighted according to priority. The focus of this audit is on user-centric metrics, so it optimises for performance a user will actually care about and respond too. Performance &lt;a href=&quot;https://googlechrome.github.io/lighthouse/scorecalc/&quot;&gt;scoring details can be found here&lt;/a&gt; for both v5 and v6 of Lighthouse.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Lighthouse Accessibility&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The accessibility score gives developers a way to identify accessibility issues on a particular page. These issues are often &#39;low-hanging fruit&#39;, that may be relatively simple to fix. A full list of the accessibility checks the tool runs can be &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1up5rxd4EMCoMaxH8cppcK1x76n6HLx0e7jxb0e0FXvc/edit#gid=0&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Note: It&#39;s important to realise that an accessibility score of 100 &lt;strong&gt;doesn&#39;t&lt;/strong&gt; mean a service is 100% accessible. It just means it has passed those specific tests. A service should still be professionally audited and tested with users.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Lighthouse Best Practice&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The best practice score looks for issues related to infrastructure and coding best practices in the page source. It is marked on a number of separate criteria. A full list of the criteria it examines and the relative weights applied can be &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1up5rxd4EMCoMaxH8cppcK1x76n6HLx0e7jxb0e0FXvc/edit#gid=0&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;core-metrics-galaxy-s4-3g-95th-percentile&quot;&gt;Core Metrics - Galaxy S4 - 3G - 95th Percentile&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Graph showing a set of core page metrics over time.&quot; decoding=&quot;async&quot; height=&quot;427&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/7e5poYdBpr-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/7e5poYdBpr-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/7e5poYdBpr-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/7e5poYdBpr-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This graph is used to track a ‘core&#39; set of web performance metrics that are considered to be very important in how a user experiences a website. Not all the metrics are &#39;user-centric&#39;, but they do give you a good sense of how well a page is performing and where bottlenecks are likely to be located.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Backend (TTFB) Time&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Backend time (also known as time to first byte), is the time it takes for a request packet to leave the browser, for the server to process the request and a response to come back to the client. DNS lookup, connection, and TLS negotiation time are also included in this time. In many cases this should be one of your more stable metrics. Things like database lookups, routing, and template compilation can have an effect on this metric. There&#39;s a whole &lt;a href=&quot;https://csswizardry.com/2019/08/time-to-first-byte-what-it-is-and-why-it-matters/&quot;&gt;blog post here&lt;/a&gt; by Harry Roberts if you&#39;d like to find out more about it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Start Render&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Start Render is the time between the initial page navigation and when the first pixel content is rendered to the display. This metric is very good for understanding the point at which a user sees &lt;em&gt;anything&lt;/em&gt; on screen. It is calculated by analising of the page loading, so is only available in synthetic testing.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Fully Loaded Time&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Fully loaded is defined as the time from the start of page navigation, up to the point where no network activity has occurred for a total of 2 seconds after document complete (&lt;code&gt;onload event&lt;/code&gt; fired). This metric is useful for understanding the amount of data being downloaded after the &lt;code&gt;onload&lt;/code&gt; event has fired (e.g. via a Single Page App, or third-party JavaScript on a page).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Visually Complete&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The visually complete metric focuses on the viewport that the page is being rendered on. It is calculated by analysing a video of the page load, then comparing each frame to the pages final state at the end of the test. This essentially plots page &#39;completeness&#39; over time. It will give you an indication of how quickly pixels are being shown to the user, and at what point the viewport reaches 100% rendered. Note: there may be other areas of the page still loading outside of the viewport (‘below the fold&#39;).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Speed Index&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index&quot;&gt;Speed index&lt;/a&gt; takes the visual progress of the visible page load (in the viewport), and calculates an overall score to help understand how quickly content was painted to the viewport. This score is calculated using the same method as mentioned above in Visually complete. The &#39;completeness&#39; of the page is plotted over time by doing a frame-by-frame analysis of each frame against the final frame. By plotting the completeness over time and calculating the (bounded) area above the graph, it gives information as to what portion of the page is left to be rendered at any point in time. A &lt;strong&gt;lower&lt;/strong&gt; Speed Index score is better.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Graph of how a pages Speed Index is calculated.&quot; decoding=&quot;async&quot; height=&quot;299&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/tDEQ4kt6DK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/tDEQ4kt6DK-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/tDEQ4kt6DK-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/tDEQ4kt6DK-809.webp 809w&quot; width=&quot;809&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;asset-sizes&quot;&gt;Asset sizes&lt;/h3&gt;&lt;p&gt;The last three graphs are fairly self-explanatory. They give an indication of how the size of each type of static asset on the page has changed over time. It is a good idea to monitor these metrics across releases to see what difference new changes have made to the download size. The aim is to minimise the size of all these assets to improve frontend performance.&lt;/p&gt;&lt;h2 id=&quot;example-dashboard-2-an-important-gov-uk-page&quot;&gt;Example dashboard 2 - An important GOV.UK page&lt;/h2&gt;&lt;p&gt;Here we&#39;ve setup a slightly different dashboard to get more information back from the test device to see if there are any CPU bottlenecks on the page (and how much CPU time is being used):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of a dashboard used for important GOV.UK pages. We have Web Vitals, Lighthouse scores, core metrics on a Galaxy S4 on a 3G connection, image size, CPU metrics, and milestone timings.&quot; decoding=&quot;async&quot; height=&quot;430&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/V_nkiwMuPU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/V_nkiwMuPU-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/V_nkiwMuPU-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/V_nkiwMuPU-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;web-vitals-iphone-5-2g-95th-percentile-2&quot;&gt;Web Vitals - iPhone 5 - 2G - 95th Percentile&lt;/h3&gt;&lt;p&gt;Same as &lt;a href=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/#web-vitals---iphone-5---2g---95th-percentile&quot;&gt;listed above&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;milestone-timings-samsung-s4-3g-95th-percentile&quot;&gt;Milestone Timings - Samsung S4 - 3G - 95th Percentile&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Graph showing a set of milestone timings for an important page over time.&quot; decoding=&quot;async&quot; height=&quot;431&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/jZb8BoUqSv-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/jZb8BoUqSv-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/jZb8BoUqSv-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/jZb8BoUqSv-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The graph shows important milestone metrics in the lifecycle of a page load. The aim is to reduce these metrics as much as possible to help improve user experience. It&#39;s worth noting that many of these aren&#39;t considered to be &lt;a href=&quot;https://web.dev/user-centric-performance-metrics/&quot;&gt;user-centric metrics&lt;/a&gt;, but they are worth monitoring to give you a more complete picture of a pages performance.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Backend (TTFB) time&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;See above under &lt;a href=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/#core-metrics---galaxy-s4---3g---95th-percentile&quot;&gt;core metrics&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;DOM Content Loaded time (DCL)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;DOM Content Loaded Time is the point at which the &lt;code&gt;DOMContentLoaded&lt;/code&gt; event fires. This is indicates when the Document Object Model (DOM) has been loaded and parsed. The event does &lt;strong&gt;not&lt;/strong&gt; wait for other assets like stylesheets and images to load before firing. This metric is often used by JavaScript libraries to identify when they can start manipulating the DOM.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Start Render&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;See above under &lt;a href=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/#core-metrics---galaxy-s4---3g---95th-percentile&quot;&gt;core metrics&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Time To Interactive (TTI)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Time to interactive is a complex metric, as it doesn&#39;t measure what most people think it measures which is the time it takes for a page to become interactive. This is very hard to actually measure via synthetic performance testing. What this metric should be called is Time to &lt;em&gt;consistently&lt;/em&gt; interactive, as the page will likely be semi-interactive before this point. The metric indicates the time at which a site is visually rendered and consistently interactive for a user. There&#39;s a &lt;a href=&quot;https://blog.dareboost.com/en/2019/05/time-to-interactive-tti-measure-interactivity/&quot;&gt;whole blog post here&lt;/a&gt; all about it for those who want to know much more.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;First Contentful Paint (FCP)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;First Contentful Paint measures how long it takes for the browser to render the first piece of DOM content to the page after user navigation. DOM content is defined as either:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Text and images&lt;/li&gt;&lt;li&gt;Non-white &lt;code&gt;&amp;lt;canvas&gt;&lt;/code&gt; elements&lt;/li&gt;&lt;li&gt;SVG&#39;s&lt;/li&gt;&lt;li&gt;Note: anything inside an &lt;code&gt;&amp;lt;iframe&gt;&lt;/code&gt; &lt;strong&gt;isn&#39;t&lt;/strong&gt; included&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;FCP is a user-centric metric. It answers a user&#39;s question &quot;Is it happening?&quot;. 0-2 seconds is considered good, 2-4 seconds is moderate, over 4 seconds in considered to be slow.&lt;/p&gt;&lt;h3 id=&quot;cpu-metrics-galaxy-s4-3g-95th-percentile&quot;&gt;CPU Metrics - Galaxy S4 - 3G - 95th Percentile&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Graph showing a set of CPU timings for an important page.&quot; decoding=&quot;async&quot; height=&quot;463&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/bAqfHEltP2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/bAqfHEltP2-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/bAqfHEltP2-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/bAqfHEltP2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The graph shows important CPU metrics related to the browsers page loading process. Any increase in these metrics means that the browser is having to work harder to paint pixels to the screen. High CPU usage can result in pages that are slow to respond to users, and have a negative impact on a devices battery life. The aim is to minimise all of these metrics.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CPU JS Scripting To Start Render&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the amount of CPU time the JavaScript used to get to the start render point. Start render time is defined as the time between initial page navigation, to the first pixel being displayed on screen.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CPU JS Scripting To Page Load&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the amount of CPU time the JavaScript used to get to page load. Page load is when the &lt;code&gt;onload&lt;/code&gt; event fires, and is the point where the page and all its dependent assets have finished loading.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CPU JS Scripting&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the total CPU time used when parsing and executing the JavaScript on the page.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CPU Layout&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the CPU time taken to layout the page. Layout computes the exact position and size of each object on a page.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CPU Loading&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the time taken to parse the HTML and the CSS, and create the render tree (DOM + CSSOM).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CPU Painting&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the CPU time taken to paint pixels to the page. The paint process takes the final render tree and renders the pixels to the screen.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;First CPU Idle&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is how long it takes a page to become minimally interactive. It is minimally interactive when most (but not necessarily all) UI elements are interactive, and the page responds to the user in a reasonable amount of time.&lt;/p&gt;&lt;h3 id=&quot;lighthouse-scores-2&quot;&gt;Lighthouse Scores&lt;/h3&gt;&lt;p&gt;See above under &lt;a href=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/#lighthouse-scores&quot;&gt;Example dashboard 1&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;core-metrics-galaxy-s4-3g-95th-percentile-2&quot;&gt;Core Metrics - Galaxy S4 - 3G - 95th Percentile&lt;/h3&gt;&lt;p&gt;See above under &lt;a href=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/#core-metrics---galaxy-s4---3g---95th-percentile&quot;&gt;Example dashboard 1&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;image-size&quot;&gt;Image Size&lt;/h3&gt;&lt;p&gt;&lt;img alt=&quot;Simple graph showing the image size on a page.&quot; decoding=&quot;async&quot; height=&quot;438&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/G5lWoZSI7E-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/G5lWoZSI7E-300.webp 300w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/G5lWoZSI7E-600.webp 600w, https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/G5lWoZSI7E-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So we are already tracking the asset sizes (CSS, JS, fonts) at an application level as seen in the &lt;a href=&quot;https://nooshu.com/blog/2020/06/17/example-synthetic-web-performance-monitoring-dashboards-using-speedcurve/#example-dashboard-1-a-gov-uk-application&quot;&gt;Example dashboard 1&lt;/a&gt; above. But because we are now focussing on an important individual page, we are also tracking the size of images on this page. This is a great idea if these images are uploaded through a content management system, as a lot of the time they may not be fully optimised.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Hopefully I&#39;ve given you an idea of what graphs you could add to a dashboard, and what each of the corresponding metrics mean. Synthetic tests give you so much information about a page, so it&#39;s important to pick out the metrics that will help you identify and rectify web performance issues. As mentioned at the start of the post, if you have any feedback on the graphs I&#39;ve chosen, or if I&#39;m missing anything please let me know via &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter&lt;/a&gt;. I&#39;d love to see the graphs &amp; dashboards you are creating in your testing tools to maximise the amount of useful data you are extracting from synthetic tests.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;21/06/20: Initial post published.&lt;/li&gt;&lt;li&gt;22/06/20: Updated Total Blocking Time explanation as it was incorrect. Thanks &lt;a href=&quot;https://twitter.com/MarkZeman&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Mark Zeman&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>WebPageTest comparison URL generator</title>
    <link href="https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/" />
    <updated>2020-04-22T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/</id>
    <content type="html">&lt;p&gt;As any reader of this blog will have noticed, in recent years I&#39;ve taken a great interest in web performance both for my current job role and also as a hobby. And anyone interested in webperf will quickly realise that one of the best tools you can use to benchmark the performance of your website is &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;. It&#39;s a tool that will give you a huge amount of information about how your website is performing, including useful data and charts that you can use to identify areas for improvement. Simply enter your URL, choose from a few settings and click &#39;Start Test&#39;. I&#39;ve written a lot more information on interpreting the results from these tests in my blog posts &#39;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;How to read a WebPageTest Waterfall View chart&lt;/a&gt;&#39; and &#39;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/&quot;&gt;How to read a WebPageTest Connection View chart&lt;/a&gt;&#39; for those interested in knowing more. But one of the most powerful features of WebPageTest is its ability to compare the results from multiple tests.&lt;/p&gt;&lt;h2 id=&quot;measuring-regressions-and-improvements&quot;&gt;Measuring regressions and improvements&lt;/h2&gt;&lt;p&gt;How do you know if you are improving a websites performance (or making it worse) if you can&#39;t easily compare results before and after you have made a change? This is where WebPageTest comes into its own. First: make sure you are logged in to your account (so your tests are saved), then run a number of tests. Next navigate to the &#39;Test history&#39; page via the main menu. If you&#39;ve run a number of tests you should see something similar to below.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of the &#39;Test History&#39; page on WebPageTest displaying multiple test results from different websites.&quot; decoding=&quot;async&quot; height=&quot;263&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/CVKVJddvhU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/CVKVJddvhU-300.webp 300w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/CVKVJddvhU-600.webp 600w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/CVKVJddvhU-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;On this page the area we are interested in is to the left of the &#39;Date/Time&#39; column (highlighted in red below). This section can be easy to miss since the form controls are quite small, but this is how you compare test results.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;&#39;Test History&#39; with the compare functionality highlighted to the left of the results.&quot; decoding=&quot;async&quot; height=&quot;263&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/-LjbcLLxZT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/-LjbcLLxZT-300.webp 300w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/-LjbcLLxZT-600.webp 600w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/-LjbcLLxZT-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Simply select all the tests you would like to compare (you can select more than 2 if you wish), then click the &#39;Compare&#39; button. WebPageTest will go away and the load a whole new view with a number of graphs and tools to help you gauge the performance difference between the tests that you have been selected (see below).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The compare view with three tests being compared.&quot; decoding=&quot;async&quot; height=&quot;673&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/aicWlJKUmt-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/aicWlJKUmt-300.webp 300w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/aicWlJKUmt-600.webp 600w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/aicWlJKUmt-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the image above you can see a visual representation of how each of the pages loaded under the test conditions you chose. I won&#39;t go into the details here, but if you are interested in finding out more information about this view, I&#39;ve written a little in &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-the-filmstrip-view-and-waterfall-chart-are-related&quot;&gt;this section&lt;/a&gt; from one of my previous blog posts.&lt;/p&gt;&lt;h2 id=&quot;how-does-this-work&quot;&gt;How does this work&lt;/h2&gt;&lt;p&gt;So under the hood, all the WebPageTest compare functionality is doing is creating a really long URL with a whole bunch of query parameters included. For example, above we selected 3 tests to compare, so the resulting URL contains three 42 character long test ID&#39;s within the &lt;code&gt;tests&lt;/code&gt; parameter. This parameter driven functionality comes with two great advantages:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Results for the specific comparison view can be bookmarked and very easily shared with anyone&lt;/li&gt;&lt;li&gt;By manipulating the URL, you can change how the tests are rendered and compared on screen&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Now point 2 sounds very useful, but it also comes with a caveat: WebPageTest URL&#39;s can be huge, making editing them manually quite a chore! When you consider each test ID is 42 characters long, and you include all of the advanced settings you can choose from, editing a URL in the browsers address bar probably isn&#39;t a feasible option. Also, mistakes are easy to make. Misplace the comma between test ID&#39;s or forget to URL encode your custom label names and you&#39;ve got a broken page view on your hands.&lt;/p&gt;&lt;h2 id=&quot;webpagetest-comparison-url-generator&quot;&gt;WebPageTest comparison URL generator&lt;/h2&gt;&lt;p&gt;The scenario I mention above I&#39;ve personally struggled with too many times to count! So I recently decided to make my life a little easier and create a tool to do this URL manipulation for me. I&#39;ve also published it online so others can use it if they so wish. It&#39;s available at &lt;a href=&quot;https://wpt-compare.app/&quot;&gt;https://wpt-compare.app/&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The compare view with three tests being compared.&quot; decoding=&quot;async&quot; height=&quot;990&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/ADRHLqwGK6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/ADRHLqwGK6-300.webp 300w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/ADRHLqwGK6-600.webp 600w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/ADRHLqwGK6-774.webp 774w&quot; width=&quot;774&quot;&gt;&lt;/p&gt;&lt;p&gt;It really is quite a simple little application. All it is doing is building the long URL for you. But it comes with a few features that you may find useful:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Ability to add as many test URLs as you need&lt;/li&gt;&lt;li&gt;Drag / drop functionality to manipulate the order in which the tests appear&lt;/li&gt;&lt;li&gt;Quickly select which median metric is used in the comparison&lt;/li&gt;&lt;li&gt;Accepts multiple URL formats to extract the WebPageTest test ID&#39;s&lt;/li&gt;&lt;li&gt;Compatible with &lt;a href=&quot;https://speedcurve.com&quot;&gt;SpeedCurve&lt;/a&gt; test URLs and custom instances&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;ve also included all the &#39;Advanced customization options&#39; as seen in the modal that pops up after clicking &#39;Advanced customization options...&#39; under the &#39;Create Video&#39; button in the comparison view (see image above). These advanced settings include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Label: customise the label used to identify a specific test&lt;/li&gt;&lt;li&gt;Specific run: Select a specific test run to use in the comparison view&lt;/li&gt;&lt;li&gt;Repeat view: Compare the repeat view only, not the first view&lt;/li&gt;&lt;li&gt;Specific step: If you are using scripting to &lt;a href=&quot;https://www.webpagetest.org/result/200421_KN_53ca24fe433b9355841dd707f68662c9/&quot;&gt;navigate through a page journey&lt;/a&gt;, each page is considered a &#39;step&#39;. So choose a specific step to compare. (Thanks &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Specific end time: Stop the comparison view at a specific number of seconds (e.g. 2.3 seconds)&lt;/li&gt;&lt;li&gt;Background and text colour: I&#39;m not sure if anyone uses this feature to customise the text and background colour of the page, but it&#39;s there if needed&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;speedcurve-urls&quot;&gt;SpeedCurve URLs&lt;/h2&gt;&lt;p&gt;As mentioned in the feature list above, SpeedCurve URLs are accepted by the tool too. You may (or may not) know that SpeedCurve runs WebPageTest instances under the hood to generate their synthetic test results that power the graphs. So if you happen to find a graph where there has been a large change between tests, you can either drill down into the results within SpeedCurve itself (click &#39;View Test&#39; - see below), or you can look at them directly on the &lt;a href=&quot;https://support.speedcurve.com/en/articles/1427921-webpagetest-results&quot;&gt;SpeedCurve WebPageTest instance&lt;/a&gt;. It&#39;s worth noting that the traditional &#39;Test history&#39; functionality (mentioned earlier) is disabled on the SpeedCurve WebPageTest instances, probably due to the load on the servers it would create to list thousands of automated tests.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;View an individual test result from a SpeedCurve graph by clicking and selecting &#39;View Test&#39;.&quot; decoding=&quot;async&quot; height=&quot;540&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/zjaEiHc3NC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/zjaEiHc3NC-300.webp 300w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/zjaEiHc3NC-600.webp 600w, https://nooshu.com/blog/2020/04/22/webpagetest-comparison-url-generator/zjaEiHc3NC-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now as we use SpeedCurve at &lt;a href=&quot;https://gds.blog.gov.uk/&quot;&gt;GDS&lt;/a&gt; to monitor our services frontend performance, I find myself repeating this specific task a lot! So I added some helper functionality into the tool to allow me to quickly create the SpeedCurve specific comparison URLs. If you are a SpeedCurve user, why not give it a try.&lt;/p&gt;&lt;h2 id=&quot;graph-page-data-urls&quot;&gt;Graph page data URL&#39;s&lt;/h2&gt;&lt;p&gt;As of the 13th April 2021 I added the functionality to allow you to generate URL&#39;s for the &#39;Graph Page&#39;. With this view you can generate a set of graphs and compare different sets of test runs under specific conditions. I&#39;ve written a whole post about it that you can &lt;a href=&quot;https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/#webpagetest-compare-tool&quot;&gt;see here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;feedback-and-feature-requests&quot;&gt;Feedback and feature requests&lt;/h2&gt;&lt;p&gt;So there you have it: a simple tool to help you generate those huge WebPageTest comparison URLs for all your blogging and screenshotting needs! Hopefully you find it useful. If you do have any feedback, bugs, feature requests etc, please do reach out to me on &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;22/04/20: Initial post published.&lt;/li&gt;&lt;li&gt;13/04/21: Added information about using the tool for generating the graph page data view.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Improving perceived performance with the CSS `font-display` property</title>
    <link href="https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/" />
    <updated>2020-02-23T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/</id>
    <content type="html">&lt;p&gt;Typography on the web has come a long way since the days of &lt;a href=&quot;https://mikeindustries.com/sifr/&quot;&gt;Scalable Inman Flash Replacement (sIFR)&lt;/a&gt; and later &lt;a href=&quot;http://cufon.shoqolate.com/generate/&quot;&gt;cufón&lt;/a&gt;. It was tough times for typographers and frontend developers on the web back then. I used to dread seeing a PSD file with some exotic font used in the design, as I knew hours of cross-browser adjustments lay ahead. Thankfully, on the modern web we have the &lt;code&gt;@font-face&lt;/code&gt; CSS at-rule which allows us to specify a custom font when displaying our text. It hasn&#39;t always &lt;a href=&quot;https://www.paulirish.com/2009/bulletproof-font-face-implementation-syntax/&quot;&gt;been plain sailing&lt;/a&gt;, but today in 2020 many of the issues have been ironed out and browser support for &lt;a href=&quot;https://caniuse.com/#feat=woff2&quot;&gt;WOFF2&lt;/a&gt; fonts is looking really good. So what&#39;s the catch? Web Performance.&lt;/p&gt;&lt;p&gt;As the proverb says: &quot;With great power comes great responsibility&quot;. Give people the ability to use custom fonts easily on a website and surly that won&#39;t be abused? Unfortunately &lt;a href=&quot;https://almanac.httparchive.org/en/2019/fonts#how-many-web-fonts-are-too-many&quot;&gt;it does&lt;/a&gt;, and it has an impact on web performance, and ultimately the usability of a website.&lt;/p&gt;&lt;h2 id=&quot;fonts-and-web-performance&quot;&gt;Fonts and Web Performance&lt;/h2&gt;&lt;p&gt;If you use a non-system font on a website, the browser is going to have to fetch that font file from the server. Not a problem you may think, but if a site uses multiple custom fonts and font weights, or if you are having to support multiple languages, sometimes the size of the font files can make up a &lt;a href=&quot;https://www.webpagetest.org/result/200102_0X_f869ceba36185b385a67541dd2335bf5/1/breakdown/&quot;&gt;large percentage of the total page download size&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So if the font is downloading, what does the browser do with the content? Well, typically what browsers used to do was render an invisible placeholder font, then replace it with the actual font once it had downloaded. This method is known as the &lt;a href=&quot;https://css-tricks.com/fout-foit-foft/&quot;&gt;Flash of Invisible Text (FOIT)&lt;/a&gt;. That&#39;s not ideal, but at least a usr can see something right? But what happens if a user is on a slow connection and the font takes 10&#39;s of seconds to download? Or what if the font never loads at all? The user is left looking at a website that looks like this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The page is loading and the page outline if visible, but a user can&#39;t see the text because the font is still travelling over the network.&quot; decoding=&quot;async&quot; height=&quot;232&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/V3gNt8hICY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/V3gNt8hICY-300.webp 300w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/V3gNt8hICY-600.webp 600w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/V3gNt8hICY-769.webp 769w&quot; width=&quot;769&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example above a user is having to wait 5 seconds before any text is rendered to the viewport. That&#39;s a long time to be waiting considering &lt;a href=&quot;https://www.blog.google/products/admanager/the-need-for-mobile-speed/&quot;&gt;Google research&lt;/a&gt; found that 53% of mobile website visitors will leave a website if it doesn&#39;t load within three seconds. So what can we do to improve this perceived performance issue? &lt;code&gt;font-display&lt;/code&gt; to the rescue!&lt;/p&gt;&lt;h2 id=&quot;font-display-property&quot;&gt;Font display property&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;font-display&lt;/code&gt; property is defined in the &lt;a href=&quot;https://drafts.csswg.org/css-fonts-4/#font-display-desc&quot;&gt;CSS Fonts Module Level 4 specification&lt;/a&gt;. What it does is allow a developer to choose what happens during the font rendering phase of a page load. Just to be 100% clear, it has no effect on the actual loading of the font itself over the network, just what happens to typography on the page during this loading phase. Font display has five possible values, let&#39;s discuss these briefly:&lt;/p&gt;&lt;h3 id=&quot;block&quot;&gt;Block&lt;/h3&gt;&lt;p&gt;This is the default for many browsers. This setting will immediately draw the invisible placeholder text (known as the block period), invisibility period persists indefinitely until the web font is loaded. The browser has an infinite period to swap the invisible font for the web font once it has loaded.&lt;/p&gt;&lt;h3 id=&quot;fallback&quot;&gt;Fallback&lt;/h3&gt;&lt;p&gt;According to the specification:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;[Fallback] gives the font face an extremely small block period (100ms or less is recommended in most cases) and a short swap period (3s is recommended in most cases).&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So, on page load invisible text could be shown for up to 100ms (known as the block period). Beyond 100ms, the next font in the CSS font stack is displayed (e.g. Arial, Helvetica Neue, Helvetica, sans-serif). The browser is then given 3 seconds to swap the fallback font for the web font. Beyond 3 seconds the fallback font will be used for the rest of the page life and the web font will never be displayed (even if it successfully downloads).&lt;/p&gt;&lt;p&gt;The 3 second cutoff is a superb setting if you know you have users on a slow connection. Imagine a page loading on a slow device. The fallback font is rendered, so a user starts to read the content. They may be 5 - 10 seconds into reading, then suddenly the web font (that they know nothing about) suddenly loads. If the font metrics of the web font and fallback font are different, this can results in a &lt;a href=&quot;https://github.com/alphagov/govuk-frontend/issues/1437#issuecomment-589633065&quot;&gt;large page jump&lt;/a&gt; which is quite disruptive.&lt;/p&gt;&lt;p&gt;From my observations (as seen later in the article), the practical application of the specification is true to its word. Invisible placeholder text &lt;em&gt;is&lt;/em&gt; displayed for 100ms, then the fallback font is painted.&lt;/p&gt;&lt;h3 id=&quot;swap&quot;&gt;Swap&lt;/h3&gt;&lt;p&gt;The specification says:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;[Swap] gives the font face an extremely small block period (100ms or less is recommended in most cases) and an infinite swap period.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Or to put it another way, on page load the browser &lt;em&gt;could&lt;/em&gt; show the invisible text and then wait for up to 100ms before displaying the fallback font. Beyond this point the browser has as much time as needed to swap the fallback font for the web font once loaded.&lt;/p&gt;&lt;p&gt;But the practical implementation in browsers is different. For &lt;code&gt;swap&lt;/code&gt; there is no block period. The fallback font is displayed as soon as the page is painted. So even though the specification makes &lt;code&gt;fallback&lt;/code&gt; and &lt;code&gt;swap&lt;/code&gt; sounds very similar on page load, they are actually different in terms of perceived performance.&lt;/p&gt;&lt;h3 id=&quot;optional&quot;&gt;Optional&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;optional&lt;/code&gt; value is the one I find most interesting, yet it is the least used according to the &lt;a href=&quot;https://almanac.httparchive.org/en/2019/fonts#fighting-against-invisible-text&quot;&gt;2019 Web Almanac&lt;/a&gt;. The specification is &lt;a href=&quot;https://drafts.csswg.org/css-fonts-4/#valdef-font-face-font-display-optional&quot;&gt;the longest&lt;/a&gt; of the five values, but in simple terms: it gives the browser the option to abort the font download, or load it at the lowest priority. This setting could be very useful if you are on a slow cellular network. Should the browser decide that the connection is too slow for the font to load, it will abort the request completely and simply display the fallback font for the rest of the page&#39;s lifetime.&lt;/p&gt;&lt;p&gt;I&#39;m intrigued as to why this option isn&#39;t used more widely, given its obvious benefit to both the usability of a website and its improved data footprint for slow connections. But if I were to hazard a guess, I&#39;d say it&#39;s because it is very strict in terms of repainting. If the fallback font is displayed, the web font will &lt;strong&gt;never&lt;/strong&gt; be shown, even if loaded quickly. So this results in users on a fast device &amp; connection where the fallback has been painted, the web font has loaded but it isn&#39;t rendered. Only if the user navigates through to another page, is the web font then rendered. So I&#39;d imagine a number of designers being quite annoyed if their chosen web font isn&#39;t visible on initial page load!&lt;/p&gt;&lt;h3 id=&quot;auto&quot;&gt;Auto&lt;/h3&gt;&lt;p&gt;A very simple one to understand. &lt;code&gt;auto&lt;/code&gt; will use whatever setting the user-agent already has defined. For many browser this will be &lt;code&gt;block&lt;/code&gt;.&lt;/p&gt;&lt;h2 id=&quot;browser-support&quot;&gt;Browser support&lt;/h2&gt;&lt;p&gt;Browser support for &lt;code&gt;font-display&lt;/code&gt; is actually looking great, with around 82% of users globally &lt;a href=&quot;https://caniuse.com/#feat=mdn-css_at-rules_font-face_font-display&quot;&gt;using a browser that supports it&lt;/a&gt;. A couple of the surprising outliers for me are:&lt;/p&gt;&lt;h3 id=&quot;samsung-internet&quot;&gt;Samsung Internet&lt;/h3&gt;&lt;p&gt;Samsung Internet (SI) is a browser that has been gaining some &lt;a href=&quot;https://www.w3counter.com/globalstats.php?year=2020&amp;month=1&quot;&gt;market share&lt;/a&gt; on Android devices. It currently doesn&#39;t support &lt;code&gt;font-display&lt;/code&gt;. I suspect this is because SI v10.x is based on Chrome 71 (which didn&#39;t support &lt;code&gt;font-display&lt;/code&gt;). There are rumours on the web that v11.x will be &lt;a href=&quot;https://www.androidpolice.com/2019/10/22/samsung-internet-v11-includes-ui-changes-and-newer-chromium-base-apk-download/&quot;&gt;based on Chrome 75&lt;/a&gt;, which does support &lt;code&gt;font-display&lt;/code&gt;. So you never know, support may be just around the corner.&lt;/p&gt;&lt;h3 id=&quot;edge-chromium&quot;&gt;Edge (Chromium)&lt;/h3&gt;&lt;p&gt;Final edit of this section. It turns out that the data in caniuse was incorrect. Edge does support &lt;code&gt;font-display&lt;/code&gt; in both &lt;code&gt;@font-face&lt;/code&gt; and via the API, as this &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=200226_R2_d47a4449b35c9304f1b2cc9badc4d86f,200226_6X_01c6377112ad6db3543409f728162c6e,200226_EG_9a6eab6d3d230565dfc43de31233698a&quot;&gt;test proves&lt;/a&gt;. I &lt;a href=&quot;https://github.com/Fyrd/caniuse/issues/5301#issuecomment-591805093&quot;&gt;opened an issue&lt;/a&gt; and it was fixed in under 10 hours. The power of open source strikes again! If you check out the &lt;a href=&quot;https://caniuse.com/#feat=mdn-api_fontface_display&quot;&gt;FontFace API: display&lt;/a&gt; now, you will see the correct values. I&#39;m leaving this section here to remind readers you can&#39;t always trust your sources! Thanks to &lt;a href=&quot;https://twitter.com/zachleat&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Zach Leatherman&lt;/a&gt; for helping clear up this mystery, and &lt;a href=&quot;https://twitter.com/GooborgStudios&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;vinyldarkscratch&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/Fyrd&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Fyrd&lt;/a&gt; for the fixes.&lt;/p&gt;&lt;h2 id=&quot;testing-the-perceived-performance&quot;&gt;Testing the perceived performance&lt;/h2&gt;&lt;p&gt;So how can we test the effect these different &lt;code&gt;font-display&lt;/code&gt; settings have on a websites perceived page performance? One way would be to simply update your &lt;code&gt;@font-face&lt;/code&gt; rule, deploy it, then run it though WebPageTest. That works but there is another way that may simplify this workflow.&lt;/p&gt;&lt;p&gt;And that&#39;s by leveraging WebPageTest&#39;s &#39;Inject Script&#39; functionality (under the &#39;Advanced&#39; tab), and using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API&quot;&gt;CSS Font Loading API&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Inject script allows you to modify the font loading properties on a given page.&quot; decoding=&quot;async&quot; height=&quot;354&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/xFZInUhIri-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/xFZInUhIri-300.webp 300w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/xFZInUhIri-600.webp 600w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/xFZInUhIri-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we use a very similar method to what &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; demonstrates in his &lt;a href=&quot;https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/&quot;&gt;Improving Perceived Performance With the Link Rel=preconnect HTTP Header&lt;/a&gt; blog post.&lt;/p&gt;&lt;p&gt;You can find some simplified code below which can be used to do this, or on a &lt;a href=&quot;https://gist.github.com/Nooshu/c0c6dbeaad55a438ec30849bf8be1d96&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gist found here&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot; data-gist=&quot;c0c6dbeaad55a438ec30849bf8be1d96&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// this will trigger a font load&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; customFont1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;custom font name&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;url([FONT_URL_HERE])&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;block&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// display setting to test here&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;weight&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;700&#39;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// font-weight&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// other font properties here&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// IMPORTANT: add the font to the document&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;customFont1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// monitor the font load (optional)&lt;/span&gt;
  customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loaded&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// log some info in the console&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Font status:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// log some info on the WPT waterfall chart&lt;/span&gt;
    performance&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mark&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;wpt.customFont1Loaded&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// if there&#39;s an error, tell us about it&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Font status:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// repeat above for multiple fonts&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Modify the above code and paste it into the &#39;Inject Script&#39; textarea. The above code will be injected into the test page before the test executes and will load and define a new font (using the same &lt;code&gt;font-family&lt;/code&gt; name) with your updated settings.&lt;/p&gt;&lt;p&gt;The key to get this solution to work is to ensure the manually added font is added &lt;strong&gt;after&lt;/strong&gt; the CSS &lt;code&gt;@font-face&lt;/code&gt; fonts are registered. As the priority of fonts with the same &lt;code&gt;font-family&lt;/code&gt; name is: the last one added wins. This mirrors CSS, e.g. if you have two selectors with the same specificity, the one which comes last &#39;overwrites&#39; the first.&lt;/p&gt;&lt;p&gt;Lets look at hypothetical test timeline:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;A WebPageTest run starts, the browser negotiates the server connection and requests the HTML.&lt;/li&gt;&lt;li&gt;HTML is downloaded and parsed, other page assets like CSS and JS are requested.&lt;/li&gt;&lt;li&gt;At this point WPT injects the JavaScript and it executes. Our new font is defined and loaded using the Font Loading API.&lt;/li&gt;&lt;li&gt;The CSS is downloaded and parsed, it also contains an &lt;code&gt;@font-face&lt;/code&gt; rule with the same basic info (font name, font URL etc).&lt;/li&gt;&lt;li&gt;The &lt;code&gt;FontFace&lt;/code&gt; object and &lt;code&gt;@font-face&lt;/code&gt; are CSS-connected. The parsing of the CSS adds the font to the bottom of the list (it is last), so it is prioritised over our manually added font.&lt;/li&gt;&lt;li&gt;Page loads as normal, no changes are observed from the injected script.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Point 3 is the part of this process that is unpredictable. There are no timing guarantees that the injected script will run after CSS download / parsing. It may be different depending on the browser and it runs as soon as the document exists. So if the above code doesn&#39;t work for you in your particular site setup, what else can you try?&lt;/p&gt;&lt;h3 id=&quot;solutions&quot;&gt;Solutions&lt;/h3&gt;&lt;p&gt;So there are two solutions that I have found to rectify this issue:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Block the font CSS&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This one is less than ideal, but it works. If you happen to have your &lt;code&gt;@font-face&lt;/code&gt; rules defined in their own separate CSS file, then you can simply use WebPageTest&#39;s &#39;block&#39; functionality to block the font CSS:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using the WPT UI you can block assets from loading. Here we block the font CSS from loading.&quot; decoding=&quot;async&quot; height=&quot;419&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/QBG5nBx-OS-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/QBG5nBx-OS-300.webp 300w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/QBG5nBx-OS-600.webp 600w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/QBG5nBx-OS-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;What happens in this situation is our injection script creates all the relevant &lt;code&gt;FontFace&lt;/code&gt; objects, and they don&#39;t get deprioritised by the &lt;code&gt;@font-face&lt;/code&gt; rules in the CSS. There are a couple of major issues with this method:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;What happens if you have lots of fonts? You need to replicate them all in the injection script.&lt;/li&gt;&lt;li&gt;What about if your font CSS is concatenated along with your other CSS? You essentially need to block your whole pages CSS.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;So yes, this method could work, but it&#39;s far from perfect.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Detect the CSS load&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: The following method does work, but web page load is complex and &lt;a href=&quot;https://en.wikipedia.org/wiki/Nondeterministic_algorithm&quot;&gt;nondeterministic&lt;/a&gt;. So what works for one run (as seen in the tests below), suddenly will stop working in other tests using the exact same code. The CSS blocking method is much more predictable.&lt;/p&gt;&lt;p&gt;A better method I have found is to hook into the fact that you can &lt;a href=&quot;https://www.phpied.com/when-is-a-stylesheet-really-loaded/&quot;&gt;detect when a CSS file has loaded&lt;/a&gt; with a little bit of JavaScript. It is possible to modify the injection script to do this. Here&#39;s a &lt;a href=&quot;https://gist.github.com/Nooshu/c857ce6ed4d644cee73bc53f82685058&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; with the modified code:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// create our custom link tag for the stylesheet&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://www.example.com/static/app.css&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// IMPORTANT: this is the CSS file that contains your @font-face rules&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; head &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementsByTagName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;head&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; link &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;link&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  link&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text/css&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  link&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;rel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;stylesheet&quot;&lt;/span&gt;
  link&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;href &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// append the stylesheet to the head&lt;/span&gt;
  head&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;link&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// wait for the CSS file to load before modifying the font setup&lt;/span&gt;
  link&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// define our font face and modify the properties (will trigger a load)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; customFont1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;nta&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;url([FONT_URL_HERE])&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;swap&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// display setting to test here&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;weight&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;700&#39;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// font-weight&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// other font properties here&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// IMPORTANT: add the modified font to the FontFaceSet&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;customFont1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// monitor the font load (optional)&lt;/span&gt;
    customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loaded&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// log some info in the console&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Font status:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// log some info on the WPT waterfall chart&lt;/span&gt;
      performance&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mark&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;wpt.customFont1Loaded&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fontFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// if there&#39;s an error, tell us about it&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Font status:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; customFont1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// optional&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// repeat above for multiple fonts&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With this script we are hooking into the load event of the CSS that contains our &lt;code&gt;@font-face&lt;/code&gt; rules (this is important). So here&#39;s what is happening in the browser:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;A WebPageTest run starts, the browser negotiates the server connection and requests the HTML.&lt;/li&gt;&lt;li&gt;HTML is downloaded and parsed, other page assets like CSS and JS are requested.&lt;/li&gt;&lt;li&gt;At this point WPT injects the JavaScript and it may execute. If it does we create a copy of the &lt;code&gt;&amp;lt;link&gt;&lt;/code&gt; element that is loading our &lt;code&gt;@font-face&lt;/code&gt; CSS and wait for it to load.&lt;/li&gt;&lt;li&gt;CSS loads and is parsed. Browser creates &lt;code&gt;FontFace&lt;/code&gt; objects for the &lt;code&gt;@font-face&lt;/code&gt; rules in the CSS (CSS-connected).&lt;/li&gt;&lt;li&gt;CSS load event fires and our custom &lt;code&gt;FontFace&lt;/code&gt; objects are added to the &lt;code&gt;FontFaceSet&lt;/code&gt; last, so is therefore prioritised over the CSS-connected font settings (e.g. the &lt;code&gt;font-display&lt;/code&gt; property).&lt;/li&gt;&lt;li&gt;Page loads with our modified font settings.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;And there you have it. A quick way to test out different &lt;code&gt;font-display&lt;/code&gt; settings on a live website and observe the results in WebPageTest.&lt;/p&gt;&lt;h2 id=&quot;debugging-a-webpagetest-run&quot;&gt;Debugging a WebPageTest run&lt;/h2&gt;&lt;p&gt;This is a really useful tip from &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; that I&#39;ve slightly modified for these examples. If you want to see what the console output is for a browser in a test. Paste the following code into the &#39;Inject Script&#39; textarea:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// hijack the console.log method, allowing you to log all console massages&lt;/span&gt;
window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_consoleMessages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;_consoleMessages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;m&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// specifically used for this font example to &#39;copy&#39; the `FontFace` object&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; key &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        result&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the following into the &#39;Custom Metrics&#39; box under the &#39;Custom&#39;:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;consoleMessages&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; font &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// push results into the hijacked console&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// could easily return a simple array though if required&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_consoleMessages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img alt=&quot;The custom metrics tab allows you to capture custom metrics from a test.&quot; decoding=&quot;async&quot; height=&quot;278&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/PD2TejEOR2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/PD2TejEOR2-300.webp 300w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/PD2TejEOR2-600.webp 600w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/PD2TejEOR2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This will give you an output that looks similar to this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The custom metrics can be found inside the test run, and they give a list of outputs captured during the test.&quot; decoding=&quot;async&quot; height=&quot;285&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/p6wHJD3EsK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/p6wHJD3EsK-300.webp 300w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/p6wHJD3EsK-600.webp 600w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/p6wHJD3EsK-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Very useful if you are looking for specific information about a browser in a test run.&lt;/p&gt;&lt;h2 id=&quot;lets-see-it-in-action&quot;&gt;Let&#39;s see it in action&lt;/h2&gt;&lt;p&gt;I&#39;ve created a &lt;a href=&quot;https://keen-northcutt-3190bd.netlify.com/&quot;&gt;super simple set of pages&lt;/a&gt; to demonstrate this technique. There&#39;s also a little information on what is printed to the &lt;code&gt;console&lt;/code&gt; in browsers that support the API. If you are familiar with DevTools, simply open them up and you will see the information in the console. I&#39;ve created 4 test pages:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Standard page with a single web font loaded via the the CSS &lt;code&gt;@font-face&lt;/code&gt; rule. This is our control (&lt;code&gt;font-display: auto&lt;/code&gt;) &lt;a href=&quot;https://keen-northcutt-3190bd.netlify.com/standard-load.html&quot;&gt;link&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Standard page + the same web font added a second time using the Font Loading API, with &lt;code&gt;font-display: swap&lt;/code&gt; set &lt;a href=&quot;https://keen-northcutt-3190bd.netlify.com/manually-added-font-swap.html&quot;&gt;link&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Standard page + the same web font added a second time using the Font Loading API, with &lt;code&gt;font-display: fallback&lt;/code&gt; set &lt;a href=&quot;https://keen-northcutt-3190bd.netlify.com/manually-added-font-fallback.html&quot;&gt;link&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Standard page + the same web font added a second time using the Font Loading API, with &lt;code&gt;font-display: optional&lt;/code&gt; set &lt;a href=&quot;https://keen-northcutt-3190bd.netlify.com/manually-added-font-optional.html&quot;&gt;link&lt;/a&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Pages 2, 3 and 4 are a proof-of-concept. They each include the above script after the &lt;code&gt;fonts.css&lt;/code&gt; file. So on these pages the font will be manually added to the &lt;code&gt;FontFaceSet&lt;/code&gt; along with a new &lt;code&gt;font-display&lt;/code&gt; property. Will it show any changes in WebPageTest? &lt;strong&gt;NOTE&lt;/strong&gt;: The web font is only applied to the header (h1) on the pages, so that&#39;s what you should be concentrating on in the following tests.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A filmstrip from the 4 pages showing the difference the script can have on font loading.&quot; decoding=&quot;async&quot; height=&quot;84&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/z9dayQKRsF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/z9dayQKRsF-300.webp 300w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/z9dayQKRsF-600.webp 600w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/z9dayQKRsF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You can take a closer look at the &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=200223_91_cb491494002d2edf7a9cf73cb948ef69%2C200223_31_ccdf8eeaa525ea23659450885d0066b6%2C200223_83_568bae4bebe84705bb7c0925c28f432c%2C200223_8V_34899456d359e7a6b939e3d81d87f5ea&amp;thumbSize=200&amp;ival=16.67&amp;end=all&quot;&gt;comparison of the 4 pages here&lt;/a&gt;, but here are the results as I see them:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Optional: The header rendered at the start with invisible placeholder text. ~100ms later fallback text is rendered. The web font never renders.&lt;/li&gt;&lt;li&gt;Fallback: The header rendered at the start with invisible placeholder text. ~100ms later fallback text is rendered. The font loads within the 3 second cut-off, so is swapped out.&lt;/li&gt;&lt;li&gt;Swap: The header immediately loads with the fallback font visible (e.g. the 0s block period in action). The fallback is swapped for the web font once loaded.&lt;/li&gt;&lt;li&gt;Auto: As expected, this is set to &lt;code&gt;block&lt;/code&gt;. Invisible placeholder text is rendered, no text is shown until the web font is loaded.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;All of the above results are the expected outcome for each &lt;code&gt;font-display&lt;/code&gt; value, and the fact that we see a difference shows that a font added programmatically via the CSS Font Loading API, &lt;em&gt;can&lt;/em&gt; change the page. So let&#39;s take it one step further. Let&#39;s get WebPageTest to inject the script for us.&lt;/p&gt;&lt;p&gt;In these tests we use the &lt;a href=&quot;https://keen-northcutt-3190bd.netlify.com/standard-load.html&quot;&gt;control page&lt;/a&gt; in all instances, then get WebPageTest to inject the script with each of the different &lt;code&gt;font-display&lt;/code&gt; values. A full comparison of the results from these tests can be &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=200223_NZ_e38d2fab336f5249fc90e301d7444c42%2C200223_G4_bc65cb4cfc4e15afa31c1e0c6727828e%2C200223_9X_3d81a9682a97926bb03beaf9c7fdf752%2C200223_8V_34899456d359e7a6b939e3d81d87f5ea&amp;thumbSize=200&amp;ival=16.67&amp;end=all&quot;&gt;seen here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Results from using WebPageTest to inject the scripts and change the font settings.&quot; decoding=&quot;async&quot; height=&quot;84&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/OLa1BziiB4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/OLa1BziiB4-300.webp 300w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/OLa1BziiB4-600.webp 600w, https://nooshu.com/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/OLa1BziiB4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It looks remarkably similar to the image above doesn&#39;t it? In fact, on closer inspection we see exactly the same results as listed above for our manually injected script pages. So we &lt;em&gt;can&lt;/em&gt; actually use WebPageTest to change a pages &lt;code&gt;font-display&lt;/code&gt; property (and really &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/FontFace#Properties&quot;&gt;any property that isn&#39;t read only&lt;/a&gt;). I&#39;m sure others may be able to come up with other uses for this technique too. Give it a try for yourself: check out &lt;a href=&quot;https://gist.github.com/Nooshu/bca22676f3491f548c297774c537b865&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;this Gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; with a link to the test page and the script for you to play with.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;And there you have it, a way to change the &lt;code&gt;font-display&lt;/code&gt; settings of a page when using WebPageTest. No need to manually update the code to see what effect the property has on a pages perceived performance. I&#39;d love to hear your ideas and / or improvements that can be made to this method, so please let me know via &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;23/02/20: Initial post published.&lt;/li&gt;&lt;li&gt;25/02/20: Posted an update about &lt;code&gt;font-display&lt;/code&gt; support on Edge (Chromium). Thanks &lt;a href=&quot;https://twitter.com/zachleat&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Zach Leatherman&lt;/a&gt;!&lt;/li&gt;&lt;li&gt;26/02/20: Posted another update around &lt;code&gt;font-display&lt;/code&gt; support in Edge. It doesn&#39;t support &lt;code&gt;display&lt;/code&gt; in the Font Loading API.&lt;/li&gt;&lt;li&gt;27/02/20: Final update for &lt;code&gt;font-display&lt;/code&gt; in Edge. It is fully supported in both &lt;code&gt;@font-face&lt;/code&gt; and the API. Caniuse data was incorrect!&lt;/li&gt;&lt;li&gt;28/02/20: Added information about debugging a test run via the &#39;Inject Script&#39; and &#39;Custom Metrics&#39; functionality.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>The impact of SSL certificate revocation on web performance</title>
    <link href="https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/" />
    <updated>2020-01-26T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/</id>
    <content type="html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;&lt;p&gt;The process of establishing a connection to a website is a complicated process. First the browser must turn the domain name into an IP address (DNS lookup), then once found it must negotiate a connection with the server via Transmission Control Protocol (TCP). Lastly if the site is being served over HTTPS (which &lt;a href=&quot;https://httparchive.org/reports/state-of-the-web#pctHttps&quot;&gt;around 83%&lt;/a&gt; of web requests are), the browser needs to negotiate an encrypted connection (via Transport Layer Security (TLS)) with the server. The last TLS stage is where digital certificates are involved, and this process is what the rest of this blog post will focus on.&lt;/p&gt;&lt;p&gt;If you&#39;d like more information on the transport stage of the connection process, check out the High Performance Browser Networking chapters &lt;a href=&quot;https://hpbn.co/building-blocks-of-tcp/&quot;&gt;here&lt;/a&gt; and &lt;a href=&quot;https://hpbn.co/building-blocks-of-udp/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;types-of-certificate-validation-levels&quot;&gt;Types of certificate validation levels&lt;/h2&gt;&lt;p&gt;There are three validation levels that are used with digital certificates, each with their own criteria to be met:&lt;/p&gt;&lt;h3 id=&quot;domain-validation&quot;&gt;Domain Validation&lt;/h3&gt;&lt;p&gt;To obtain a Domain Validation (DV) certificate you must prove you have control over &lt;strong&gt;one&lt;/strong&gt; of the following criteria related to a domain: the whois records, DNS records file, email or web hosting account.&lt;/p&gt;&lt;h3 id=&quot;organisation-validation&quot;&gt;Organisation Validation&lt;/h3&gt;&lt;p&gt;An Organisation Validation (OV) class certificate will be issued if you can meet &lt;strong&gt;two&lt;/strong&gt; criteria related to a domain: one of the rights to administer a domain (as listed above with DV certificates), and an organisations existence as a legal entity.&lt;/p&gt;&lt;h3 id=&quot;extended-validation&quot;&gt;Extended Validation&lt;/h3&gt;&lt;p&gt;The Extended Validation (EV) class of verification is the most comprehensive of the levels. To obtain one you must prove you have control over the right to administer a domain (as with DV and OV certificates), an organisations existence as a legal entity (as with OV certificates), and there will also be verification &lt;a href=&quot;https://expeditedsecurity.com/blog/are-ev-ssl-certificates-worth-it/#the-company-exists-at-an-qualified-independent-information-source-and-when-contacted-someone-will-confirm-the-admin-contact-s-authority-in-the-company-&quot;&gt;checks by a verifying agent from the CA&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;An important point to note about all three certificates above, in terms of technology they are exactly the same. They are all &lt;a href=&quot;https://en.wikipedia.org/wiki/X.509&quot;&gt;X.509&lt;/a&gt; public key certificates. The only difference is what is contained within the certificates. As you might expect, EV certificates will &lt;a href=&quot;https://expeditedsecurity.com/blog/are-ev-ssl-certificates-worth-it/#show-me-&quot;&gt;contain more information&lt;/a&gt; and have different properties set to distinguish them from DV/OV certificates. For example, EV certs have a different &lt;code&gt;subject&lt;/code&gt; which identifies a legal entity rather than a domain.&lt;/p&gt;&lt;h2 id=&quot;establishing-trust&quot;&gt;Establishing trust&lt;/h2&gt;&lt;p&gt;So how does your browser ensure that the site being visited is who they say they are?&lt;/p&gt;&lt;h3 id=&quot;certificate-authority&quot;&gt;Certificate Authority&lt;/h3&gt;&lt;p&gt;A Certificate Authority (CA) is the trusted organisation that will issue a certificate once you (or your company) have met the criteria listed above. They are the &lt;a href=&quot;https://en.wikipedia.org/wiki/Trusted_third_party&quot;&gt;trusted third party&lt;/a&gt; between the browser (user) and the server (website). When communicating with a server we want to make sure that they are who they say they are. And this is achieved by completing one of the validation processes mentioned earlier.&lt;/p&gt;&lt;h3 id=&quot;the-chain-of-trust&quot;&gt;The Chain of Trust&lt;/h3&gt;&lt;p&gt;There is something called a &lt;a href=&quot;https://en.wikipedia.org/wiki/Chain_of_trust&quot;&gt;Chain of Trust&lt;/a&gt; which is relied upon when establishing the encrypted tunnel. And it works a little like this. The CA holds what is known as the root certificate and key, which is used to sign (verify) the intermediate certificate. The intermediate certificate and its key is then used to sign the sites (leaf) certificate.&lt;/p&gt;&lt;p&gt;The browser trusts the CA (root) which has signed the intermediate certificate. So the browser will also trust the intermediate certificate. The same process happens with the site certificate. It has been signed by the intermediate (via the root) so the browser now trusts the site certificate. With a chain of trust linking site certificate to the root, the browser can now trust the site.&lt;/p&gt;&lt;p&gt;If at any point in the process a site certificate has been modified in any way, it wouldn&#39;t validate against the layer directly above it (intermediate then root). The browser would be able to see that the Chain of Trust has been broken and will report back to the user that the encrypted tunnel has been compromised (an unsecured network).&lt;/p&gt;&lt;p&gt;A SSL error can also happen if the webserver isn&#39;t correctly setup, and doesn&#39;t provide correct intermediate certificates in the chain to lead back to the root. &lt;a href=&quot;https://docs.google.com/document/d/1ryqFMSHHRDERg1jm3LeVt7VMfxtXXrI8p49gmtniNP0/edit?pli=1&quot;&gt;In a report from 2016&lt;/a&gt;, missing intermediates account for &gt;10% of all certificate validation errors in Chrome. Thankfully there is an extension in SSL certificates called &lt;a href=&quot;http://www.pkiglobe.org/auth_info_access.html&quot;&gt;Authority Information Access (AIA)&lt;/a&gt; that provides information about the issuer, and will include a link to the intermediate certificates if they are missing. Your users therefore shouldn&#39;t see an ugly &lt;code&gt;NET::ERR_CERT_AUTHORITY_INVALID&lt;/code&gt; error message if AIA is used. NOTE: If the browser does AIA, it will slow down the SSL negotiation as the browser needs to fetch the intermediate certificate from the CA before the SSL negotiation can complete. So remember, &lt;a href=&quot;https://www.sslshopper.com/ssl-checker.html&quot;&gt;check that your server is serving your intermediate certificates correctly&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;For a little more clarification, there are a few more details on each of the certificate types below:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Root certificate - usually physically distributed with the operating system and is used to issue intermediate certificates.&lt;/li&gt;&lt;li&gt;Intermediate certificate(s) - CA&#39;s use the intermediate certificates to insulate themselves from having to use the root certificate to sign leaf certificates. A compromised root certificate is bad, so issuing off an intermediate certificate is much safer. It&#39;s next to impossible to revoke a root and replace it, and it would take years for the new one to filter through the ecosystem.&lt;/li&gt;&lt;li&gt;Leaf certificate - This is the sites certificate which is issued from the intermediate certificate.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;NOTE: It is possible to have multiple intermediate certificates, although not recommended as it adds extra steps to the checking process.&lt;/p&gt;&lt;h3 id=&quot;tls-handshake&quot;&gt;TLS Handshake&lt;/h3&gt;&lt;p&gt;When using HTTPS (as is &lt;a href=&quot;https://httparchive.org/reports/state-of-the-web#pctHttps&quot;&gt;increasingly common&lt;/a&gt; for websites and is a recommended best practice), the connection must be setup and secured cryptographically. The method of encryption and which keys to use are negotiated during the TLS handshake, which must happen before a HTTPS connection can be established. Before any application data can be sent over the connection, the HTTPS connection must be established.&lt;/p&gt;&lt;p&gt;It&#39;s important to remember that a TLS handshake doesn&#39;t come for free. Time and resources are used to establish the secure connection. It can typically take 2-RTT to establish this encrypted tunnel (depending on the server setup), but if the certificate size is large it could be more. If the server supports TLSv1.3 then it is possible to establish the connection in 1-RTT, or even &lt;a href=&quot;https://blog.cloudflare.com/introducing-0-rtt/&quot;&gt;0-RTT&lt;/a&gt; in some circumstances.&lt;/p&gt;&lt;p&gt;For example, let&#39;s assume our hypothetical server takes 2-RTT to establish this encrypted tunnel, and our user is on a mobile with a 3G connection. A 3G connection will likely have a minimum RTT of around 300 ms. So it will take a minimum of 600 ms to establish the encrypted tunnel, and that&#39;s not even taking into account the DNS lookup, connection negotiation and any network congestion. This is why Transmission Control Protocol (TCP) connections are even more expensive in the new world of HTTPS. Thankfully &lt;a href=&quot;https://blog.cloudflare.com/the-road-to-quic/&quot;&gt;QUIC&lt;/a&gt;, a new internet transport protocol which uses User Datagram Protocol (UDP) has been created which should hopefully solve this issue.&lt;/p&gt;&lt;h2 id=&quot;broken-trust&quot;&gt;Broken trust&lt;/h2&gt;&lt;p&gt;There are times where the trust between browser and server can be broken. It could be that a certificate has been compromised, and when this happens there needs to be a way to let the browser know about it. If the certificate has been compromised, you don&#39;t want a person with malicious intentions to be able to mimic a trusted website. That would be incredibly damaging to users, the website, and the Certificate Authority involved. To allow this to happen, ways to revoke certificates have been invented:&lt;/p&gt;&lt;h3 id=&quot;certificate-revocation-list&quot;&gt;Certificate Revocation List&lt;/h3&gt;&lt;p&gt;A Certificate Revocation List (CRL) is exactly what the name suggests. It is a large list containing the serial numbers of revoked certificates. Each and every CA updates this list regularly, and the list is shared with browsers. Now as you can imagine, the web has a lot of certificates, and therefore there&#39;s going to be a lot of revoked certificates. So this list will continue to grow and grow. Thankfully expired certificates aren&#39;t revoked as there&#39;s no need too, and their serials are often trimmed from the CRL&#39;s.&lt;/p&gt;&lt;p&gt;To use the list, a browser must download it in its entirety, and loop through each serial number to check and see if the specific certificate they are examining has been revoked. This takes both time and resources, which can slow down the TLS handshake. And what happens if the browser can&#39;t update the CRL? Does it just assume that the certificate it has been sent hasn&#39;t been revoked, then proceed as normal hoping there isn&#39;t an issue with it? (this is known as a &#39;soft-fail&#39;). Given that the vast majority of certificates are NOT revoked, a &#39;soft-fail&#39; is really the only viable option. Unfortunately a &#39;soft-fail&#39; strategy has a critical security flaw, as &lt;a href=&quot;https://blog.cloudflare.com/high-reliability-ocsp-stapling/#anamesoftfailasoftfailitsnotmuchbetter&quot;&gt;described here&lt;/a&gt;. How a &#39;soft-fail&#39; is handled depends entirely on the browser being used.&lt;/p&gt;&lt;h3 id=&quot;online-certificate-status-protocol&quot;&gt;Online Certificate Status Protocol&lt;/h3&gt;&lt;p&gt;So if CRL doesn&#39;t work particularly well, what else can we use? This is the reason the Online Certificate Status Protocol (OCSP) was created. Essentially, it allows a browser to send information about the certificate it is verifying to an OCSP responder, and the responder will send a message back saying either: &lt;code&gt;Good&lt;/code&gt;, &lt;code&gt;Revoked&lt;/code&gt; or &lt;code&gt;Unknown&lt;/code&gt;. The payload is small and looping through a huge list of revoked certificate serials isn&#39;t required. Sounds better doesn&#39;t it?&lt;/p&gt;&lt;p&gt;So with the creation of OCSP has the problem of certificate revocation been solved? I&#39;m afraid not. OCSP also comes with a number of issues:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Checking requires a TCP connection to the OCSP responder - This network overhead takes time to establish and adds latency to the SSL negotiation.&lt;/li&gt;&lt;li&gt;OCSP slows down the TLS handshake - The SSL negotiation cannot fully complete until a response is received from the OCSP responder. (there is a fallback which I mentioned earlier: &#39;soft-fail&#39;)&lt;/li&gt;&lt;li&gt;An OCSP responder can sometimes fail - What happens if the browser doesn&#39;t get a response? It can either blindly trust the certificate is still valid (&#39;soft-fail&#39;), or terminate the connection (&#39;hard-fail&#39;).&lt;/li&gt;&lt;li&gt;Privacy concerns - By contacting the responder, the browser is leaking what website a user is accessing to the CA. Sounds like a potential method to &lt;a href=&quot;https://www.bleepingcomputer.com/news/security/firefox-users-fingerprinted-via-cached-intermediate-https-certificates/&quot;&gt;track a person across the web&lt;/a&gt; that they don&#39;t know about or opt-out of.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;proprietary-systems&quot;&gt;Proprietary systems&lt;/h3&gt;&lt;p&gt;Due to the issues with both CRL and OCSP, browser vendors and Operating System (OS) manufacturers have come up with their own proprietary systems for handling certificate revocations:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Chrome (Chromium) uses &lt;a href=&quot;https://dev.chromium.org/Home/chromium-security/crlsets&quot;&gt;CRLSets&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Firefox uses &lt;a href=&quot;https://blog.mozilla.org/security/2015/03/03/revoking-intermediate-certificates-introducing-onecrl/&quot;&gt;OneCRL&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Safari is handled at the &lt;a href=&quot;https://github.com/crtsh/certwatch_db/issues/52&quot;&gt;OS level&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Edge(Chromium) uses &lt;a href=&quot;https://dev.chromium.org/Home/chromium-security/crlsets&quot;&gt;CRLSets&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Edge(EdgeHTML) handles it at the &lt;a href=&quot;https://www.sysadmins.lv/blog-en/dump-authroot-and-disallowed-certificates-with-powershell.aspx&quot;&gt;OS level&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Internet Explorer handles it at the &lt;a href=&quot;https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/ee619754(v=ws.10)?redirectedfrom=MSDN#certificate-validation&quot;&gt;OS level&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Mobile browsers... could be &lt;a href=&quot;https://books.google.co.uk/books?id=y11NBQAAQBAJ&amp;pg=PA164&amp;lpg=PA164&amp;dq=CRLsets+android&amp;source=bl&amp;ots=nVZEGTsU4B&amp;sig=ACfU3U2FhCuJp_ZGYNavA-bj2oxBLaU_xg&amp;hl=en&amp;sa=X&amp;ved=2ahUKEwivt7qs7JvnAhVWh1wKHb6GB_MQ6AEwA3oECAoQAQ#v=onepage&amp;q=CRLsets%20android&amp;f=false&quot;&gt;OS level&lt;/a&gt;, I&#39;m not 100% sure.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is my current understanding from lots of searching. If anyone has any information on how mobile browser revocation works I&#39;d love to know. I&#39;ve struggled to find any definitive answers.&lt;/p&gt;&lt;h3 id=&quot;how-browsers-handle-ocsp&quot;&gt;How browsers handle OCSP&lt;/h3&gt;&lt;p&gt;Next, let&#39;s examine what browsers do with OCSP when the different types of certificates are encountered, as this is an important consideration when it comes to web performance. For this investigation I&#39;ve used a mixture of &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt; for the visualisation of the waterfall, &lt;code&gt;tcpdump&lt;/code&gt; and &lt;a href=&quot;https://www.wireshark.org/&quot;&gt;Wireshark&lt;/a&gt; for the packet level analysis, and plain old Google detective work.&lt;/p&gt;&lt;p&gt;The spreadsheet I&#39;ve compiled can be &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1ZglJ9tYwIXLDz-sPmnXXo18euquWYJcjqyTI5J0bpgU/edit?usp=sharing&quot;&gt;found here&lt;/a&gt;. There&#39;s some missing information where I either couldn&#39;t test, or couldn&#39;t find any solid information about what happens. If you spot any errors or can fill in the blanks please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I have to admit, finding this information isn&#39;t easy. Browsers progress so fast, documentation can quickly become out of date, and analysing the network for OCSP requests &lt;a href=&quot;https://twitter.com/sleevi_/status/1219866395242115072&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;isn&#39;t as simple as I first thought it would be&lt;/a&gt;. But assuming that the spreadsheet is mostly correct, you will see that a number of browsers actually use OCSP to check the revocation status of SSL certificates.&lt;/p&gt;&lt;h2 id=&quot;web-performance-and-ocsp&quot;&gt;Web Performance and OCSP&lt;/h2&gt;&lt;p&gt;So as OCSP checks are made in quite a number of browsers (and more specifically always made with EV certificates in Chrome), what&#39;s the issue with OCSP and web performance? Let&#39;s go into a few details:&lt;/p&gt;&lt;h3 id=&quot;tcp-connections&quot;&gt;TCP connections&lt;/h3&gt;&lt;p&gt;A OCSP revocation check to a responder requires opening one or more TCP connections. Now admittedly a couple of TCP connections (over HTTP, not HTTPS) to another domain isn&#39;t a big deal (especially considering the number of &lt;a href=&quot;https://httparchive.org/reports/page-weight#reqJs&quot;&gt;JavaScript requests&lt;/a&gt; on sites nowadays). But it&#39;s the position they occur at in the waterfall that is important. They are right at the start of the waterfall. If there are network connectivity issues with the responder, or you are on a flakey mobile connection, this TCP connection is going to damage the performance. And considering no actual HTTP data can be transferred until the connection is established, your user could end up staring at a completely blank page.&lt;/p&gt;&lt;p&gt;In the example waterfall below you can see two TCP connections to two separate domains. Request 1 is for the intermediate certificates OCSP check, and request 2 is the OCSP check for the leaf certificate.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the example waterfall we see 2 TCP connections for the OCSP check, one for the intermediate certificate, the other for the leaf.&quot; decoding=&quot;async&quot; height=&quot;188&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/UJ6AAZC2NM-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/UJ6AAZC2NM-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/UJ6AAZC2NM-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/UJ6AAZC2NM-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Don&#39;t be fooled into thinking you only require a single TCP connection to an OCSP responders domain either, as that isn&#39;t always the case. In the example below (taken from the WebPageTest connection view), the browser opens 3 connections to the responder:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here the browser opens three TCP connections to the same OCSP responder.&quot; decoding=&quot;async&quot; height=&quot;202&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/mF7ZO0Bp4A-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/mF7ZO0Bp4A-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/mF7ZO0Bp4A-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/mF7ZO0Bp4A-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;And I&#39;m not quite sure what is happening with this connection view below. We have 1 successful attempt to the responder at request 1, and what looks to be 5 failed requests. Then later in the waterfall we see 5 successful connections to the same responder. It&#39;s &lt;a href=&quot;https://www.webpagetest.org/result/180709_WZ_e188d139e9222bed762058403b0af0d6/1/details/#waterfall_view_step1&quot;&gt;quite an unusual waterfall chart&lt;/a&gt; and I&#39;d like to think there&#39;s something unusual with either the server setup or test itself, and this is a rare occurrence.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In this connection view we see 11 connections to OCSP responders (both attempted and successful).&quot; decoding=&quot;async&quot; height=&quot;271&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/6im4qAM1Yr-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/6im4qAM1Yr-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/6im4qAM1Yr-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/6im4qAM1Yr-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;roundtrip-times&quot;&gt;Roundtrip Times&lt;/h3&gt;&lt;p&gt;When it comes to web performance and loading websites quickly, you may think it is the bandwidth of the connection that is the most important factor. But that &lt;a href=&quot;https://hpbn.co/building-blocks-of-tcp/#slow-start&quot;&gt;isn&#39;t the case&lt;/a&gt;. It&#39;s actually the roundtrip time (RTT). The longer it takes a packet to travel to the server and the response coming back again, the slower the whole experience will be for a user. Let&#39;s see what happens if the RTT is large:&lt;/p&gt;&lt;p&gt;Here&#39;s an example setup to demonstrate:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;A WebPageTest run on Apple.com (which uses an EV certificate)&lt;/li&gt;&lt;li&gt;Google Chrome used as the browser so an OCSP check occurs&lt;/li&gt;&lt;li&gt;3G connection with 300ms of latency added to the &lt;a href=&quot;https://hpbn.co/primer-on-latency-and-bandwidth/#last-mile-latency&quot;&gt;last mile&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Our user is located in Vietnam&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now you may be asking &quot;Why Vietnam?&quot;. Well, it&#39;s important to remember that the location of your users has a huge effect on the RTT. If your user is located in a country outside of America, but the OCSP responder is only located in America, that&#39;s a fair bit of latency being added to the RTT. So what does the start of a waterfall look like in this scenario:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;OCSP can be seen in request 1 and it has a big impact on the SSL negotiation time, taking almost 1.5 seconds on its own in this instance. 1 second of that is waiting on the OCSP response.&quot; decoding=&quot;async&quot; height=&quot;323&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/xTG_ZgCKJZ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/xTG_ZgCKJZ-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/xTG_ZgCKJZ-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/xTG_ZgCKJZ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice how the OCSP request isn&#39;t made right away because it needs to get the certificate from the negotiation to know what to check! Before this point the DNS lookup and initial TCP connection need to complete. Both of these stages are also impacted heavily by a large RTT. Once the SSL negotiation starts it takes 1.5 seconds on its own(!), and 67% (1022 ms) of this time is spent waiting for the OCSP response.&lt;/p&gt;&lt;p&gt;It&#39;s hard to know if 1.5 seconds is a long time for a SSL negotiation if we have nothing to compare it with. Thankfully we can use &lt;a href=&quot;https://telemetry.mozilla.org/new-pipeline/dist.html#!cumulative=0&amp;end_date=2020-01-03&amp;include_spill=0&amp;keys=__none__!__none__!__none__&amp;max_channel_version=beta%252F72&amp;measure=CERT_VALIDATION_HTTP_REQUEST_SUCCEEDED_TIME&amp;min_channel_version=beta%252F71&amp;processType=*&amp;product=Firefox&amp;sanitize=1&amp;sort_by_value=0&amp;sort_keys=submissions&amp;start_date=2019-12-02&amp;table=0&amp;trim=1&amp;use_submission_date=0&quot;&gt;Mozilla Telemetry data&lt;/a&gt; to see how long a typical SSL negotiation takes across a large number of requests (from real browser data):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Firefox beta 72 (all connections types)&lt;/li&gt;&lt;li&gt;740 billion measurements&lt;/li&gt;&lt;li&gt;Median time for SSL completion: 177 ms&lt;/li&gt;&lt;li&gt;95th Percentile: 1090 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Although the browsers are different in the comparison (Chrome vs Firefox), the numbers give you an idea of what the usual SSL negotiation time is for many requests. The median, and even the 95th percentile are nowhere near the 1.5 seconds we see above. What&#39;s also interesting is we know from the &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1ZglJ9tYwIXLDz-sPmnXXo18euquWYJcjqyTI5J0bpgU/edit#gid=0&quot;&gt;browser spreadsheet&lt;/a&gt; I compiled earlier in the post, Firefox will OCSP check &lt;a href=&quot;https://www.ssl.com/article/how-do-browsers-handle-revoked-ssl-tls-certificates/#firefox&quot;&gt;all certificates types&lt;/a&gt;. So even accounting for that in the large data set, the results listed aren&#39;t anywhere close to the 1.5 seconds we see in the demonstration.&lt;/p&gt;&lt;h3 id=&quot;ocsp-is-synchronous&quot;&gt;OCSP is synchronous&lt;/h3&gt;&lt;p&gt;Looking closely at the waterfall where and OCSP request is made, you can see that these responses are synchronous and blocking.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An OCSP check must complete before the SSL negotiation is setup. This can be very costly if multiple checks need to happen as in the waterfall shown.&quot; decoding=&quot;async&quot; height=&quot;267&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/bY0azCjDxJ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/bY0azCjDxJ-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/bY0azCjDxJ-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/bY0azCjDxJ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As can be seen in the waterfall above, the OCSP check at request number two must wait for request number 1 to complete. And the whole SSL negotiation has to wait for both checks to complete before it can fully negotiate the encrypted tunnel. Having synchronous and blocking requests at the start of a waterfall doesn&#39;t sound too great for web performance!&lt;/p&gt;&lt;h3 id=&quot;responder-reliability&quot;&gt;Responder reliability&lt;/h3&gt;&lt;p&gt;As you can probably guess, the reliability of the OCSP responder in this whole revocation process is very important. After all, if the response is slow (or it times out) this is going to have a impact on the connection negotiation, the Time to First Byte (TTFB), and pretty much the whole waterfall after it. Again Mozilla Telemetry can give us an indication of what Desktop Firefox 71 beta users are experiencing. These results suggest that over &lt;a href=&quot;https://telemetry.mozilla.org/new-pipeline/dist.html#!cumulative=0&amp;end_date=2019-12-02&amp;include_spill=0&amp;keys=__none__!__none__!__none__&amp;max_channel_version=beta%252F71&amp;measure=CERT_VALIDATION_HTTP_REQUEST_RESULT&amp;min_channel_version=beta%252F68&amp;processType=*&amp;product=Firefox&amp;sanitize=1&amp;sort_by_value=0&amp;sort_keys=submissions&amp;start_date=2019-10-20&amp;table=0&amp;trim=1&amp;use_submission_date=0&quot;&gt;7% of OCSP checks are timing out today&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;And this is what a waterfall looks like if the OCSP check(s) take a long time:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here it takes 22 seconds for the connection to be established to Apple.com from a test agent in Russia.&quot; decoding=&quot;async&quot; height=&quot;227&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/A_AlEDXoLE-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/A_AlEDXoLE-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/A_AlEDXoLE-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/A_AlEDXoLE-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This waterfall is from a WebPageTest user who&#39;d recently added EV certificates to their website. They&#39;d then noticed that this change had caused a delay of up to 22 seconds with agents located in China, Russia, and India. They also note it occurs on a number or websites that require OCSP checks, not just their own. You can read the whole forum post &lt;a href=&quot;https://web.archive.org/web/20201031171823/https://www.webpagetest.org/forums/showthread.php?tid=14075&quot;&gt;here&lt;/a&gt; if you are interested. Upon reading it, it was one of the reasons why I decided to write this very blog post.&lt;/p&gt;&lt;p&gt;There&#39;s a very interesting section related to this in the &lt;a href=&quot;https://cabforum.org/wp-content/uploads/CA-Browser-Forum-EV-Guidelines-v1.7.1.pdf&quot;&gt;EV SSL Certificate Guidelines 1.7.1&lt;/a&gt;. Section 13 states that:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The requirements in Section 4.9 of the Baseline Requirements apply equally to EV Certificates. However, CAs MUST ensure that CRLs for an EV Certificate chain can be &lt;strong&gt;downloaded in no more than three (3) seconds over an analog telephone line under normal network conditions&lt;/strong&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Strangely, there&#39;s next to no mention of OCSP in these guidelines. So I wonder what the minimum response requirements would be for OCSP responders?&lt;/p&gt;&lt;p&gt;So a question you may be asking is what happens if the browser fails to receive an OCSP response? Maybe the connection times out, or the responder is offline, or the request is being blocked (corporate firewall etc)? Since the whole point of the OCSP check is to see if a certificate is still valid and hasn&#39;t been revoked, you may assume that the SSL negotiation will fail and the connection won&#39;t complete. That isn&#39;t the case though. This is where a browser will do what is called a &#39;soft-fail&#39;, which involves the browser trying to check the certificates revocation status and failing to get a response. Instead of killing the connection the browser proceeds with the connection regardless. In doing so the browser is assuming that the certificate hasn&#39;t been compromised. Sounds a little risky doesn&#39;t it? There&#39;s a whole &lt;a href=&quot;https://www.imperialviolet.org/2014/04/19/revchecking.html&quot;&gt;post by Adam Langley&lt;/a&gt; from 2014 all about this particular issue that&#39;s well worth reading if you want to know more.&lt;/p&gt;&lt;h3 id=&quot;third-party-domains-can-require-ocsp-checks-too&quot;&gt;Third party domains can require OCSP checks too&lt;/h3&gt;&lt;p&gt;It isn&#39;t just the origin domain where these checks can happen. Any third party domain you load from may require the browser to perform an OCSP check on their domain too:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see Firefox checking third party domains before these assets can be loaded.&quot; decoding=&quot;async&quot; height=&quot;312&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/PCCSTGztwB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/PCCSTGztwB-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/PCCSTGztwB-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/PCCSTGztwB-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the waterfall above we can see that the browser (Firefox) is making a number of OCSP requests for multiple domains, not just the origin. So it&#39;s always a good idea to load these scripts in an asynchronous fashion, that way they aren&#39;t blocking. You could even use progressive enhancement to allow whatever content to be seen before the JavaScript has loading. Once loaded, this JavaScript then enhances the user experience.&lt;/p&gt;&lt;h2 id=&quot;ev-certificates&quot;&gt;EV certificates&lt;/h2&gt;&lt;p&gt;Lets&#39;s focus on EV certificates, as I feel there are some unique things to consider when it comes to web performance.&lt;/p&gt;&lt;h3 id=&quot;ev-certificates-and-ocsp&quot;&gt;EV Certificates and OCSP&lt;/h3&gt;&lt;p&gt;I&#39;d like to bring your attention to the results from Chrome in the &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1ZglJ9tYwIXLDz-sPmnXXo18euquWYJcjqyTI5J0bpgU/edit#gid=0&quot;&gt;browser spreadsheet&lt;/a&gt;, as you will see EV certs &lt;strong&gt;are always&lt;/strong&gt; OCSP checked when using it, but other certificate types aren&#39;t. Now considering Chrome is the most popular browser on the planet (with around 56% usage &lt;a href=&quot;https://www.w3counter.com/globalstats.php&quot;&gt;according to W3Counter&lt;/a&gt;), that&#39;s a lot of users having to do an OCSP check on sites that use EV certificates. And that&#39;s not even including all the other browsers that require it too. Browsers like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Firefox&lt;/li&gt;&lt;li&gt;Edge (Chromium &amp; EdgeHTML)&lt;/li&gt;&lt;li&gt;Firefox (Android)&lt;/li&gt;&lt;li&gt;Samsung Internet&lt;/li&gt;&lt;li&gt;IE7-11&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We already discussed the performance impact of OCSP revocation checking. So the fact that by choosing a certain type of certificate will force the most popular browser on the planet to encounter these performance issues, we should examine why you&#39;d choose to use one.&lt;/p&gt;&lt;h3 id=&quot;why-use-an-ev-certificate&quot;&gt;Why use an EV certificate?&lt;/h3&gt;&lt;p&gt;So what are the advantages of choosing to use an EV certificate over say, a DV certificate?&lt;/p&gt;&lt;p&gt;One of the driving forces behind their adoption could probably be marketing related. Browsers used to show the name of the website right next to a little green lock in the URL bar:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Images of Safari and Edge by the Browser Lock Museum&quot; decoding=&quot;async&quot; height=&quot;491&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/swlmYGBJeP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/swlmYGBJeP-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/swlmYGBJeP-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/swlmYGBJeP-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;My guess would be that because you have your site name next to the &lt;a href=&quot;https://www.tunetheweb.com/blog/what-does-the-green-padlock-really-mean/&quot;&gt;little green lock&lt;/a&gt;, the site looks &#39;more legitimate&#39;, &#39;more trusted&#39; and &#39;more secure&#39;... maybe? But it isn&#39;t that simple. The name next to the lock may not match the branding of the website, due websites being owned by parent/subsidiary companies or other legal entities. And if that were the case then it just looks strange. To a user it may even look like the site is broken. They have no idea who the parent company is of the site they are visiting (or probably even care). So why does the little green lock reference a company they&#39;ve never heard of? A user reaction could be: &quot;Is this website trying to steal my bank details?&quot;.&lt;/p&gt;&lt;p&gt;The green lock and name in the certificate have been visible in the past, but what about in 2020: Do modern browsers still show the name next to the green lock for EV certificates?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Chrome: Has removed the name from the URL bar &lt;a href=&quot;https://groups.google.com/a/chromium.org/forum/m/#!msg/security-dev/h1bTcoTpfeI/jUTk1z7VAAAJ&quot;&gt;from version 77&lt;/a&gt;, but it&#39;s shown when clicked on. The padlock is no longer green.&lt;/li&gt;&lt;li&gt;Safari: Removed the name from the URL bar in &lt;a href=&quot;https://cabforum.org/2018/06/06/minutes-for-ca-browser-forum-f2f-meeting-44-london-6-7-june-2018/#Apple-Root-Program-Update&quot;&gt;June 2018&lt;/a&gt;, but it&#39;s shown when clicked on. The padlock is still green.&lt;/li&gt;&lt;li&gt;Firefox: Moved the name from the URL bar &lt;a href=&quot;https://blog.mozilla.org/security/2019/10/15/improved-security-and-privacy-indicators-in-firefox-70/&quot;&gt;in version 70&lt;/a&gt;, but it&#39;s shown when clicked on. The padlock is no longer green.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There&#39;s a whole thread from &lt;a href=&quot;https://twitter.com/scott_helme/status/1159978356021968896&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Scott Helme on Twitter&lt;/a&gt; that talks all about these changes. In my opinion the marketing angle of EV certificates was weak before, but now &lt;a href=&quot;https://www.troyhunt.com/extended-validation-certificates-are-dead/&quot;&gt;it&#39;s non existent&lt;/a&gt;. Scott also has a &lt;a href=&quot;https://scotthelme.co.uk/are-ev-certificates-worth-the-paper-theyre-written-on/&quot;&gt;blog post here&lt;/a&gt; discussing many other aspects of the perceived &#39;positives&#39; of EV certificates, which makes for an interesting read.&lt;/p&gt;&lt;h3 id=&quot;how-popular-are-ev-certificates&quot;&gt;How popular are EV Certificates?&lt;/h3&gt;&lt;p&gt;So how many sites actually use EV certificates? &lt;a href=&quot;https://telemetry.mozilla.org/&quot;&gt;Mozilla Telemetry&lt;/a&gt; data gives us some insightful stats about the number of &lt;a href=&quot;https://telemetry.mozilla.org/new-pipeline/dist.html#!cumulative=0&amp;end_date=2020-01-03&amp;include_spill=0&amp;keys=__none__!__none__!__none__&amp;max_channel_version=beta%252F72&amp;measure=CERT_EV_STATUS&amp;min_channel_version=beta%252F71&amp;processType=*&amp;product=Firefox&amp;sanitize=1&amp;sort_by_value=0&amp;sort_keys=submissions&amp;start_date=2019-12-02&amp;table=0&amp;trim=1&amp;use_submission_date=0&quot;&gt;EV certificates that are encountered&lt;/a&gt; in their data set (based on real browser usage):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Firefox desktop, beta 72 (all connection types)&lt;/li&gt;&lt;li&gt;19.2 billion measurements&lt;/li&gt;&lt;li&gt;434M (2.24%) invalid certificates&lt;/li&gt;&lt;li&gt;18.7B (96.51%) DV certificates&lt;/li&gt;&lt;li&gt;242M (1.25%) EV certificates&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;As you can see, EV certificates are pretty rare on the web. This is exactly what &lt;a href=&quot;https://twitter.com/Scott_Helme&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Scott Helme&lt;/a&gt; reported &lt;a href=&quot;https://twitter.com/Scott_Helme/status/886872029344956418&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;back in 2017&lt;/a&gt; some 2.5 years ago. So I&#39;d guess that number may be even lower now? A look at some more of Scott&#39;s stats from February 2019 sees the &lt;a href=&quot;https://scotthelme.co.uk/alexa-top-1-million-analysis-february-2019/#ev-certificates&quot;&gt;same downwards trend&lt;/a&gt; of EV certificate usage. And according to &lt;a href=&quot;https://www.ssllabs.com/ssl-pulse/&quot;&gt;SSL Labs SSL Pulse&lt;/a&gt;, EV certs are only used on 8.3% of the top 150,000 websites from &lt;a href=&quot;https://www.alexa.com/topsites&quot;&gt;Alexa&#39;s&lt;/a&gt; list of the most popular sites in the world.&lt;/p&gt;&lt;p&gt;If you are interested a whole host of WebPageTest research on EV certificates and OCSP stapling, I highly recommend reading &lt;a href=&quot;https://twitter.com/aaronpeters&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Aaron Peters&lt;/a&gt; superb &lt;a href=&quot;https://www.aaronpeters.nl/blog/ev-certificates-make-the-web-slow-and-unreliable/&quot;&gt;blog post &#39;EV Certificates Make The Web Slow and Unreliable&#39;&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;how-to-improve-performance&quot;&gt;How to improve performance?&lt;/h2&gt;&lt;p&gt;So OCSP doesn&#39;t sound great for performance, but what can we do about it? Thankfully there is something called &#39;OCSP stapling&#39; that can alleviate some of the performance pain (but not all of it!).&lt;/p&gt;&lt;h3 id=&quot;ocsp-stapling&quot;&gt;OCSP stapling&lt;/h3&gt;&lt;p&gt;The problem with OCSP requests is every browser of every user must make these requests (assuming the browser does the checks). That sounds like a very wasteful strategy. So what if we got the websites server to periodically check on the OCSP status of its own certificate (which is cryptographically signed so that its validity can be verified). It could then send the response from this check during the TLS handshake to the browser, at the same time as the certificate. In doing so the users browser gets the revocation information it needs during the TLS handshake, so there&#39;s no need for it to make a separate request to the OCSP responder.&lt;/p&gt;&lt;p&gt;What I&#39;ve just described is exactly what OCSP stapling does. You enable it on the server and it will do what I described above. Resulting in better privacy for the user, &lt;a href=&quot;https://www.tunetheweb.com/performance/ocsp-stapling/&quot;&gt;better performance&lt;/a&gt;, and less load on the OCSP responder. Excellent stuff! So what&#39;s the catch?&lt;/p&gt;&lt;p&gt;Unfortunately OCSP stapling doesn&#39;t solve all the problems. When a server staples a response it will only do it to one &#39;level&#39; deep. So you can staple the leaf certificate check (the one for the site), but you can&#39;t staple the full chain. This is a known issue and there&#39;s an &lt;a href=&quot;https://tools.ietf.org/html/rfc6961&quot;&gt;RFC6961 from June 2013: &#39;Multiple Certificate Status Request Extension&#39;&lt;/a&gt; to add this ability to TLS, but this has now been obsoleted by &lt;a href=&quot;https://tools.ietf.org/html/rfc8446#section-4.4.2.1&quot;&gt;RFC8446: &#39;The Transport Layer Security (TLS) Protocol Version 1.3&#39;&lt;/a&gt;, so maybe this will be possible with TLS 1.3? I&#39;m interested to find out if this is possible in 1.3, so if you know please &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;get in touch&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So what does OCSP stapling look like on a WebPageTest waterfall? NOTE: The site the tests were run on uses an EV certificate.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Stapling off&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Two OCSP checks can be seen below. One for the intermediate certificate at request 1 (which contacts the root OCSP responder), the other check is for the sites certificate at request 2 (which contacts the intermediates OCSP responder):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;With stapling off on the server you can see 2 OCSP requests before the TLS negotiation.&quot; decoding=&quot;async&quot; height=&quot;201&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/DmOHuf4w_z-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/DmOHuf4w_z-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/DmOHuf4w_z-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/DmOHuf4w_z-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Stapling on&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;With stapling enabled, one of the checks is removed. The OCSP check made to the intermediate responder for the site certificate has now been &#39;stapled&#39;, and is sent to the browser along with the site certificate during the TLS handshake:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;With stapling on, only one certificate check can be seen. The one to the root responder for the intermediate certificate.&quot; decoding=&quot;async&quot; height=&quot;201&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/MIJiNoMZ5h-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/MIJiNoMZ5h-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/MIJiNoMZ5h-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/MIJiNoMZ5h-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;oscp-stapling-and-ev-certificates-in-chrome&quot;&gt;OSCP stapling and EV certificates in Chrome&lt;/h3&gt;&lt;p&gt;So what does OCSP stapling mean for EV certificates? Unfortunately it doesn&#39;t completely solve the performance issue. Even with OSCP stapling enabled, Chrome is going to run an OSCP check on the intermediate certificate. So you&#39;re going to have a OCSP performance hit with EV certificates no matter what:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;EV certificates always require OCSP checks, even with OCSP stapling enabled. As seen on request 1 here for apple.com.&quot; decoding=&quot;async&quot; height=&quot;175&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/4fRsyKj1Bj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/4fRsyKj1Bj-300.webp 300w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/4fRsyKj1Bj-600.webp 600w, https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/4fRsyKj1Bj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As they say, the proof is in the WebPageTest waterfall chart. Above I have run a test on Apple.com, it has an EV certificate and the server has OCSP stapling enabled (&lt;a href=&quot;https://www.ssllabs.com/ssltest/analyze.html?d=www.apple.com&amp;s=2600%3a1406%3ab000%3a197%3a0%3a0%3a0%3a1aca&quot;&gt;SSLLabs test&lt;/a&gt;). The test is run using Chrome and the OCSP check can still be seen at request number 1. OCSP stapling has stapled the leaf certificate check and sent it along with the leaf certificate during the TLS handshake, but the check on the intermediate certificate to the root OCSP responder is still visible. It is worth noting that commonly used intermediate certificates will be cached between sites, so the impact of this may not be as large as it sounds. But caching of intermediate certificates comes with some &lt;a href=&quot;https://www.bleepingcomputer.com/news/security/firefox-users-fingerprinted-via-cached-intermediate-https-certificates/&quot;&gt;privacy concerns&lt;/a&gt; in Firefox (I&#39;ve not seen any similar reports for Chrome).&lt;/p&gt;&lt;h2 id=&quot;the-future-of-certificate-revocation&quot;&gt;The future of certificate revocation&lt;/h2&gt;&lt;p&gt;From the outside looking in (as I was, and still am, a complete &#39;n00b&#39; to the subject until recently) it comes across to me as incredibly complicated and all a bit of a mess. CRL doesn&#39;t scale well, and native OCSP has been all but abandoned by Chrome (apart from for EV certs). OCSP stapling solves some problems with OCSP, but also comes with its own set of &lt;a href=&quot;https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html&quot;&gt;problems&lt;/a&gt;. So what else is on the horizon?&lt;/p&gt;&lt;p&gt;Well there&#39;s an interesting new technology called CRLite that &lt;a href=&quot;https://blog.mozilla.org/security/2020/01/09/crlite-part-1-all-web-pki-revocations-compressed/&quot;&gt;Mozilla have been testing&lt;/a&gt; in the Firefox Nightly builds since December 2019.&lt;/p&gt;&lt;h3 id=&quot;how-does-crlite-differ-from-other-crl-revocation-technology&quot;&gt;How does CRLite differ from other CRL revocation technology?&lt;/h3&gt;&lt;p&gt;CRLite promises a lot, but how is it different to other CRL revocation technology available in browsers? I&#39;ll try and summarise below:&lt;/p&gt;&lt;h4 id=&quot;crl&quot;&gt;CRL&lt;/h4&gt;&lt;p&gt;As mentioned before, CRL is a list of digital certificates that have been revoked by the issuing CA. The browser downloads a copy of the list which it can then use to check to see if the certificate it has been provided is on the list. A big problem with CRL is the list of serials is large and can be quite a considerable file size over the network. And to make matters worse, if a client doesn&#39;t have a fresh copy of the list, it has to fetch one during the initial connection to the site! This slows down the whole SSL negotiation process.&lt;/p&gt;&lt;h4 id=&quot;onecrl&quot;&gt;OneCRL&lt;/h4&gt;&lt;p&gt;OneCRL is what Firefox currently uses for CRL revocation checking. It&#39;s been using it &lt;a href=&quot;https://blog.mozilla.org/security/2015/03/03/revoking-intermediate-certificates-introducing-onecrl/&quot;&gt;since version 37&lt;/a&gt;, released back in March 2015. In practice it&#39;s a centralised list of revoked certificates that is periodically checked and updated. This list is then pushed out to the browser. To do this, Firefox extends an existing mechanism called &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/QA/Graphics_Blocklisting&quot;&gt;blocklisting&lt;/a&gt;, which is already used to protect users from errant add-ons, plugins and buggy graphics drivers.&lt;/p&gt;&lt;p&gt;OneCRL comes with a major benefit: speed. If a certificate is covered by OneCRL, a live OCSP request isn&#39;t required across the network, so the revocation check can all be done locally. No additional latency is incurred. This is very important for EV certs where a positive OCSP response is required. An important point to note is OneCRL only covers CA intermediate certificates so as to limit the size of the blocklist.&lt;/p&gt;&lt;h4 id=&quot;crlsets&quot;&gt;CRLSets&lt;/h4&gt;&lt;p&gt;CRLSets is the primary method Chrome (and Chromium based browsers) currently use for quickly revoking certificates in emergency situations. A secondary function is to revoke non-emergency certificates. These secondary revocations are found by crawling CRL&#39;s published by CA&#39;s. The method of how the sets are delivered to the browser sound very similar to OneCRL in Firefox. The list is compiled daily and then the delta is pushed to the browser via an auto-update mechanism. &lt;a href=&quot;https://twitter.com/agl__&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Adam Langley&lt;/a&gt; wrote a very interesting post about CRLSets &lt;a href=&quot;https://www.imperialviolet.org/2014/04/29/revocationagain.html&quot;&gt;back in 2014&lt;/a&gt;, which gives some insight into how they work and some of their pitfalls.&lt;/p&gt;&lt;h4 id=&quot;crlite&quot;&gt;CRLite&lt;/h4&gt;&lt;p&gt;There&#39;s a detailed post about how CRLite works &lt;a href=&quot;https://blog.mozilla.org/security/2020/01/09/crlite-part-2-end-to-end-design/&quot;&gt;here&lt;/a&gt;, but as I understand it, the whole revocation list resides inside a users browser and it is updated four times per day. This sounds very much like OneCRL, only with a major difference:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;CRLite has low bandwidth costs: it can represent &lt;strong&gt;all certificates&lt;/strong&gt; with an initial download of 10 MB (less than 1 byte per revocation) followed by daily updates of 580 KB on average.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;That&#39;s an impressive amount of compression! Where OneCRL could only cover intermediate certificates, CRLite should be able to cover them all. If it works, it promises to allow revocation checks when required that replace a network round-trip, with a local lookup for a larger set of revoked certificates. So no web performance overhead like what is seen when establishing connections to an OCSP responder. Sounds promising! I&#39;m interested in seeing how it develops and if it can be standardised and other browser vendors are willing to adopt it too.&lt;/p&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;&lt;p&gt;So we come to the end of a meandering blog post on different types of SSL certificates and the performance impact of OCSP revocation checks. As we&#39;ve seen, these checks don&#39;t come for free. They can have a significant impact on web performance depending on what browser a user is using, and the location they are browsing from. And if a site uses an EV certificate, it is forcing the most popular browser on the planet (Chrome) to open a revocation check, which will have an impact on performance for those users who visit your site using it. It makes you wonder if the &#39;perceived&#39; advantages of using an EV certificate are really worth it? Personally, I don&#39;t see any advantages, only disadvantages. And there are a number of &lt;a href=&quot;https://www.troyhunt.com/extended-validation-certificates-are-dead/&quot;&gt;blog&lt;/a&gt; &lt;a href=&quot;https://www.troyhunt.com/extended-validation-certificates-are-really-really-dead/&quot;&gt;posts&lt;/a&gt; that &lt;a href=&quot;https://scotthelme.co.uk/are-ev-certificates-worth-the-paper-theyre-written-on/&quot;&gt;agree&lt;/a&gt; with &lt;a href=&quot;https://scotthelme.co.uk/gone-for-ever/&quot;&gt;this&lt;/a&gt; &lt;a href=&quot;https://scotthelme.co.uk/extended-validation-not-so-extended/&quot;&gt;stance&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So when you are next considering what type of certificate to purchase for your website, it&#39;s worth weighing up the pro&#39;s and con&#39;s of each, and how your choice could effect your sites performance.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;26/01/20: Initial blog post published. Thanks &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for the (lots of) technical review and feedback.&lt;/li&gt;&lt;li&gt;27/01/20: Small tweaks to some sections and added information about the Authority Information Access (AIA) SSL extension. Thanks &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;30/01/20: Added link to Aaron Peters &lt;a href=&quot;https://www.aaronpeters.nl/blog/ev-certificates-make-the-web-slow-and-unreliable/&quot;&gt;blog post &#39;EV Certificates Make The Web Slow and Unreliable&#39;&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>How to read a WebPageTest Connection View chart</title>
    <link href="https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/" />
    <updated>2019-12-30T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Table of contents&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#intro&quot;&gt;Intro&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#basic-layout&quot;&gt;Basic Layout&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#1---key&quot;&gt;1 - Key&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#2---tcp-connection-list&quot;&gt;2 - TCP connection list&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#3---connection-timeline&quot;&gt;3 - Connection timeline&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#4---cpu-utilisation&quot;&gt;4 - CPU Utilisation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#5---bandwidth-in&quot;&gt;5 - Bandwidth In&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#6---browser-main-thread&quot;&gt;6 - Browser Main Thread&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#7---page-is-interactive&quot;&gt;7 - Page is Interactive&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#connection-negotiation-and-vertical-lines&quot;&gt;Connection negotiation and vertical lines&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#domain-name-system-dns-lookup---turquoise-line-&quot;&gt;Domain Name System (DNS) Lookup - Turquoise line &lt;span style=&quot;width:12px;background-color:#007b84;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#initial-connection---orange-line-&quot;&gt;Initial Connection - Orange line &lt;span style=&quot;width:12px;background-color:#ff7b00;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#ssl-negotiation---magenta-line-&quot;&gt;SSL Negotiation - Magenta line &lt;span style=&quot;width:12px;background-color:#cf25df;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#start-render---green-line-&quot;&gt;Start Render - Green line &lt;span style=&quot;width:12px;background-color:#28bc00;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#dom-content-loaded---pink-line-&quot;&gt;DOM Content Loaded - Pink line &lt;span style=&quot;width:12px;background-color:#d888df;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#on-load---lavender-line-&quot;&gt;On Load - Lavender line &lt;span style=&quot;width:12px;background-color:#c0c0ff;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#document-complete---blue-line-&quot;&gt;Document Complete - Blue line &lt;span style=&quot;width:12px;background-color:#00f;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#horizontal-timings&quot;&gt;Horizontal timings&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#request-1---the-html&quot;&gt;Request 1 - The HTML&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#request-4---static-assets&quot;&gt;Request 4 - Static assets&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#request-5---fonts&quot;&gt;Request 5 - Fonts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#request-10---download-pause&quot;&gt;Request 10 - Download &quot;pause&quot;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#summary&quot;&gt;Summary&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#using-the-waterfall-view-and-connection-view-together&quot;&gt;Using the &#39;Waterfall View&#39; and &#39;Connection View&#39; together&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#common-scenarios&quot;&gt;Common scenarios&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#http2-coalescing&quot;&gt;HTTP/2 Coalescing&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#http11-vs-http2&quot;&gt;HTTP/1.1 vs HTTP/2&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#underutilised-connections&quot;&gt;Underutilised connections&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#cors-anonymous-connections&quot;&gt;CORS &#39;anonymous&#39; connections&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#ocsp&quot;&gt;OCSP&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#third-party-overload&quot;&gt;Third party overload&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;disclaimer&quot;&gt;Disclaimer&lt;/h2&gt;&lt;p&gt;This article is the sister post to my &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;&#39;How to read a WebPageTest waterfall chart&#39;&lt;/a&gt;, so if you are interested in the &#39;Waterfall View&#39; chart, then check out that article too. Note that reading it isn&#39;t mandatory. I have written this post to be fairly independent of the first, and will cross link relevant sections where required. If you do happen to read both, there could be some repeated information (sorry about that).&lt;/p&gt;&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;&lt;p&gt;When you run a test using WebPageTest the Connection View is the waterfall chart directly below the main Waterfall View that everyone knows and loves. It&#39;s often underused, but the Connection View offers insights that are not easily seen in the main Waterfall View so it really shouldn&#39;t be ignored. Once you understand what it is displaying it&#39;s an incredibly powerful tool, allowing you to quickly identify many web performance issues simply by looking at the connections a browser makes to the server. Read on to find out more.&lt;/p&gt;&lt;h2 id=&quot;basic-layout&quot;&gt;Basic Layout&lt;/h2&gt;&lt;p&gt;Here&#39;s the basic layout of the connection view user interface:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The connection view is made up of a number of key areas: 1 - Key, 2 - TCP connection list, 3 - Connection timeline, 4 - CPU Utilisation, 5 - Bandwidth In, 6 - Browser main thread, 7 - Page is Interactive visualisation.&quot; decoding=&quot;async&quot; height=&quot;436&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/xVDbUwK_KL-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/xVDbUwK_KL-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/xVDbUwK_KL-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/xVDbUwK_KL-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;1-key&quot;&gt;1 - Key&lt;/h3&gt;&lt;p&gt;The key shows two types of information:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Information about the connection status (DNS lookup, connection established, SSL negotiation)&lt;/li&gt;&lt;li&gt;The type of resource being requested (e.g. HTML, images etc)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Each resource has 2 colours, a light shade and a dark shade. The light shade signifies the point at which the browser has made the request for the resource. The dark shade is the point at which the resource is actually downloading. See this &lt;a href=&quot;https://web.archive.org/web/20191228065137/https://www.webpagetest.org/forums/showthread.php?tid=14643&amp;pid=28907#pid28907&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. Pat&#39;s response is also quoted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The lighter time is the time from when the browser sends the request out on the wire until the first byte comes back. The darker color is the period where the resource is actually downloading.&lt;/p&gt;&lt;p&gt;Looks like the production site is using HTTP/2 and the dev site is not which is why you see radically different request behavior.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;With http/2 the browser can issue all of the requests to the server in one go and the server will send them back in the order the browser prioritized them (mostly).&lt;/p&gt;&lt;p&gt;with the dev server, the browser can only make 6 requests at a time and has to wait for each to complete before issuing more.&lt;/p&gt;&lt;p&gt;Generally, all else being equal, HTTP/2 will perform better so it is going to be hard to compare your dev work to the production system without having the same serving architecture.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The Connection View doesn&#39;t show request &#39;wait time&#39; or &#39;JS Execution&#39; like the Waterfall View does. It simply isn&#39;t possible to visualise these on this view since asset requests stack horizontally. See &lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#request-4---static-assets&quot;&gt;&#39;Request 4 - Static assets&#39;&lt;/a&gt; for more details on this.&lt;/p&gt;&lt;h3 id=&quot;2-tcp-connection-list&quot;&gt;2 - TCP connection list&lt;/h3&gt;&lt;p&gt;This displays a list of individual TCP connections that the browser negotiated while loading the page, and the order in which they were opened. The first request is usually the request for the page HTML, unless an &lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/#ocsp&quot;&gt;OCSP check&lt;/a&gt; is required. Note the yellow closed padlock icon to the far left of the row. This tells you the TCP connection is secure (HTTPS).&lt;/p&gt;&lt;h3 id=&quot;3-connection-timeline&quot;&gt;3 - Connection timeline&lt;/h3&gt;&lt;p&gt;The timeline shows time along the x-axis (horizontal) vs the individual TCP connection on the y-axis (vertical). Each row shows a TCP connection being opened (DNS + Connect + SSL), and then the individual asset requests that were downloaded using this TCP connection. By examining the key (1), it is possible to easily identify the type of file that was downloaded on the connection (the multi-coloured banding).&lt;/p&gt;&lt;h3 id=&quot;4-cpu-utilisation&quot;&gt;4 - CPU Utilisation&lt;/h3&gt;&lt;p&gt;A simple graph showing the CPU utilisation of the browser process running on the device. It displays how much CPU the current webpage is using at any point in time. It ranges from 0 - 100% utilisation. See this &lt;a href=&quot;https://web.archive.org/web/20100711140312/https://www.webpagetest.org/forums/showthread.php?tid=256&amp;pid=1068#pid1068&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. The quote of what Pat said is below too:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The CPU utilization line is the CPU utilization of just the browser process that is running the test (same as you would see in task manager) on a fixed scale from 0-100% (sampled in 0.1 sec intervals).&lt;/p&gt;&lt;p&gt;There are lots of things that cause the CPU utilization to spike but javascript is a very common contributor (particularly if there are gaps in the waterfall where the CPU is pegged at 100% - telltale sine of javascript that needs optimizing). The CPU will also get exercised as the browser figures out how to layout the page and apply styles and finally, the testing itself can also introduce some spikes (really short spikes when the screen shots are taken for example). If you capture a video then the impact of recording the screen shots will be higher.&lt;/p&gt;&lt;p&gt;Usually if I see something that looks suspicious I&#39;ll load it up into Dynatrace Ajax Edition (great free profiler for IE) and run the page through to see if anything jumps out. I&#39;d love to integrate the capture of the profiling data at some point in the future.&lt;/p&gt;&lt;p&gt;Even though it wouldn&#39;t really impact the numbers, right now only one test is ever executing on a given machine at a time. We can (and have) run configurations with multiple tests at the same time but when video capture was introduced it killed our ability to run in parallel. I&#39;m experimenting with running the tests in VM&#39;s to see if I can get more testing capacity out of the hardware because the machines are pretty beefy and could easily handle 8+ tests in parallel but I won&#39;t do it if it introduces any variability into the results.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;5-bandwidth-in&quot;&gt;5 - Bandwidth In&lt;/h3&gt;&lt;p&gt;This is an indicator of when data is coming into the browser. The visualisation is helpful to see when the browser is doing useful work vs wasted time. The absolute scale can be ignored, as it isn&#39;t very accurate. Use the &#39;Capture network packet trace (tcpdump)&#39; option in the advanced tab on the WebPageTest homepage if you want more accurate results. You will also need to enable this feature if you want to &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/issues/1320#issuecomment-568777858&quot;&gt;see the chart on mobile devices&lt;/a&gt;. See this &lt;a href=&quot;https://web.archive.org/web/20191223160902/https://www.webpagetest.org/forums/showthread.php?tid=266&amp;pid=1168#pid1168&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. I&#39;ve quoted what Pat said below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The bandwidth graph is more just an indicator of when data is coming in. I&#39;ve debated removing it but it is helpful to see when the page is actually downloading stuff (useful work) and when it&#39;s not (wasted time). This can help a fair bit when you flush the document early to see the initial chunk of data coming in and to make sure that data continues to come in from then on.&lt;/p&gt;&lt;p&gt;Don&#39;t pay too much attention to the absolute scale because the raw measurements will not be very accurate (just by definition in how things are measured from the client side).&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;6-browser-main-thread&quot;&gt;6 - Browser Main Thread&lt;/h3&gt;&lt;p&gt;This graph visualises what the browsers main thread is doing at any specific point in time (x-axis). The y-axis shows the percentage from 0 - 100%. The colours were copied from the Chrome DevTools CPU graph (under the performance tab). Here&#39;s what each of the colours mean:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Orange - Script parsing, evaluation and execution &lt;span style=&quot;width:6px;background-color:#e5c35c;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;li&gt;Purple - Layout &lt;span style=&quot;width:6px;background-color:#927de2;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;li&gt;Green - Painting &lt;span style=&quot;width:6px;background-color:#87b268;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;li&gt;Blue - HTML parsing &lt;span style=&quot;width:6px;background-color:#86a5e1;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Using this graph it is possible to see if the CPU is becoming a bottleneck in one of the above areas.&lt;/p&gt;&lt;h3 id=&quot;7-page-is-interactive&quot;&gt;7 - Page is Interactive&lt;/h3&gt;&lt;p&gt;This graph gives you an indication of when the main thread is blocked. The red blocks indicate that the main thread has been blocked for 100ms (which will also block inputs like button presses). Green indicates the thread isn&#39;t blocked. &lt;strong&gt;Note:&lt;/strong&gt; it still may be possible to scroll during red blocked phases, as scrolling is usually handled off the main thread for most browsers. See this &lt;a href=&quot;https://web.archive.org/web/20191230204442/https://www.webpagetest.org/forums/showthread.php?tid=15235&amp;pid=30556#pid30556&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. I&#39;ve quoted Pats reply below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Scrolling will generally be fine since it is usually handled off-thread for most browsers as long as there are no active scroll events attached. Clicking should be delayed but not necessarily for the full duration of the red (though the first one looks like a single executing JS function so it would be for the full duration of that block).&lt;/p&gt;&lt;p&gt;The red blocks are any point where the main thread is blocked (including from input) for 100ms. A lot of separate 100ms events can make it look like a long block or a single really long event can also show up (which could be a way worse experience).&lt;/p&gt;&lt;p&gt;How did you measure being able to click on a link during one of the red blocks? You&#39;d have to have a test machine handy with a similar CPU and connectivity and then try to click in the UI immediately after it starts to render.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2 id=&quot;connection-negotiation-and-vertical-lines&quot;&gt;Connection negotiation and vertical lines&lt;/h2&gt;&lt;p&gt;Just under the Connection View header, you can see a key with each stage in a connection negotiation and the coloured vertical lines (example below):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Connection view key consists of: DNS Lookup, Initial Connection, SSL Negotiation, Start Render, RUM First Paint, DOM Content Loaded, On Load, Document complete.&quot; decoding=&quot;async&quot; height=&quot;53&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/mRPTD80G7s-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/mRPTD80G7s-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/mRPTD80G7s-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/mRPTD80G7s-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;But what do each of them mean?&lt;/p&gt;&lt;h3 id=&quot;domain-name-system-dns-lookup-turquoise-line&quot;&gt;Domain Name System (DNS) Lookup - Turquoise line &lt;span style=&quot;width:12px;background-color:#007b84;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The browser wants to convert a domain name into a server IP address. It first checks its cache for the DNS record, if not found it will contact a DNS server for the correct IP address. The turquoise line you see is the time taken for this request to happen.&lt;/p&gt;&lt;h3 id=&quot;initial-connection-orange-line&quot;&gt;Initial Connection - Orange line &lt;span style=&quot;width:12px;background-color:#ff7b00;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This is where the browser negotiates a TCP connection with the server (located at the IP address found via the DNS lookup above). There&#39;s a whole lot more than that goes on behind the scenes that I won&#39;t cover here. Check out the &lt;a href=&quot;https://hpbn.co/building-blocks-of-tcp/&quot;&gt;&#39;Building Blocks of TCP&#39;&lt;/a&gt; chapter in &lt;a href=&quot;https://hpbn.co/&quot;&gt;&#39;High Performance Browser Networking (HPBN)&#39;&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/igrigorik&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Ilya Grigorik&lt;/a&gt; if you want to know (lots) more.&lt;/p&gt;&lt;h3 id=&quot;ssl-negotiation-magenta-line&quot;&gt;SSL Negotiation - Magenta line &lt;span style=&quot;width:12px;background-color:#cf25df;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;If the website is using a secure connection (HTTPS), the final stage of the connection negotiation is the TLS handshake. The browser and server agree on the TLS protocol version, the ciphersuite to use and verify certificates. Only once this encrypted tunnel is negotiated can the browser and server start to exchange data related to the web page itself. Again, if you wish to know more check out the chapter &lt;a href=&quot;https://hpbn.co/transport-layer-security-tls/&quot;&gt;&#39;Transport Layer Security (TLS)&#39;&lt;/a&gt; in HPBN.&lt;/p&gt;&lt;h3 id=&quot;start-render-green-line&quot;&gt;Start Render - Green line &lt;span style=&quot;width:12px;background-color:#28bc00;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This is the first point at which a user will see pixels painted to the screen. The pixels could be from anything at all (background colours, borders etc), not necessarily content. Before this point the screen was blank. This metric is measured by analysing individual video frames that are captured during the page load. See &lt;a href=&quot;https://web.archive.org/web/20191223051303/https://www.webpagetest.org/forums/showthread.php?tid=13265&amp;pid=24577#pid24577&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. I&#39;ve quoted Pat&#39;s reply below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Start render is measured by capturing video of the page load and looking at each frame for the first time the browser displays something other than a blank page. It can only be measured in a lab and is generally the most accurate measurement for it.&lt;/p&gt;&lt;p&gt;msFirstPaint is a measurement reported by the browser itself about when it thinks it painted the first content. It is available from javascript and can be reported from the field. It is usually pretty accurate but sometimes it reports the time when the browser painted nothing but a blank screen.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;dom-content-loaded-pink-line&quot;&gt;DOM Content Loaded - Pink line &lt;span style=&quot;width:12px;background-color:#d888df;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The point at which the HTML has been loaded and parsed, and the browser has reached the end of the document. All blocking scripts have been loaded and run. The DOM at this point is completely defined. See &lt;a href=&quot;https://web.archive.org/web/20191225070452/https://www.webpagetest.org/forums/showthread.php?tid=12846&amp;pid=29909&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. I&#39;ve quoted what Pat replied below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Usually Visually Complete WILL happen before Document Complete. Document Complete is the onload event which fires when all of the scripts, css and images defined in the HTML have finished loading (including below-the-fold content).&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;domContentLoaded is a technical event the browser fires when it has finished parsing the HTML and reached the end of the document (which means it has loaded and run run all blocking scripts as well). At that point the &quot;DOM&quot; (all of the elements in the document) is completely defined. The event shows up as a vertical pink bar in the waterfall.&lt;/p&gt;&lt;p&gt;There is a lot of non-blocking content that will still need to load that can radically impact the visual experience though (images and fonts in particular).&lt;/p&gt;&lt;p&gt;Visually Complete is measured from a video capture of the viewport loading and is the point when the visible part of the page first reached 100% &quot;completeness&quot; compared to the end state of the test.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;Visually Complete can happen before domContentLoaded when the browser is busy parsing and executing things that aren&#39;t visible at the top of the page. A likely cause is blocking scripts at the end of the document. They don&#39;t visually impact what the user sees in the viewport but the parser can&#39;t reach the end of the document and fire DCL until it finishes downloading and running them.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;on-load-lavender-line&quot;&gt;On Load - Lavender line &lt;span style=&quot;width:12px;background-color:#c0c0ff;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The point at which the window load event fires. All objects are in the DOM and all images and scripts have finished loading.&lt;/p&gt;&lt;h3 id=&quot;document-complete-blue-line&quot;&gt;Document Complete - Blue line &lt;span style=&quot;width:12px;background-color:#00f;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The point where the &lt;code&gt;onload&lt;/code&gt; event has fired and all the static image content has loaded. Content changes that are triggered by JavaScript execution may not be included.&lt;/p&gt;&lt;h2 id=&quot;horizontal-timings&quot;&gt;Horizontal timings&lt;/h2&gt;&lt;p&gt;Let&#39;s now take a look at the panel with all the interesting details, the Connection timeline. As mentioned earlier, it looks quite similar to the Waterfall View, only in this view each row is a TCP connection. The coloured bands you see (sometimes multiple types), are individual assets being downloaded in succession.&lt;/p&gt;&lt;p&gt;Lets now examine a few of the TCP connections from &lt;a href=&quot;https://www.webpagetest.org/result/191104_P4_271636e67cd95db8e230fa83d29048a9/1/details/#waterfall_view_step1&quot;&gt;this connection view&lt;/a&gt; to explain what is actually happening.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: When looking at the Connection View it is worth keeping in mind that it doesn&#39;t give an indication of the number of bytes being downloaded over a specific time period. So it may look like two connections are doing the same amount of work. For example, one connection could be downloading 10MB in 1 seconds, and another 1KB in 1 second and it will look like they are doing the same amount of work. This is one of the limitations of the connection view.&lt;/p&gt;&lt;h3 id=&quot;request-1-the-html&quot;&gt;Request 1 - The HTML&lt;/h3&gt;&lt;p&gt;Request one is a simple one. It looks exactly the same as it would in the Waterfall View. It gives you a visualisation of the time taken to negotiate the connection and how long from the HTML request to when it completed. The whole row is dedicated to that single origin domain, so if there were more assets downloaded from it they would show on the same row.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Request 1 is a simple connection to the pages HTML, with information about connection setup time and the time it took to download.&quot; decoding=&quot;async&quot; height=&quot;406&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/GmBmpHlQCs-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/GmBmpHlQCs-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/GmBmpHlQCs-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/GmBmpHlQCs-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice how after the first &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#download-chunks&quot;&gt;download chunk&lt;/a&gt; of HTML (the first dark band), this triggers HTML parsing on the browsers main thread. This is an example of &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#html-early-flush&quot;&gt;HTML early flush&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The reason the other requests are blurred is purely so it&#39;s easier for the reader to focus on a single request, not because it contains sensitive information. Unfortunately there is no way to filter and show only selected requests when you &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-generate-customised-waterfall-chart-images&quot;&gt;customise&lt;/a&gt; the Connection View waterfall chart. Possible feature request &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;? :)&lt;/p&gt;&lt;h3 id=&quot;request-4-static-assets&quot;&gt;Request 4 - Static assets&lt;/h3&gt;&lt;p&gt;Request 4 triggers another TCP connection negotiation since the static assets aren&#39;t on the origin (it therefore gets a dedicated row). We also see the first example of multiple asset types being downloaded on the connection, and the vertical coloured banding this creates. Examining the key we can see that CSS is being downloaded, as well as JavaScript (both requested and downloaded).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Request 4 gives an example of how the connection view visualises multiple asset downloads on a single connection, where different coloured bands are shown corresponding to the type of files downloaded.&quot; decoding=&quot;async&quot; height=&quot;406&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/slusPvXrBR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/slusPvXrBR-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/slusPvXrBR-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/slusPvXrBR-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice how even though the JavaScript is downloaded, actual JS Execution isn&#39;t visualised on the connection view like it is on the &lt;a href=&quot;https://nooshu.github.io/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-7---a-third-party-javascript-file&quot;&gt;waterfall view&lt;/a&gt;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the waterfall view you can see the JS execution a slim, pink lines after the download has been completed.&quot; decoding=&quot;async&quot; height=&quot;245&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/qQsIcIG4d0-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/qQsIcIG4d0-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/qQsIcIG4d0-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/qQsIcIG4d0-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You can verify the execution is actually taking place in the Connection View by looking at the Browser Main Thread graph at the bottom of the UI (yellow spikes). Visualisation of the JS execution isn&#39;t possible on the Connection View due to the way assets can overlap when being downloaded. It is therefore better to refer to the Waterfall View when examining JS Execution.&lt;/p&gt;&lt;h3 id=&quot;request-5-fonts&quot;&gt;Request 5 - Fonts&lt;/h3&gt;&lt;p&gt;For HTTP/1.1 you could have 6 &lt;code&gt;credentialed&lt;/code&gt; connections and 6 &lt;code&gt;anonymous&lt;/code&gt; connections to the same domain, and with HTTP/2 you may typically see 1 or 2 connections. Here&#39;s where the connection view is a really powerful tool: visualising multiple TCP connections to the same domain. Looking at this from the Waterfall View you may assume that all assets coming from the same domain use the same TCP connection. With the Connection View it is easy to see that isn&#39;t the case:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Fonts require a very specific CORS setup, where by they need an &#39;anonymous mode&#39; connection to download. This can clearly be seen in the connection view as there are two TCP connections to the sae domain. One credentialed (for other assets), another &#39;anonymous&#39; (only for fonts).&quot; decoding=&quot;async&quot; height=&quot;406&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IU1cf4NIVk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IU1cf4NIVk-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IU1cf4NIVk-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IU1cf4NIVk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You may be wondering why there are two connections to the same domain when one of them isn&#39;t being used, but this is due to how the fetch specifications work. Same origin requests can still redirect to a cross-origin one, which would result in different requests depending on the credentials mode. So whenever you &lt;code&gt;preload&lt;/code&gt; a web font, &lt;strong&gt;always&lt;/strong&gt; include the &lt;code&gt;crossorigin&lt;/code&gt; attribute (even when serving the font from the same domain). If you don&#39;t the font will be downloaded twice (once on a &lt;code&gt;credentialed&lt;/code&gt; connection that can&#39;t be used, then again on an &lt;code&gt;anonymous&lt;/code&gt; connection to be used).&lt;/p&gt;&lt;p&gt;Request 4 downloads the static assets (as seen in the previous section), request 5 is a TCP connection dedicated to &lt;code&gt;anonymous&lt;/code&gt; mode assets (in this case only the fonts). Notice how request 5 doesn&#39;t require a DNS lookup, since this already happened in request 4. It&#39;s also very interesting to see the difference in SSL negotiation time to the same domain. This &lt;em&gt;could&lt;/em&gt; be due to bandwidth limitations on the device now that other TCP connections are using it. But you&#39;d usually expect the SSL time to be shorter due to &lt;a href=&quot;https://blog.cloudflare.com/tls-session-resumption-full-speed-and-secure/&quot;&gt;TLS session resumption&lt;/a&gt; when re-establishing a connection shortly after another one.&lt;/p&gt;&lt;h3 id=&quot;request-10-download-pause&quot;&gt;Request 10 - Download &quot;pause&quot;&lt;/h3&gt;&lt;p&gt;Here&#39;s another connection opened to download JavaScript assets, but what is interesting about request 10 is the fact that there is a gap between the horizontal blocks:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Initial JavaScript file downloads, the browser then no longer receives data on it, until around 300 ms later when the browser receives another JavaScript file.&quot; decoding=&quot;async&quot; height=&quot;406&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/io8-e1kp2A-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/io8-e1kp2A-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/io8-e1kp2A-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/io8-e1kp2A-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In fact, this is because two separate JavaScript files are being downloaded. Now, this could be due to the way these asset requests are being scheduled for download. The browser simply requests one first, then at a later time (perhaps after executing this first JavaScript?) it makes another request for some more JavaScript across the same connection.&lt;/p&gt;&lt;p&gt;But there&#39;s something even more interesting happening here that may not be obvious. Here is an example of &lt;a href=&quot;https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/&quot;&gt;HTTP/2 connection coalescing&lt;/a&gt; in action. The following domains share similar properties like certificate, IP address etc. Under these conditions the browser realises it doesn&#39;t need to open a new TCP connection, it can simply use the one already open. So two subdomains are now associated with a single TCP connection. In this case:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;az725175.vo.msecnd.net&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;az416426.vo.msecnd.net&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&#39;s difficult to see this coalescing on a standard Waterfall View, but on the Connection View it&#39;s much more obvious. See &#39;HTTP/2 Coalescing&#39; scenario later in the post.&lt;/p&gt;&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;&lt;p&gt;And here you can see the Connection View we&#39;ve been examining in all its glory:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The whole connection view with all 15 rows visible. 3 of the 15 rows are &#39;anonymous&#39; used only for fonts.&quot; decoding=&quot;async&quot; height=&quot;286&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/SJReb0gQOG-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/SJReb0gQOG-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/SJReb0gQOG-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/SJReb0gQOG-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see there are 15 rows, meaning there are 15 TCP connections created in the loading of this website. Each of these connections had some form of DNS, TCP, SSL negotiation involved, which is expensive in terms of time and can affect web performance. The Connection View is a great way to see very quickly how many connections there are and how each of them is being used. Using it in conjunction with the Waterfall View will allow you to identify potential performance issues quickly, that using the Waterfall View on its own would be difficult to see.&lt;/p&gt;&lt;h2 id=&quot;using-the-waterfall-view-and-connection-view-together&quot;&gt;Using the &#39;Waterfall View&#39; and &#39;Connection View&#39; together&lt;/h2&gt;&lt;p&gt;One point that is worth talking about is how each view relates to each other. It may look like they are two separate views that are unrelated, but that&#39;s not true:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The two views are on the same timeline, so you can scroll between them to identify information about both requests and connections very quickly.&quot; decoding=&quot;async&quot; height=&quot;434&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZO72H9awqx-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZO72H9awqx-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZO72H9awqx-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZO72H9awqx-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;All three lines identified in screenshot all line up perfectly. This isn&#39;t just a lucky coincidence, this is intentional. Both views are using the same timescale, therefore if you see a request in the Waterfall View that you&#39;d like to know about the connection information, simply scroll down the page and you will find it in the same place on the Connection View. The Connection View adds a whole new dimension to the traditional WebPageTest waterfall chart.&lt;/p&gt;&lt;h2 id=&quot;common-scenarios&quot;&gt;Common scenarios&lt;/h2&gt;&lt;p&gt;Here&#39;s a list of common patterns seen in the WebPageTest Connection View. I&#39;ll add more of these over time as I encounter them.&lt;/p&gt;&lt;h3 id=&quot;http-2-coalescing&quot;&gt;HTTP/2 Coalescing&lt;/h3&gt;&lt;p&gt;Under HTTP/1.1 a browser would typically open six TCP connections to a single domain, thus allowing six assets to be downloaded from that domain at any one time. HTTP/2 changed this by adding &lt;a href=&quot;https://stackoverflow.com/a/36519379&quot;&gt;multiplex streaming&lt;/a&gt; of files over a single TCP connection. &lt;a href=&quot;https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/&quot;&gt;HTTP/2 Coalescing&lt;/a&gt; is quite a complex topic, but a quick TL;DR is: if a domain and sub-domain share similar properties, the browser does not need to open a second connection; it may reuse the first. All content from coalesced domains can be transferred over the single shared connection. It&#39;s difficult to spot this coalescing happening on the Waterfall View, but in the Connection View it is relatively simple to spot:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the connection view we see en.wikipedia.org and login.wikimedia.org being coalesced over a single TCP connection.&quot; decoding=&quot;async&quot; height=&quot;186&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/fyQS3Fvyec-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/fyQS3Fvyec-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/fyQS3Fvyec-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/fyQS3Fvyec-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This is a really simple example of coalescing. Here &lt;code&gt;en.wikipedia.org&lt;/code&gt;, &lt;code&gt;login.wikimedia.org&lt;/code&gt; is coalescing and using the same TCP connection (connection 1). Due to the use of ellipses for long domain strings, when this occurs it can be quite difficult to tell exactly what domains are being coalesced. But if you do happen to see &#39;...&#39; in the domain listings, there&#39;s a chance coalescing could be occurring.&lt;/p&gt;&lt;p&gt;I&#39;d love to see an additional icon next to the SSL lock icon to identify when coalescing is occurring in the Connection View, just to make it really obvious... just an idea if you happen to be reading &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat&lt;/a&gt; :)&lt;/p&gt;&lt;h3 id=&quot;http-1-1-vs-http-2&quot;&gt;HTTP/1.1 vs HTTP/2&lt;/h3&gt;&lt;p&gt;This isn&#39;t a straight forward scenario to answer, as it completely relies on how the website has been setup in terms of sub-domains and third parties. I can&#39;t cover all eventualities but I can hopefully give you a general idea of the difference you see on the connection graph. Lets run a test on the same domain, once with HTTP/2, the other with HTTP/1.1 (via the &lt;code&gt;--disable-http2&lt;/code&gt; Chrome flag). For this example I&#39;ve chosen to examine the &lt;a href=&quot;https://www.bbc.co.uk/news&quot;&gt;BBC News&lt;/a&gt; website using both protocols:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;HTTP/1.1&lt;/strong&gt; &lt;img alt=&quot;In the HTTP/1.1 waterfall you can clearly see the number of TCP connections that need to be opened to download the required assets. There are a number that require 6 connections to the same domain.&quot; decoding=&quot;async&quot; height=&quot;599&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IRukXC4dpu-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IRukXC4dpu-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IRukXC4dpu-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/IRukXC4dpu-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This is a fairly busy connection chart, with a total of 29 TCP connections required to load the page. The &lt;code&gt;dns-prefetch&lt;/code&gt; and &lt;code&gt;preconnect&lt;/code&gt; that are in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag can be seen on connection 8 and 13. You can also see the browser creating 12 connections to just two domains (&lt;code&gt;nav.files.bbci.co.uk&lt;/code&gt;, &lt;code&gt;ichef.bbci.co.uk&lt;/code&gt;, 6 each, 2-7 &amp; 13-18). This is an example of HTTP/1.1 domain sharding in action. With TCP connections 22 &amp; 23 you can see an example of &lt;code&gt;credentialed&lt;/code&gt; vs &lt;code&gt;non-credentialed&lt;/code&gt; TCP connections. Connections 1 - 12 show some connection reuse, but not much. I&#39;ll talk about this more in the &#39;Underutilised connections&#39; section.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;HTTP/2&lt;/strong&gt; &lt;img alt=&quot;The HTTP/2 waterfall looks a lot less busy, with only 11 TCP connections and examples of HTTP/2 connection coalescing and multiplex streaming over multiple single connections.&quot; decoding=&quot;async&quot; height=&quot;350&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/zgyPq9VpBb-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/zgyPq9VpBb-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/zgyPq9VpBb-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/zgyPq9VpBb-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Compared to the HTTP/1.1 connection graph, HTTP/2 is a lot simpler. There are only 11 TCP connections compared to 29, so where have these connections gone? A large number have been removed because the browser is no longer opening 12 connections for only two domains (&lt;code&gt;nav.files.bbci.co.uk&lt;/code&gt;, &lt;code&gt;ichef.bbci.co.uk&lt;/code&gt;). The other reason for the reduction of connections is due to HTTP/2 connection coalescing, as discussed earlier. The BBC News website is actually doing this really efficiently, with 5 domains being coalesced in request 2:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;m.files.bbci.co.uk&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;emp.bbc.co.uk&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;ichef.bbci.co.uk&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;static.bbci.co.uk&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;idcta.api.bbc.co.uk&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;As you can see in request 2, because of coalescing this single TCP connection is doing a huge chunk of work. It is downloading JavaScript, CSS, images, and JSON (other). The same can be seen in request number 3, another busy TCP connection that is streaming multiple assets. While we&#39;re on the topic, I&#39;m curious to find out why &lt;code&gt;nav.files.bbci.co.uk&lt;/code&gt; isn&#39;t actually being coalesced into request 2. It is coming from the same IP address, but maybe the certificate is different. Maybe an engineer from the BBC can investigate and let me know.&lt;/p&gt;&lt;p&gt;As you can see with HTTP/2, if setup correctly it can reduce the number of TCP connections quite dramatically, thus allowing multiple assets to be streamed over a single connection. This allows for better prioritisation and more efficient use of the bandwidth available. This is especially important considering HTTP/2 is built upon TCP, and these are subject to &lt;a href=&quot;https://blog.stackpath.com/tcp-slow-start/&quot;&gt;TCP slow start&lt;/a&gt;. So the more a single TCP connection is used, the quicker it will become (assuming a stable connection with minimal congestion).&lt;/p&gt;&lt;h3 id=&quot;underutilised-connections&quot;&gt;Underutilised connections&lt;/h3&gt;&lt;p&gt;A really great use of the Connection View is to identify how connections that the browser has opened are being utilised. After all, it takes time and resources to open a TCP connection, so you want to make sure each is being used efficiently. So let&#39;s turn our attention to a site I know quite well, GOV.UK, and see if we are using our TCP connections efficiently (tested on Chrome - 3G - HTTP/1.1):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The connection view can tell you a lot about how your connections are being utilised. Focus on each one in turn and see how much of the row is empty. This will show you the wasted time on each connection.&quot; decoding=&quot;async&quot; height=&quot;298&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/J-bGC-HLFL-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/J-bGC-HLFL-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/J-bGC-HLFL-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/J-bGC-HLFL-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Looking at each connection (row) you can see how each is being utilised. A row filled with colour is being utilised a lot more than one that is empty.&lt;/p&gt;&lt;p&gt;Request number 1 for example it only being used for the initial HTML download, it is never used again. Request 2-5 and 8 are CORS &lt;code&gt;crossorigin=&#39;anonymous&#39;&lt;/code&gt; connections, so are only used for assets that require no credentials. Six of these are created for CSS, JS, and fonts, but once all these assets are downloaded they aren&#39;t used again. Request 6,7, 9-12 are &lt;code&gt;credentialed&lt;/code&gt; connections on which all the images are downloaded. Here we see the reverse of &lt;code&gt;crossorigin=&#39;anonymous&#39;&lt;/code&gt; connections above. Request 6,7, 9-12 are opened late (wasted time at the start of the waterfall), then they are heavily utilised later.&lt;/p&gt;&lt;p&gt;With HTTP/1.1 we want to be aiming for a connection graph that fully utilises all open connections across the whole width. For example the Apple website does a good job of utilising all connections under HTTP/1.1:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Apple.com has 6 connections to the origin, and they are all utilised really well.&quot; decoding=&quot;async&quot; height=&quot;323&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/cjQhvcX0c4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/cjQhvcX0c4-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/cjQhvcX0c4-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/cjQhvcX0c4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we see six TCP connections to the origin, all of which are heavily utilised by the browser (Chrome on 4G). There&#39;s no need for an additional domain shard in this instance (and therefore another 6 TCP connections), as the first 6 are already maxing out the bandwidth available for a large percentage of the total page load time.&lt;/p&gt;&lt;h3 id=&quot;cors-anonymous-connections&quot;&gt;CORS &#39;anonymous&#39; connections&lt;/h3&gt;&lt;p&gt;I&#39;ve mentioned &lt;code&gt;anonymous&lt;/code&gt; (&lt;code&gt;non-credentialed&lt;/code&gt;) connections in this post earlier but haven&#39;t really explained what that actually means. The value of &lt;code&gt;anonymous&lt;/code&gt; means that there will be no exchange of user credentials via cookies, client-side SSL certificates or HTTP authentication across the connection, unless on the same origin. You can find much more information about this in the &lt;a href=&quot;https://www.w3.org/TR/cors/&quot;&gt;Cross-Origin Resource Sharing (CORS) specification&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;CORS is particularly important when you come to using web fonts on your website, since the &lt;a href=&quot;https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements&quot;&gt;font-face specification&lt;/a&gt; requires that fonts &lt;strong&gt;served from a different domain than the origin&lt;/strong&gt; are loaded in &quot;anonymous mode&quot;. It is easy to spot &lt;code&gt;non-credentialed&lt;/code&gt; connections on the Connection View since many websites may only use them for font loading (but &lt;a href=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/&quot;&gt;not always&lt;/a&gt;!). So if you see a font loading from a different domain that is on a separate TCP connection, it will be &lt;code&gt;crossorigin=&#39;anonymous&#39;&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the connection view we can see 3 examples of separate TCP connections being made only for fonts since they aren&#39;t on the origin domain (www.microsoftedgeinsider.com).&quot; decoding=&quot;async&quot; height=&quot;286&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZbISvEsDR--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZbISvEsDR--300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZbISvEsDR--600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ZbISvEsDR--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the Connection View example above we can see three TCP connections (requests 5, 7, 9) setup purely to download the fonts the page requires. We can confirm that the browser has already seen these domains since there&#39;s no DNS lookup needed for these connections (turquoise line). If you&#39;d like to confirm that these connections are indeed using CORS, simply scroll up and find the same request in the Waterfall View. Clicking on the request, under the &#39;Request&#39; tab you will see either &lt;code&gt;sec-fetch-mode: no-cors&lt;/code&gt; for non-CORS connections, or &lt;code&gt;sec-fetch-mode: cors&lt;/code&gt; for CORS connections.&lt;/p&gt;&lt;h3 id=&quot;ocsp&quot;&gt;OCSP&lt;/h3&gt;&lt;p&gt;Online Certificate Status Protocol (OCSP) is an internet protocol used for obtaining the revocation status of a digital certificate. In some situations a browser will send a request to an OCSP responder to see if a sites certificate has been revoked. Chrome doesn&#39;t do it for Domain Validation (DV) and Organisation Validation (OV) certificates, but it does do it for Extended Validation (EV) certificates. Where as Internet Explorer does it for both.&lt;/p&gt;&lt;p&gt;This can quite clearly be seen in the Connection View as it will be the very first connection in the chart (even before the HTML request):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the connection waterfall there are 2 OCSP checks, 1 to the origin, the other to Google Analytics.&quot; decoding=&quot;async&quot; height=&quot;214&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/TDZR4P54-F-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/TDZR4P54-F-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/TDZR4P54-F-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/TDZR4P54-F-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There are two OCSP check in the above waterfall, one for the origin, the other for Google Analytics. Look at the effect this OCSP check has on the SSL negotiation time, as the SSL negotiation cannot complete until it receives a response from the OCSP responder. If you happen to use an &lt;a href=&quot;https://en.wikipedia.org/wiki/Extended_Validation_Certificate&quot;&gt;EV&lt;/a&gt; certificate on your site, you should consider the web performance impact of this delay (especially on mobile connections). There&#39;s a lot to OCSP (including stapling) and I&#39;ve written a little more about it &lt;a href=&quot;https://calendar.perfplanet.com/2019/reading-a-webpagetest-waterfall-chart/#ocsp_validation&quot;&gt;here&lt;/a&gt; and &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#ocsp&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;third-party-overload&quot;&gt;Third party overload&lt;/h3&gt;&lt;p&gt;Another great use of the Connection View is to see how many third party requests a site is making. When you break a site load down by the number of connections, third parties have a huge impact on the number of connections required. For example, let&#39;s take a look at &lt;a href=&quot;https://www.theguardian.com/uk&quot;&gt;The Guardian&lt;/a&gt; website (for no particular reason, most online news sites suffer the same issue). How many third party requests can we see:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Guardian website opens 73 TCP connections when opening the homepage. Thats a lot of negotiations going on, and it has an impact on performance.&quot; decoding=&quot;async&quot; height=&quot;1089&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/gy50VeN4vF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/gy50VeN4vF-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/gy50VeN4vF-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/gy50VeN4vF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we see a grand total of 73 separate TCP connections that the browser is opening when visiting the homepage. It&#39;s quite clear to see the connection negotiations taking place on each row due to the very distinctive DNS + Connect + SSL colours. You could guess there&#39;s a lot of TCP connections using the Waterfall View, but it doesn&#39;t illustrate the point as well as the Connection view does.&lt;/p&gt;&lt;p&gt;Using the same method &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; uses in his &lt;a href=&quot;https://andydavies.me/blog/2018/02/19/using-webpagetest-to-measure-the-impact-of-3rd-party-tags/&quot;&gt;&#39;Measuring the Impact of 3rd-Party Tags With WebPageTest&#39;&lt;/a&gt; blog post, we can easily block all third party requests and see what impact this has on the Connection View:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;And this is what the Guardian website looks like with all the third party assets blocked. 5 connections in total&quot; decoding=&quot;async&quot; height=&quot;148&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ifVd-ViCHX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ifVd-ViCHX-300.webp 300w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ifVd-ViCHX-600.webp 600w, https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/ifVd-ViCHX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;From a total of 73 connections in the Connection View, down to 5 when you block all the third party requests. That&#39;s a 93% reduction in the number of connections! If you&#39;ve ever wondered why the web experience is bad on many mobile / low specification devices, here&#39;s a clear illustration why.&lt;/p&gt;&lt;h3 id=&quot;more-to-be-added-soon&quot;&gt;More to be added soon...&lt;/h3&gt;&lt;p&gt;As I discover more common scenarios I will add them here. If you know of any common ones that are missing, please do &lt;a href=&quot;https://hachyderm.io/@TheRealNooshu&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;So there&#39;s a big brain dump of information about the WebPageTest Connection View, that should allow you to extract as much information for debugging web performance issues as possible. It&#39;s a really powerful view that can be used to spot many potential issues on its own, but when used in conjunction with the Waterfall View above it, it adds a whole new dimension to the test results.&lt;/p&gt;&lt;p&gt;As always, if there are any issues, errors, or anything I&#39;ve missed please do &lt;a href=&quot;https://hachyderm.io/@TheRealNooshu&quot;&gt;let me know&lt;/a&gt;. I hope you&#39;ve found it useful, and thanks for reading.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;30/12/19: Initial blog post published. Thanks &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for the technical review and feedback.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>HTTP/2 and Subresource Integrity don&#39;t always get on</title>
    <link href="https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/" />
    <updated>2019-12-17T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/</id>
    <content type="html">&lt;p&gt;I recently wrote a couple of blog posts about the WebPageTest waterfall chart, one on this very blog &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;here&lt;/a&gt;, and another on this years &lt;a href=&quot;https://calendar.perfplanet.com/2019/reading-a-webpagetest-waterfall-chart/&quot;&gt;PerfPlanet 2019&lt;/a&gt;. I&#39;ve learnt lots while writing them and also had some really great feedback and questions. One question in particular by &lt;a href=&quot;https://twitter.com/IacobanIulia/status/1204935306690072584&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@IacobanIulia&lt;/a&gt; really opened a can of worms. What initially I thought was a simple case of &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#chrome-stair-step&quot;&gt;Chrome stair-stepping&lt;/a&gt; turned out to be something a little more complicated. First a little bit of context about the waterfall chart &lt;a href=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/#the-waterfall&quot;&gt;in question&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;the-waterfall&quot;&gt;The Waterfall&lt;/h2&gt;&lt;p&gt;Back in November 2018 we (at &lt;a href=&quot;https://gds.blog.gov.uk/&quot;&gt;GDS&lt;/a&gt;) were trialing the use of &lt;a href=&quot;https://http2.github.io/&quot;&gt;HTTP/2&lt;/a&gt; on &lt;a href=&quot;https://www.gov.uk/&quot;&gt;GOV.UK&lt;/a&gt;. According to quite a few sources, enabling HTTP/2 should improve web performance for users by introducing technology like multiplexed streams, HPACK header compression and stream prioritisation. But at GDS we try not to enable features blindly and accept them at face value, so I decided to setup a set of tests to see if enabling HTTP/2 did actually improve performance. I won&#39;t go into all the (synthetic) test details here, but from testing 5 different page types on multiple devices, browsers and connections, it was found that it actually made performance worse in many cases. Sometimes only by a few 100 ms, other times by 10 seconds+ on a 2G connection (metrics examined: First visual change, Visually complete 95%, Last visual change, Speed index, load time). Not great, so I&#39;m glad I tested! Summary of results from the tests can be seen below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Spreadsheet summarising results from the HTTP/1.1 vs HTTP/2 comparison tests. In most cases HTTP/1.1 scored better.&quot; decoding=&quot;async&quot; height=&quot;241&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/z8Hgwq87_8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/z8Hgwq87_8-300.webp 300w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/z8Hgwq87_8-600.webp 600w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/z8Hgwq87_8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This was quite unexpected and I spent the next few days trying to work out what was going on, and why performance actually seemed worse. It was very easy to see that HTTP/2 was enabled as the HPACK header compression was working:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;HTTP/1.1&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;h2load https://www.gov.uk -n 4 --h1 | tail -6 | head -1
traffic: 117.22KB (120036) total, 2.45KB (2504) headers (space savings 0.00%), 114.36KB (117104) data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;HTTP/2&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;h2load https://www.gov.uk -n 4 | tail -6 | head -1
traffic: 115.28KB (118042) total, 793B (793) headers (space savings 67.82%), 114.36KB (117104) data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;68% saving in header data over H2 compared to H1. Another easy confirmation was the shape of the resulting waterfall graphs &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=181010_CQ_13c6a6e982262e3288c3538f5f64de77%2C181017_KV_23430e4e9a5a712149dece3828808a84&amp;thumbSize=200&amp;ival=500&amp;end=all&quot;&gt;were totally different&lt;/a&gt;. So it was enabled and the browsers were seeing the difference.&lt;/p&gt;&lt;h2 id=&quot;the-performance-issue&quot;&gt;The performance issue&lt;/h2&gt;&lt;p&gt;In many cases a lot of the tests were only slower by around 100-200ms. It doesn&#39;t sound like much, but that&#39;s quite a lot of time in the web performance world. Especially considering all we had done was enabled HTTP/2 on the CDN. It all boiled down to the fact that after the initial HTML download, all other page assets were being delayed (by ~100-200ms). On slower devices and connections, this short delay at the start of the waterfall was compounded, leading to a difference of many seconds by the end of the page load.&lt;/p&gt;&lt;p&gt;Now at the time, I put this down to the fact that we have a separate assets domain that all our static assets are served from. The DNS + Connect + SSL negotiation to this &quot;asset&quot; domain had become a bottleneck under HTTP/2:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;HTTP/2 Waterfall chart showing all assets waiting for the negotiation to the assets domain before download can commence.&quot; decoding=&quot;async&quot; height=&quot;261&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/4t8B8LqZBB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/4t8B8LqZBB-300.webp 300w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/4t8B8LqZBB-600.webp 600w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/4t8B8LqZBB-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see from the waterfall, none of the other assets can be requested or downloaded until that 2nd TCP connection is established. If you compare it to the HTTP/1.1 version of the waterfall, you will see the difference quite clearly:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Waterfall chart showing a similar wait time for HTTP/1.1, but there are now six TCP connections being negotiated.&quot; decoding=&quot;async&quot; height=&quot;271&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/0AjVCpIXdo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/0AjVCpIXdo-300.webp 300w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/0AjVCpIXdo-600.webp 600w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/0AjVCpIXdo-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Under HTTP/1.1, the browser opens multiple TCP connections (usually around 6) to maximise connection throughput by downloading files in parallel. It is the browser that is deciding the priority of the assets being requested. So it requests the highest priority ones first (the CSS). And since there are 6 parallel connections to choose from, it can do this very quickly.&lt;/p&gt;&lt;p&gt;With HTTP/2, everything is prioritised and streamed over a single TCP connection (for optimal performance). It is the server that is deciding the priority (&lt;a href=&quot;https://github.com/andydavies/http2-prioritization-issues&quot;&gt;often badly&lt;/a&gt; unfortunately). The browser simply requests a whole bunch of files, then the server will drip feed them to the browser in the order it thinks is best. The browser is very dependant on this single connection being negotiated quickly. Until it does, nothing can be downloaded from the domain.&lt;/p&gt;&lt;p&gt;In my head this was the issue that was slowing the pages down. The impact of the second domain (or shard as they are also known) needed to be minimised. So following this path we started to look into potential fixes.&lt;/p&gt;&lt;h3 id=&quot;1-preconnect-header&quot;&gt;1 - Preconnect header&lt;/h3&gt;&lt;p&gt;The theory is, if you can establish a connection to the assets domain as soon as possible, this should speed up all downloads from that domain when they are actually required. The quickest way to do this is to use the &lt;a href=&quot;https://www.w3.org/TR/resource-hints/#fetching-the-resource-hint-link&quot;&gt;preconnect header&lt;/a&gt;. As soon as the HTML page headers are received, the browser is given a hint that in the future it will need to connect to another domain. Example below doesn&#39;t add the &lt;code&gt;crossorigin&lt;/code&gt; attribute but it is an option, more on this later.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Link: &amp;lt;https://www.example.com&gt;; rel=preconnect
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There are two methods to tell a browser about the &lt;code&gt;preconnect&lt;/code&gt;. Either as a header, or as &lt;code&gt;link&lt;/code&gt; element in the HTML &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;. We choose the header method as it is easier to roll-out across the whole site, and Edge (18) &lt;a href=&quot;https://caniuse.com/#feat=link-rel-preconnect&quot;&gt;only supports the header version&lt;/a&gt;. This will all change with the release of Edge Chromium (76).&lt;/p&gt;&lt;p&gt;Notice that this is only a &lt;em&gt;hint&lt;/em&gt;. The browser may choose to ignore the hint if it already has everything under control. We tested with &amp; without the &lt;code&gt;preconnect&lt;/code&gt; header and it didn&#39;t make any difference from what we could see. For more information on the &lt;code&gt;preconnect&lt;/code&gt; header checkout &lt;a href=&quot;https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/&quot;&gt;&#39;Improving Perceived Performance With the Link rel=preconnect HTTP Header&#39;&lt;/a&gt; by Andy Davies.&lt;/p&gt;&lt;h3 id=&quot;2-http-2-connection-coalescing&quot;&gt;2 - HTTP/2 connection coalescing&lt;/h3&gt;&lt;p&gt;HTTP/2 connection coalescing, in theory should allow you to have your cake and eat it when it comes to migrating from HTTP/1.1 domain sharding, to HTTP/2. A simple explanation is: if a domain and a sub-domain share similar properties (e.g. IP address, SSL certificate), then the browser need not open a second connection. It can just use the first. In our case, being as the origin and assets domain sit on the same infrastructure (CDN), there&#39;s no reason why this can&#39;t happen. Unfortunately browser support is &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=1011685&quot;&gt;flaky&lt;/a&gt;. We checked all the criteria we needed to meet, but saw no actual improvement in performance. You would not believe how many times I read the post &lt;a href=&quot;https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/&quot;&gt;&#39;HTTP/2 connection coalescing&#39;&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/bagder&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Daniel Stenberg&lt;/a&gt;, trying to figure out why we were seeing no improvement at all! In the end I assumed it simply wasn&#39;t working due to browser support issues.&lt;/p&gt;&lt;h2 id=&quot;switching-http-2-off&quot;&gt;Switching HTTP/2 off&lt;/h2&gt;&lt;p&gt;After the initial trial of around 4 weeks, and with no progress on fixing the issue to make HTTP/2 match (let alone beat) HTTP/1.1 performance, I made the decision to switch it off. There&#39;s no reason to be slowing down some users experience of GOV.UK for the &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1up5rxd4EMCoMaxH8cppcK1x76n6HLx0e7jxb0e0FXvc/edit#gid=0&quot;&gt;extra 7.1%&lt;/a&gt; that enabling HTTP/2 gives you in your Lighthouse best practices score.&lt;/p&gt;&lt;p&gt;The only route forward I could see was:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Remove the assets domain completely and serve the CSS, JS, and images from the origin.&lt;/li&gt;&lt;li&gt;Serve only the HTML &amp; CSS from the origin, but leave the rest on the assets domain.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Written down, these don&#39;t sound too bad to implement. Unfortunately when it comes to a website the size and complexity of GOV.UK, nothing is ever that simple. So a future upgrade to HTTP/2 was left for when this work could be prioritised.&lt;/p&gt;&lt;h2 id=&quot;the-rogue-image&quot;&gt;The rogue image&lt;/h2&gt;&lt;p&gt;Enough context and rambling, back to the initial waterfall chart I &lt;a href=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/#the-waterfall&quot;&gt;mentioned at the start&lt;/a&gt;. Examining it again, there is something strange going on. If you look at request 2, there is a rogue image that is being downloaded. The reason why it&#39;s strange is because according to the same waterfall chart, the DNS + Connect + SSL connection to the &#39;assets&#39; domain hasn&#39;t been negotiated yet (request 3).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;On request number 2, the image is downloading from the assets domain before the connection has been established.&quot; decoding=&quot;async&quot; height=&quot;326&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/sDRG07AaiP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/sDRG07AaiP-300.webp 300w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/sDRG07AaiP-600.webp 600w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/sDRG07AaiP-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So how can that image be downloading from the assets domain &lt;strong&gt;before&lt;/strong&gt; the connection to it has been established? This is where everything slots into place and it all starts to make sense. If we examine the connection view for the same test:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Examining the connection view, coalescing was actually happening but a second TCP connection was still being opened.&quot; decoding=&quot;async&quot; height=&quot;160&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/eTK1l3y29h-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/eTK1l3y29h-300.webp 300w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/eTK1l3y29h-600.webp 600w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/eTK1l3y29h-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;HTTP/2 coalescing was actually happening(!), hence why the image was downloading &lt;strong&gt;before&lt;/strong&gt; the assets domain connection was established. In other words, the image was downloaded on the same connection as was used when downloading the initial HTML. The question is: why is a second connection to the asset domain actually required then? This is where Subresource Integrity(SRI) comes into play.&lt;/p&gt;&lt;h3 id=&quot;subresource-integrity&quot;&gt;Subresource Integrity&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity&quot;&gt;Subresource Integrity&lt;/a&gt; is a security measure that developers can enable to make sure that assets being served from a domain are only used if they contain code that is as expected. This is useful if you have a third party script hosted on an external domain, and you want to be sure that if the script were to change (e.g. is hacked), it won&#39;t run.&lt;/p&gt;&lt;p&gt;This is done by attaching an &lt;code&gt;integrity&lt;/code&gt; attribute to the &lt;code&gt;&amp;lt;script&gt;&lt;/code&gt; tag, with a sha256, sha384, or sha512 encoded hash of the file as the value, e.g:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;https://assets.publishing.service.gov.uk/static/libs/jquery/jquery-1.12.4-c731c20e2995c576b0509d3bd776f7ab64a66b95363a3b5fae9864299ee594ed.js&quot; crossorigin=&quot;anonymous&quot; integrity=&quot;sha256-xzHCDimVxXawUJ0713b3q2Sma5U2OjtfrphkKZ7llO0=&quot;&gt;&amp;lt;/script&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once downloaded, the browser will calculate the hash of the file. If it matches the hash listed in the &lt;code&gt;integrity&lt;/code&gt; attributes value, then the script will execute. Should the third party site be hacked and our embedded script is modified, the browser calculated hash won&#39;t match that of the &lt;code&gt;integrity&lt;/code&gt; attribute and the script won&#39;t be executed.&lt;/p&gt;&lt;h3 id=&quot;crossorigin-anonymous&quot;&gt;Crossorigin anonymous&lt;/h3&gt;&lt;p&gt;But if you look closely at the above script you will see another attribute: &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt;. This &lt;code&gt;crossorigin&lt;/code&gt; attribute provides support for &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&quot;&gt;CORS&lt;/a&gt;, and it defines how the element handles cross-origin requests. The value of &#39;anonymous&#39; means that there will be no exchange of user credentials via cookies, client-side SSL certificates or HTTP authentication, unless on the same origin. The &lt;code&gt;crossorigin&lt;/code&gt; attribute is a required security measure needed for SRI to work, and can be seen in &lt;a href=&quot;https://www.w3.org/TR/SRI/#cross-origin-data-leakage&quot;&gt;the specification&lt;/a&gt;. And at this point we reach the root of the issue.&lt;/p&gt;&lt;h2 id=&quot;the-issue&quot;&gt;The issue&lt;/h2&gt;&lt;p&gt;In order for the browser to download any of these SRI requests, it must first establish a TCP connection in &#39;anonymous mode&#39;, since the initial connection to the server is using a non-CORS credentialed connection. This obviously takes time, as TCP connections &lt;a href=&quot;https://hpbn.co/building-blocks-of-tcp/&quot;&gt;are expensive&lt;/a&gt;. While this new connection is being established, all the assets on the page are waiting for it.&lt;/p&gt;&lt;p&gt;What makes this even worse in GOV.UK&#39;s case is that the CSS also has SRI enabled. The browser is desperately trying to download the CSS so it can create the render tree (CSSOM + DOM), but the delay is stopping it from doing so. Once done it can move then onto the &lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction&quot;&gt;layout stage&lt;/a&gt; (also known as reflow) of the page build process. This delay is a real killer, and it puts the browser on the back-foot in terms of performance right from the start.&lt;/p&gt;&lt;h3 id=&quot;order-of-events&quot;&gt;Order of events&lt;/h3&gt;&lt;p&gt;Lets quickly run through the order of events the browser is seeing under these conditions:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Initial connection to the server negotiated, the HTML page downloads&lt;/li&gt;&lt;li&gt;Browser sees the &lt;code&gt;preconnect&lt;/code&gt; in the headers and examines the domain that is referenced&lt;/li&gt;&lt;li&gt;Browser sees that the origin and &lt;code&gt;preconnect&lt;/code&gt; domain are similar, so coalescing can occur&lt;/li&gt;&lt;li&gt;With this coalesced connection now ready, the first image is sent by the server&lt;/li&gt;&lt;li&gt;The browser now sees that the CSS has SRI enabled, and realises it doesn&#39;t have an &#39;anonymous&#39; connection established to allow it to be downloaded&lt;/li&gt;&lt;li&gt;Browser opens the &#39;anonymous&#39; connection (after some delay), and all other CSS / JS assets are downloaded using it (2nd line in the connection view)&lt;/li&gt;&lt;li&gt;Other non-SRI assets continue to use the initial connection&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;There&#39;s another issue here for HTTP/2. TCP connections are independent of each other, so HTTP/2 prioritisation won&#39;t work across them (as they have no knowledge of what each other is doing). For HTTP/2 prioritisation to work properly, the browser needs to be using multiplexing streams over a single TCP connection.&lt;/p&gt;&lt;h3 id=&quot;the-impact-sri-is-having&quot;&gt;The impact SRI is having&lt;/h3&gt;&lt;p&gt;Looking at the waterfall, it is possible to estimate the impact this SRI setup is having on the page metrics under HTTP/2:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Under the conditions seen in the sample waterfall, the requests could be bumped up by 750ms.&quot; decoding=&quot;async&quot; height=&quot;243&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/vXpvPi-wHa-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/vXpvPi-wHa-300.webp 300w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/vXpvPi-wHa-600.webp 600w, https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/vXpvPi-wHa-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Assuming both the CSS and JS are allowed to use the initial coalesced connection, their request start time for this particular test could be brought forward by 750 ms! That&#39;s a huge chunk of time in web performance terms. Assuming we can make similar proportional changes across all the tests I listed in the spreadsheet above, I&#39;m sure a high percentage would suddenly turn very green and switch to &#39;h2&#39;.&lt;/p&gt;&lt;h2 id=&quot;future-plans&quot;&gt;Future plans&lt;/h2&gt;&lt;p&gt;So now that this has been spotted, what next? Well, the bare minimum that needs to happen is that SRI is disabled on CSS assets (or changed to &lt;code&gt;crossorigin=&quot;use-credentials&quot;&lt;/code&gt; which should in theory allow us to use that already established, credentialed connection). If this happens, at least the browser can quickly create the render tree over the coalesced connection in a non-blocking fashion. By this point the &lt;code&gt;preconnect&lt;/code&gt; domains for any &#39;anonymous mode&#39; assets should have been negotiated in parallel, ready for it to be used when needed (e.g. with fonts).&lt;/p&gt;&lt;p&gt;There&#39;s then the question of disabling SRI on JavaScript assets. Personally I&#39;d like to see it disabled as it will allow all assets to be multiplex streamed over the single TCP connection. Since the assets domain is first party (we control it), it seems to make sense to do this. Although I&#39;m sure many security experts may be shaking their heads and disagreeing with this. If you are, I&#39;d love to hear why over on &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;And lastly, the final option could be to remove the assets domain (shard) completely, and serve all static assets from the main domain. This would be the ideal solution, as it removes any need for the flaky HTTP/2 connection coalescing. But due to the complexity of the GOV.UK website, I don&#39;t see this happening anytime soon.&lt;/p&gt;&lt;p&gt;I&#39;ll be sure to post an update once there&#39;s been some progress and verified it actually fixes the issue (fingers crossed!). Thanks go out to &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for their technical input, and &lt;a href=&quot;https://twitter.com/IacobanIulia&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Iulia Iacoban&lt;/a&gt; for the original question!&lt;/p&gt;&lt;h2 id=&quot;update-january-2020&quot;&gt;Update - January 2020&lt;/h2&gt;&lt;h3 id=&quot;cors-throws-a-spanner-in-the-works&quot;&gt;CORS throws a spanner in the works&lt;/h3&gt;&lt;p&gt;Just when I thought everything was going to be simple, CORS rears its safety conscious head and stops you in your tracks. If you happen to be serving your assets from the other domain with the &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; header, you can&#39;t simply swap &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; for &lt;code&gt;crossorigin=&quot;use-credentials&quot;&lt;/code&gt; as the browser will give you a few angry looking warning and block the request:&lt;/p&gt;&lt;p&gt;&lt;code&gt;Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘https://assets.example.com/script.js&#39;. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin&#39; is ‘*&#39;).&lt;/code&gt;&lt;/p&gt;&lt;p&gt;It turns out the wildcard value doesn&#39;t allow it to be used in this way over a credentialed connection. Attempting to use the wildcard with credentials will &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials&quot;&gt;result in an error&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you are interested in what the &lt;a href=&quot;https://fetch.spec.whatwg.org&quot;&gt;Fetch specification&lt;/a&gt; says, there&#39;s some &lt;a href=&quot;https://fetch.spec.whatwg.org/#cors-protocol-and-credentials&quot;&gt;information in the table&lt;/a&gt; (5th row down), and it&#39;s reiterated again in the paragraph under the table:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;, &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;, and &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; response headers can only use &lt;code&gt;*&lt;/code&gt; as value when request&#39;s credentials mode is not &quot;include&quot;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Thinking about it more (assuming I understand CORS), in using the wildcard and asking the browser to use a credentialed connection, you are essentially telling the browser to allow a script to be loaded from a cross-origin domain that can be executed by all domains (&lt;code&gt;*&lt;/code&gt;), and you are telling the browser to use a credentialed connection. So really, it is removing the security protections that CORS is there to fix.&lt;/p&gt;&lt;p&gt;Back to the drawing board for the moment...&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Implementing Webmentions on this blog</title>
    <link href="https://nooshu.com/blog/2019/11/18/implementing-webmentions/" />
    <updated>2019-11-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2019/11/18/implementing-webmentions/</id>
    <content type="html">&lt;p&gt;I never got around to implementing a comment system on this blog. After migrating from Wordpress to Jekyll and Github Pages (a static site) it was never someting that really interested me enough to spend time on. I briefly looked into a client side comment systems like Disqus but never actually took it any further than the research phase. I&#39;m glad about that considering the hit Disqus has on web performance and issues data ownership. Things like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;loading 450KB of data, most of which is JavaScript&lt;/li&gt;&lt;li&gt;loading takes place from multiple different domains&lt;/li&gt;&lt;li&gt;poor cache optimisation for loaded content&lt;/li&gt;&lt;li&gt;you don&#39;t own the comments, they are stored on a third-party server&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There&#39;s an excellent post here by &lt;a href=&quot;https://nicolas-hoizey.com/2017/07/so-long-disqus-hello-webmentions.html&quot;&gt;Nicolas Hoizey&lt;/a&gt; who goes into these issues and others in much more detail if you&#39;d like to know more. So no Disqus comments. What&#39;s the alternative?&lt;/p&gt;&lt;h2 id=&quot;introducing-webmentions&quot;&gt;Introducing Webmentions&lt;/h2&gt;&lt;p&gt;I must admit I hadn&#39;t heard of Webmentions until fairly recently, but once I did I thought they sounded very interesting. So I&#39;ve spent the past few weeks looking into implementing them on this blog. So what are they you may ask? Well I&#39;ll let the &lt;a href=&quot;https://www.w3.org/TR/webmention/&quot;&gt;W3C specification&lt;/a&gt; explain that:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Webmention is a simple way to notify any URL when you mention it on your site. From the receiver&#39;s perspective, it&#39;s a way to request notifications when other sites mention it.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Essentially they are quite similar to the WordPress Pingback and Trackback systems, which allowed WordPress blogs to talk to each other. For example, a blog post written on one WordPress blog could tell another that it had linked to it using these systems. Unfortunately the systems were hijacked by spammers resulting in its decline. Great idea, ruined by a certain demographic of users on the web. This is why we can&#39;t have nice things...&lt;/p&gt;&lt;p&gt;The &lt;a href=&quot;https://www.w3.org/TR/2016/WD-webmention-20160112/&quot;&gt;first W3C Public Working Draft&lt;/a&gt; was published on 12 January 2016 by &lt;a href=&quot;https://aaronparecki.com/&quot;&gt;Aaron Parecki&lt;/a&gt;, and it has grown in popularity ever since. Webmention offers a number of advantages over Pingbacks and Trackbacks:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;it uses HTTP and &lt;code&gt;x-www-form-urlencoded&lt;/code&gt; content, a much more widely accepted format compared to XMLRPC used by PingBacks and Trackbacks&lt;/li&gt;&lt;li&gt;faster and easier to integrate due to use of these accepted formats&lt;/li&gt;&lt;li&gt;W3C standard, rather than a proprietary system (bundled with WordPress)&lt;/li&gt;&lt;li&gt;spam protection using the &lt;a href=&quot;https://indieweb.org/Vouch&quot;&gt;Vouch protocol&lt;/a&gt;&lt;/li&gt;&lt;li&gt;human readable format rather than a simple title and an ellipsed summary as was the case with Pingbacks&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you want to know more about Webmentions, checkout this post by &lt;a href=&quot;https://adactio.com/journal/6495&quot;&gt;Jeremy Keith&lt;/a&gt; and this &lt;a href=&quot;https://alistapart.com/article/webmentions-enabling-better-communication-on-the-internet/&quot;&gt;ALA article&lt;/a&gt; by Chris Aldrich.&lt;/p&gt;&lt;h2 id=&quot;my-implementation&quot;&gt;My implementation&lt;/h2&gt;&lt;p&gt;As mentioned before I use &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; via GitHub pages to compile a set of posts and templates into static HTML pages. It works well, but a major issue with GH pages is that you can&#39;t use custom plugins. You can only use those listed on &lt;a href=&quot;https://pages.github.com/versions/&quot;&gt;this page&lt;/a&gt;. That means &lt;a href=&quot;https://github.com/aarongustafson/jekyll-webmention_io&quot;&gt;jekyll-webmention_io&lt;/a&gt; isn&#39;t an option, unless I plan to completely change my workflow. This isn&#39;t something I&#39;m willing to do at the moment as I plan to migrate to &lt;a href=&quot;https://www.11ty.io/&quot;&gt;11ty&lt;/a&gt; at some point in the future. I will leave that large change until then. So I decided to look into a client-side only alternative.&lt;/p&gt;&lt;p&gt;I stumbled across the article &lt;a href=&quot;https://keithjgrant.com/posts/2019/02/adding-webmention-support-to-a-static-site/&quot;&gt;&quot;Adding Webmention Support to a Static Site&quot;&lt;/a&gt; by Keith Grant, which was incredibly useful. &lt;a href=&quot;https://github.com/keithjgrant/keithjgrant.com/blob/master/themes/shindig/static-src/js/webmentions.js&quot;&gt;His code&lt;/a&gt; wasn&#39;t quite what I wanted, so I decided to adapt it a little more for my needs. Also, I don&#39;t get to write code as much as I used to, so it was a good opportunity to write a little JavaScript again!&lt;/p&gt;&lt;p&gt;This is what I wanted from my implementation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;progressive enhancement&lt;/li&gt;&lt;li&gt;minimal impact on web performance&lt;/li&gt;&lt;li&gt;no external dependencies and minimal impact on the build pipeline&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;ll go into each of these points individually.&lt;/p&gt;&lt;h3 id=&quot;progressive-enhancement&quot;&gt;Progressive enhancement&lt;/h3&gt;&lt;p&gt;Working at &lt;a href=&quot;https://www.gov.uk/government/organisations/government-digital-service&quot;&gt;Government Digital Service (GDS)&lt;/a&gt; I&#39;m often involved in a lot of discussions about &lt;a href=&quot;https://www.gov.uk/service-manual/technology/using-progressive-enhancement&quot;&gt;progressive enhancement&lt;/a&gt; and why it is the approach we should (and are) using. At it&#39;s heart, progressive enhancement is focused on building the most resilient website you can through the use of layering technologies on top of each other. Build your core functionality using only HTML, layer on the CSS, then enhance further using JavaScript. If a user has a device that supports the latest features, they get the full experience. A user on an older device, or if something fails on the page (e.g. the JavaScript), these users won&#39;t get the full experience, but they will get &lt;strong&gt;an experience&lt;/strong&gt;. If built correctly they should still be able to complete the core task they came to the website for.&lt;/p&gt;&lt;p&gt;The core experience of this blog is for a user to be able to visit the site and read the content. A user can do this if the JavaScript fails (or is disabled). They can even read the content should the CSS fail (or be disabled). Give it a try some time. It is the ultimate minimalist responsive layout. Without JavaScript, a user gets to submit a Webmention via the form provided on each post page. If JavaScript loads and executes they get the enhanced experience. All Webmentions and comments are loaded, and the Webmention form submits via Ajax rather than a standard form post.&lt;/p&gt;&lt;h3 id=&quot;minimal-impact-on-web-performance&quot;&gt;Minimal impact on web performance&lt;/h3&gt;&lt;p&gt;I wanted to keep the script as small as possible as this is one way to minimise the performance impact on devices. But I also acknowledge that not all users are going to read the whole post. They may just be skimming, or land on the incorrect page. So why burden these users with JavaScript for content they aren&#39;t going to see. This is where the use of the &lt;code&gt;IntersectionObserver&lt;/code&gt; API comes in handy. It&#39;s an API that allows you to read the position of a DOM element relative to the top-level viewport. It is often used to lazy-load images and scripts. Only once a user reaches a particular point in the page will an image load or script execute. For this blog I&#39;ve set the bottom of a post to be the point where the script is loaded and executed.&lt;/p&gt;&lt;p&gt;There&#39;s a small &lt;code&gt;app.js&lt;/code&gt; file (1.1KB minified, 686 bytes gzipped) that does a few things. It performs a very simplistic version of &quot;&lt;a href=&quot;https://responsivenews.co.uk/post/18948466399/cutting-the-mustard&quot;&gt;cutting the mustard&lt;/a&gt;&quot;, by testing the availability of the &lt;code&gt;IntersectionObserver&lt;/code&gt; API. If the browser passes, there is then a test to see if certain ES6 (ES2015) features are supported (more on that later). Assuming the browser passes both tests, the loading of the &lt;code&gt;webmentions.js&lt;/code&gt; script is primed (6.4KB minified, 2.1KB gzipped). If a user now scrolls to the bottom of the page, the &lt;code&gt;webmentions.js&lt;/code&gt; script is injected in to the page and it executes. This in turn loads all the Webmentions that the post has (if any).&lt;/p&gt;&lt;p&gt;The &lt;code&gt;webmentions.js&lt;/code&gt; script has a couple of performance considerations too. All the templates for the replies, likes, and reposts are contained within this file, rather than using a &lt;code&gt;&amp;lt;template&gt;&lt;/code&gt; tag. This means that these templates are &lt;em&gt;only&lt;/em&gt; loaded if a users browser passes both tests, and the user scrolls to the bottom of the page. This gives a slight saving in initial HTML size for all users, no matter what browser they use.&lt;/p&gt;&lt;p&gt;The second consideration is related to the profile images that are retrieved for each Webmention. By default, each of these profile images is large in terms of physical dimensions and page weight. Now considering we could be loading quite a few of these profile images, and they are only displayed at thumbnail size, that&#39;s a lot of pixels and data that will be loaded that isn&#39;t needed. To solve this I&#39;ve made use of the &lt;a href=&quot;https://cloudinary.com/invites/lpov9zyyucivvxsnalc5/kh7jghmmsicppn6aq461&quot;&gt;Cloudinary&lt;/a&gt; free tier. The Cloudinary API allows you to send an image URL to their servers, which then responds back with an optimised (and resized) image for use on a page. These optimised images are cached on their server once generated, so image manipulation / optimisation only occurs once.&lt;/p&gt;&lt;h3 id=&quot;no-external-dependencies-and-minimal-impact-on-build-pipeline&quot;&gt;No external dependencies and minimal impact on build pipeline&lt;/h3&gt;&lt;p&gt;It was very important to me not to require any external dependencies when creating this implementation. DOM manipulation would have been so much easier if I&#39;d loaded a version of jQuery, but do I really need to load 30KB of minified and gzipped JavaScript just for DOM manipulation and Ajax? Not at all. So all code was written in plain old vanilla JavaScript. If this is something you are considering I recommend checking out &lt;a href=&quot;http://youmightnotneedjquery.com/&quot;&gt;You might not need jQuery&lt;/a&gt; and &lt;a href=&quot;https://blog.garstasio.com/you-dont-need-jquery/&quot;&gt;You Don&#39;t Need jQuery!&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;And lastly I didn&#39;t want to have to modify my build process to be able to use new ES6 (ES2015) features. The usual method for using modern JavaScript syntax is to use &lt;a href=&quot;https://babeljs.io/&quot;&gt;Babel&lt;/a&gt;. You input JavaScript with the latest features and get ES5 browser compatible JavaScript out the other end. Now since I&#39;m only using a few modern features (&lt;code&gt;const&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt;, &lt;code&gt;template literals&lt;/code&gt;), and I plan to completely overhaul the build pipeline when migrating to &lt;a href=&quot;https://www.11ty.io/&quot;&gt;11ty&lt;/a&gt; in the future, this is something I decided to avoid for the moment. Hence the inclusion of the test for ES6 (ES2015) features mentioned earlier. A browser will only get to the &quot;modern&quot; JavaScript features if it passes both the &lt;code&gt;IntersectionObserver&lt;/code&gt; test and the ES6 (ES2015) test.&lt;/p&gt;&lt;h2 id=&quot;show-me-the-code-already&quot;&gt;Show me the code already!&lt;/h2&gt;&lt;p&gt;I&#39;m sure you&#39;re bored of my ramblings by now and all you want is the code. So here it is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://gist.github.com/Nooshu/121e5bf7f3d5c6528413c57db257894f&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gist for &lt;code&gt;app.js&lt;/code&gt; and &lt;code&gt;webmentions.js&lt;/code&gt;&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The code contains lots of comments and links to sources where I have copy &amp; pasted others code (isn&#39;t open source great!). You can see it in action if you scroll to the bottom of &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/&quot;&gt;this page on WebPageTest waterfall charts&lt;/a&gt;. If you spot any improvements or idiotic errors in the code (or this post), either leave a comment on &lt;a href=&quot;https://gist.github.com/Nooshu/121e5bf7f3d5c6528413c57db257894f&quot; title=&quot;Opens in a new tab&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;the gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt; or send me a Webmention! I&#39;m using &lt;a href=&quot;https://brid.gy/&quot;&gt;Bridgy&lt;/a&gt;, so a mention on &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter&lt;/a&gt; should work too.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>How to read a WebPageTest Waterfall View chart</title>
    <link href="https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/" />
    <updated>2019-10-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Table of contents&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#basic-layout&quot;&gt;Basic Layout&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#1---key&quot;&gt;1 - Key&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#2---request-list&quot;&gt;2 - Request list&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#3---request-timeline&quot;&gt;3 - Request timeline&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#4---cpu-utilisation&quot;&gt;4 - CPU Utilisation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#5---bandwidth-in&quot;&gt;5 - Bandwidth In&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#6---browser-main-thread&quot;&gt;6 - Browser Main Thread&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#7---page-is-interactive-long-tasks&quot;&gt;7 - Page is Interactive (Long Tasks)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#vertical-lines&quot;&gt;Vertical lines&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#horizontal-timings&quot;&gt;Horizontal timings&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-1---the-html&quot;&gt;Request 1 - The HTML&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-7---a-third-party-javascript-file&quot;&gt;Request 7 - A third-party JavaScript file&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-15---a-sponsor-png&quot;&gt;Request 15 - A sponsor PNG&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-23---gif-file-moved&quot;&gt;Request 23 - GIF file moved&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#download-chunks&quot;&gt;Download chunks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#inline-script-execution&quot;&gt;Inline script execution&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#updated-javascript-execution-visualisation&quot;&gt;Updated JavaScript execution visualisation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#different-first-byte-values&quot;&gt;Different First Byte values&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#common-scenarios&quot;&gt;Common scenarios&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#dns-prefetch&quot;&gt;DNS-prefetch&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#preconnect&quot;&gt;Preconnect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#prefetch&quot;&gt;Prefetch&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#prerender&quot;&gt;Prerender&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#preloading&quot;&gt;Preloading&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#http11-vs-http2&quot;&gt;HTTP/1.1 vs HTTP/2&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#ocsp&quot;&gt;OCSP&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#firefox-enhanced-tracking-protection&quot;&gt;Firefox enhanced tracking protection&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#service-worker-precaching&quot;&gt;Service worker precaching&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#identifying-requests-not-from-the-main-page-document&quot;&gt;Identifying requests not from the main page document&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#chrome-stair-step&quot;&gt;Chrome stair-step&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#html-early-flush&quot;&gt;HTML early flush&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#long-dom-content-loaded-dcl-time&quot;&gt;Long DOM content loaded (DCL) time&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#the-never-ending-waterfall&quot;&gt;The never-ending waterfall&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#rogue-image-downloads-http2-with-sri&quot;&gt;Rogue image downloads (HTTP/2 with SRI)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#requests-with-no-response-body-errorstatus-code--1---2&quot;&gt;Requests with no response body (Error/Status Code: -1 &amp; -2)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#third-party-blocking-javascript&quot;&gt;Third party blocking JavaScript&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#http2-prioritisation&quot;&gt;HTTP/2 prioritisation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#large-time-to-first-byte-ttfb&quot;&gt;Large Time to First Byte (TTFB)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#inactivity-after-ssl-negotiation&quot;&gt;Inactivity after SSL negotiation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#missing-component-times-dns-connect-tls&quot;&gt;Missing component times (DNS, Connect, TLS)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#the-impact-of-a-cpu-bottleneck&quot;&gt;The impact of a CPU bottleneck&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#misconfigured-cors-headers&quot;&gt;Misconfigured CORS headers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#consequences-of-a-301-redirect-when-changing-a-url&quot;&gt;Consequences of a 301 redirect when changing a URL&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#the-missing-intermediate-certificate&quot;&gt;The missing intermediate certificate&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#a-vary-unusual-waterfall-empty-gaps-between-download-data&quot;&gt;A &lt;code&gt;vary&lt;/code&gt; unusual waterfall (empty gaps between download data)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#examining-undefined-204-status-codes&quot;&gt;Examining undefined 204 status codes&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#service-worker-precache-slowing-a-page-load&quot;&gt;Service Worker precache slowing a page load&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#http11-and-the-connection-close-header&quot;&gt;HTTP/1.1 and the &lt;code&gt;Connection: close&lt;/code&gt; header&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#content-autofill-on-mobile-devices&quot;&gt;Content autofill on mobile devices&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#identifying-a-cross-origin-resource-sharing-cors-connection&quot;&gt;Identifying a Cross-Origin Resource Sharing (CORS) connection&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#eager-prefetches&quot;&gt;Eager prefetches&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#the-performance-impact-of-font-face-source-failures&quot;&gt;The performance impact of &lt;code&gt;@font-face&lt;/code&gt; source failures&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#double-font-downloads-when-using-the-preload-resource-hint&quot;&gt;Double font downloads when using the &lt;code&gt;preload&lt;/code&gt; resource hint&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#svgs-opening-up-a-new-tcp-connection&quot;&gt;SVG&#39;s opening up a new TCP connection&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#hidden-gems&quot;&gt;Hidden gems&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-the-filmstrip-view-and-waterfall-chart-are-related&quot;&gt;How the filmstrip view and waterfall chart are related&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#what-do-the-filmstrip-thumbnail-border-colours-signify&quot;&gt;What do the filmstrip thumbnail border colours signify&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-generate-customised-waterfall-chart-images&quot;&gt;How to generate customised waterfall chart images&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-examine-detailed-information-about-images-on-a-page&quot;&gt;How to examine detailed information about images on a page&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-inspect-the-browser-main-thread---part-1&quot;&gt;How to inspect the &#39;Browser Main Thread&#39; - Part 1&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-inspect-the-browser-main-thread---part-2&quot;&gt;How to inspect the &#39;Browser Main Thread&#39; - Part 2&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-analyse-page-images-for-optimal-compression-and-render-size&quot;&gt;How to analyse page images for optimal compression and render size&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-visualise-the-requests-from-your-test&quot;&gt;How to visualise the requests from your test&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-visualise-variance-in-metrics-across-multiple-tests&quot;&gt;How to visualise variance in metrics across multiple tests&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-add-custom-marks-using-the-user-timing-api&quot;&gt;How to add custom marks using the User Timing API&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-display-the-bandwidth-graph-on-mobile-devices&quot;&gt;How to display the bandwidth graph on mobile devices&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-test-a-404-pages-web-performance&quot;&gt;How to test a 404 pages web performance&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-delve-into-the-download-chunk-data&quot;&gt;How to delve into the download chunk data&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-view-a-sites-http2-dependency-graph-chrome&quot;&gt;How to view a sites HTTP/2 dependency graph (Chrome)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-view-a-sites-http2-dependency-graph-firefox&quot;&gt;How to view a sites HTTP/2 dependency graph (Firefox)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-to-view-the-consolelog-information-from-a-test&quot;&gt;How to view the &lt;code&gt;console.log&lt;/code&gt; information from a test&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I often find myself looking at &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt; (WPT) waterfall charts, but as I seem to have the &lt;a href=&quot;https://www.nationalgeographic.com/content/news/en_US/2015/02/150225-dogs-memories-animals-chimpanzees-science-mind-psychology.html&quot;&gt;memory of a chimpanzee&lt;/a&gt; (not a goldfish, that&#39;s a myth), I tend to forget some of the details and what they all mean. So I decided to pull together many bits of information into a single blog post I can refer to at a later date. If you find it useful, or think I&#39;ve forgotten anything, please &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;. If you are interested in learning about the &lt;code&gt;Connection View&lt;/code&gt; that sits directly below the &lt;code&gt;Waterfall View&lt;/code&gt;, then check out my post &lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/&quot;&gt;&#39;How to read a WebPageTest Connection View chart&#39;&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;basic-layout&quot;&gt;Basic Layout&lt;/h2&gt;&lt;p&gt;Here&#39;s the basic layout of the waterfall chart user interface:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Basic layout of the Web Page Test waterfall chart includes the 1:key, 2:request list, 3:request timeline, 4:CPU utilisation, 5:Bandwidth in, 6:browser main thread, 7:page is interactive.&quot; decoding=&quot;async&quot; height=&quot;599&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/SczmWWq6U5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/SczmWWq6U5-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/SczmWWq6U5-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/SczmWWq6U5-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;1-key&quot;&gt;1 - Key&lt;/h3&gt;&lt;p&gt;The key shows three types of information:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Information about the connection status (DNS lookup, connection established, SSL negotiation)&lt;/li&gt;&lt;li&gt;The type of resource being requested (e.g. HTML, images etc)&lt;/li&gt;&lt;li&gt;Miscellaneous events (wait, JavaScript execution)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Each resource has 2 colours, a light shade and a dark shade. The light shade signifies the point at which the browser has made the request for the resource. The dark shade is the point at which the resource is actually downloading. See this &lt;a href=&quot;https://web.archive.org/web/20191228065137/https://www.webpagetest.org/forums/showthread.php?tid=14643&amp;pid=28907#pid28907&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. His reply is pasted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The lighter time is the time from when the browser sends the request out on the wire until the first byte comes back. The darker color is the period where the resource is actually downloading.&lt;/p&gt;&lt;p&gt;Looks like the production site is using HTTP/2 and the dev site is not which is why you see radically different request behavior.&lt;/p&gt;&lt;p&gt;With http/2 the browser can issue all of the requests to the server in one go and the server will send them back in the order the browser prioritized them (mostly).&lt;/p&gt;&lt;p&gt;with the dev server, the browser can only make 6 requests at a time and has to wait for each to complete before issuing more.&lt;/p&gt;&lt;p&gt;Generally, all else being equal, HTTP/2 will perform better so it is going to be hard to compare your dev work to the production system without having the same serving architecture.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The &quot;wait&quot; visualisation is a new addition to WPT. It shows you the time between when the browser first discovered the resource on the page, up to when the request for the resource was made by the browser to the server.&lt;/p&gt;&lt;h3 id=&quot;2-request-list&quot;&gt;2 - Request list&lt;/h3&gt;&lt;p&gt;A list of assets the browser has discovered on the page and the order in which they were requested. Note the request number on the far left, as well as the yellow lock if the request is being made over a secure connection (HTTPS).&lt;/p&gt;&lt;h3 id=&quot;3-request-timeline&quot;&gt;3 - Request timeline&lt;/h3&gt;&lt;p&gt;The timeline shows the time along the horizontal (x) axis verses each request made on the vertical (y) axis. From this you can see the lifecycle of a request made by the browser. From discovery (wait), through to request being made, and finally to asset being download.&lt;/p&gt;&lt;p&gt;Ideally you want to make sure this timeline covers as little time as possible, as this indicates better overall performance. The thinner the timeline, the quicker the page loads for a user.&lt;/p&gt;&lt;h3 id=&quot;4-cpu-utilisation&quot;&gt;4 - CPU Utilisation&lt;/h3&gt;&lt;p&gt;A simple graph showing the CPU utilisation of the browser process running on the device. It displays how much CPU the current webpage is using at any point in time. It ranges from 0 - 100% utilisation. See this &lt;a href=&quot;https://web.archive.org/web/20191224135704/https://www.webpagetest.org/forums/showthread.php?tid=256&amp;pid=1068&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. His response is also quoted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The CPU utilization line is the CPU utilization of just the browser process that is running the test (same as you would see in task manager) on a fixed scale from 0-100% (sampled in 0.1 sec intervals).&lt;/p&gt;&lt;p&gt;There are lots of things that cause the CPU utilization to spike but javascript is a very common contributor (particularly if there are gaps in the waterfall where the CPU is pegged at 100% - telltale sine of javascript that needs optimizing). The CPU will also get exercised as the browser figures out how to layout the page and apply styles and finally, the testing itself can also introduce some spikes (really short spikes when the screen shots are taken for example). If you capture a video then the impact of recording the screen shots will be higher.&lt;/p&gt;&lt;p&gt;Usually if I see something that looks suspicious I&#39;ll load it up into Dynatrace Ajax Edition (great free profiler for IE) and run the page through to see if anything jumps out. I&#39;d love to integrate the capture of the profiling data at some point in the future.&lt;/p&gt;&lt;p&gt;Even though it wouldn&#39;t really impact the numbers, right now only one test is ever executing on a given machine at a time. We can (and have) run configurations with multiple tests at the same time but when video capture was introduced it killed our ability to run in parallel. I&#39;m experimenting with running the tests in VM&#39;s to see if I can get more testing capacity out of the hardware because the machines are pretty beefy and could easily handle 8+ tests in parallel but I won&#39;t do it if it introduces any variability into the results.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;5-bandwidth-in&quot;&gt;5 - Bandwidth In&lt;/h3&gt;&lt;p&gt;This is an indicator of when data is coming into the browser. The visualisation is helpful to see when the browser is doing useful work vs wasted time. The absolute scale can be ignored, as it isn&#39;t very accurate. Use the &quot;Capture network packet trace (tcpdump)&quot; option in the advanced tab on the WebPageTest homepage if you want more accurate results. See this &lt;a href=&quot;https://web.archive.org/web/20100711164736/https://www.webpagetest.org/forums/showthread.php?tid=266&amp;pid=1168#pid1168&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. His response is also quoted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The bandwidth graph is more just an indicator of when data is coming in. I&#39;ve debated removing it but it is helpful to see when the page is actually downloading stuff (useful work) and when it&#39;s not (wasted time). This can help a fair bit when you flush the document early to see the initial chunk of data coming in and to make sure that data continues to come in from then on.&lt;/p&gt;&lt;p&gt;Don&#39;t pay too much attention to the absolute scale because the raw measurements will not be very accurate (just by definition in how things are measured from the client side).&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;6-browser-main-thread&quot;&gt;6 - Browser Main Thread&lt;/h3&gt;&lt;p&gt;This graph visualises what the browsers main thread is doing at any specific point in time (x-axis). The y-axis shows the percentage from 0 - 100%. The colours were copied from the Chrome DevTools CPU graph (under the performance tab). Here&#39;s what each of the colours mean:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Orange - Script parsing, evaluation and execution &lt;span style=&quot;width:6px;background-color:#e5c35c;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;li&gt;Purple - Layout &lt;span style=&quot;width:6px;background-color:#927de2;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;li&gt;Green - Painting &lt;span style=&quot;width:6px;background-color:#87b268;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;li&gt;Blue - HTML parsing &lt;span style=&quot;width:6px;background-color:#86a5e1;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;li&gt;Gray - Main thread time used for task processing not accounted for in other categories &lt;span style=&quot;width:6px;background-color:#b3b3b3;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Using this graph it is possible to see if the CPU is becoming a bottleneck in one of the above areas.&lt;/p&gt;&lt;h3 id=&quot;7-page-is-interactive-long-tasks&quot;&gt;7 - Page is Interactive (Long Tasks)&lt;/h3&gt;&lt;p&gt;This graph gives you an indication of when the main thread is blocked. The red blocks indicate that the main thread has been blocked for 50ms (which will also block inputs like button presses). Green indicates the thread isn&#39;t blocked. &lt;strong&gt;Note:&lt;/strong&gt; it still may be possible to scroll during red blocked phases, as scrolling is usually handled off the main thread for most browsers. See this &lt;a href=&quot;https://web.archive.org/web/20200608005427/https://www.webpagetest.org/forums/showthread.php?tid=15235&amp;pid=30556#pid30556&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. His response is also quoted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Scrolling will generally be fine since it is usually handled off-thread for most browsers as long as there are no active scroll events attached. Clicking should be delayed but not necessarily for the full duration of the red (though the first one looks like a single executing JS function so it would be for the full duration of that block).&lt;/p&gt;&lt;p&gt;The red blocks are any point where the main thread is blocked (including from input) for 100ms. A lot of separate 100ms events can make it look like a long block or a single really long event can also show up (which could be a way worse experience).&lt;/p&gt;&lt;p&gt;How did you measure being able to click on a link during one of the red blocks? You&#39;d have to have a test machine handy with a similar CPU and connectivity and then try to click in the UI immediately after it starts to render.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Note: As of Feb 2021 &quot;Page is interactive&quot; was renamed to &quot;Long tasks&quot;. Everything stays the same, it&#39;s only a name change.&lt;/p&gt;&lt;h2 id=&quot;vertical-lines&quot;&gt;Vertical lines&lt;/h2&gt;&lt;p&gt;You can see the key for each of the coloured vertical lines below the &quot;Waterfall View&quot; header as seen below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Key for the vertical lines displayed on the timeline. From left to right: Start render, First Contentful Paint, Largest Contentful Paint, Layout Shift,DOM interactive, DOM content loaded, On load, Document complete.&quot; decoding=&quot;async&quot; height=&quot;73&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/fhJZzCfMwm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/fhJZzCfMwm-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/fhJZzCfMwm-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/fhJZzCfMwm-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;But what do each of them mean?&lt;/p&gt;&lt;h3 id=&quot;start-render-green-line&quot;&gt;Start Render - Green line &lt;span style=&quot;width:12px;background-color:#28bc00;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This is the first point at which a user will see pixels painted to the page. The pixels could be from anything at all (background colours, borders etc), not necessarily content. Before this point the screen was blank. This metric is measured by analysing individual video frames that are captured during the page load. See &lt;a href=&quot;https://web.archive.org/web/20210307002722/https://www.webpagetest.org/forums/showthread.php?tid=13265&amp;pid=24577#pid24577&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information (see below too):&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Start render is measured by capturing video of the page load and looking at each frame for the first time the browser displays something other than a blank page. It can only be measured in a lab and is generally the most accurate measurement for it.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;msFirstPaint is a measurement reported by the browser itself about when it thinks it painted the first content. It is available from javascript and can be reported from the field. It is usually pretty accurate but sometimes it reports the time when the browser painted nothing but a blank screen.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;first-contentful-paint-light-green-line&quot;&gt;First Contentful Paint - Light green line &lt;span style=&quot;width:12px;background-color:#8fbc83;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: In March 2021 the old &#39;RUM First Paint&#39; line was replaced with First Contentful Paint.&lt;/p&gt;&lt;p&gt;This is the point where the browser renders &lt;em&gt;anything&lt;/em&gt; to the screen that is visually different from before navigation (i.e. the blank screen for WPT). This metric is reported using the browser&#39;s performance timeline if the test browser supports the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver&quot;&gt;&lt;code&gt;PerformanceObserver&lt;/code&gt;&lt;/a&gt; API. So the value is when &lt;em&gt;it&lt;/em&gt; thinks it painted the first content to the viewport.&lt;/p&gt;&lt;h3 id=&quot;largest-contentful-paint-dark-green-dashed-line&quot;&gt;Largest Contentful Paint - Dark green dashed line &lt;span style=&quot;width:12px;background-color:green;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Largest Contentful Paint is an important metric that is part of the &lt;a href=&quot;https://web.dev/vitals/#core-web-vitals&quot;&gt;Core Web Vitals&lt;/a&gt;. It&#39;s a user-centric metric that helps developers understand the perceived performance of a web page. In practical terms it reports the render time of the largest image or text block visible within the viewport relative to when the page started to load. Much more information about LCP can be &lt;a href=&quot;https://web.dev/lcp/&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;layout-shift-orange-dashed-line&quot;&gt;Layout Shift - Orange dashed line &lt;span style=&quot;width:12px;background-color:#ff8000;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;A pages Cumulative Layout Shift (CLS) score is another metric from the &lt;a href=&quot;https://web.dev/vitals/#core-web-vitals&quot;&gt;Core Web Vitals&lt;/a&gt;. A user-centric metric that measures the overall stability of a page. WebPageTest will highlight every &lt;em&gt;unexpected layout shift&lt;/em&gt; that the test browsers API detects. An unexpected layout shift is one that isn&#39;t initiated by a users input (e.g. a button press). Much more information about CLS can be &lt;a href=&quot;https://web.dev/cls/&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You can have multiple layout shift lines on a waterfall chart simply because you can have multiple layout shifts during the test period. The layout shift line is the only line in the key where this happens.&lt;/p&gt;&lt;h3 id=&quot;dom-interactive-yellow-line&quot;&gt;DOM Interactive - Yellow line &lt;span style=&quot;width:12px;background-color:#ffc61a;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The point at which the browser has finished parsing all the HTML, and DOM construction is complete. Unfortunately it&#39;s &lt;a href=&quot;https://www.webpagetest.org/forums/showthread.php?tid=15495&amp;pid=31165#pid31165&quot;&gt;not a reliable metric&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;dom-content-loaded-pink-line&quot;&gt;DOM Content Loaded - Pink line &lt;span style=&quot;width:12px;background-color:#d888df;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The point at which the HTML has been loaded and parsed, and the browser has reached the end of the document. All blocking scripts have been loaded and run. The DOM at this point is completely defined. See &lt;a href=&quot;https://web.archive.org/web/20191225070452/https://www.webpagetest.org/forums/showthread.php?tid=12846&amp;pid=29909#pid29909&quot;&gt;comment from Pat Meenan&lt;/a&gt; for more information. Note the thickness of this line in the key. This is to indicate that this line covers a duration, rather than a single point in time in the waterfall. Pat&#39;s responses are also quoted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;domContentLoaded is a technical event the browser fires when it has finished parsing the HTML and reached the end of the document (which means it has loaded and run run all blocking scripts as well). At that point the &quot;DOM&quot; (all of the elements in the document) is completely defined. The event shows up as a vertical pink bar in the waterfall.&lt;/p&gt;&lt;p&gt;There is a lot of non-blocking content that will still need to load that can radically impact the visual experience though (images and fonts in particular).&lt;/p&gt;&lt;p&gt;Visually Complete is measured from a video capture of the viewport loading and is the point when the visible part of the page first reached 100% &quot;completeness&quot; compared to the end state of the test.&lt;/p&gt;&lt;p&gt;Visually Complete can happen before domContentLoaded when the browser is busy parsing and executing things that aren&#39;t visible at the top of the page. A likely cause is blocking scripts at the end of the document. They don&#39;t visually impact what the user sees in the viewport but the parser can&#39;t reach the end of the document and fire DCL until it finishes downloading and running them.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;on-load-lavender-line&quot;&gt;On Load - Lavender line &lt;span style=&quot;width:12px;background-color:#c0c0ff;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The point at which the window load event fires. All objects are in the DOM and all images and scripts have finished loading. Note the thickness of this line in the key. This is to indicate that this line covers a duration, rather than a single point in time in the waterfall.&lt;/p&gt;&lt;h3 id=&quot;document-complete-blue-line&quot;&gt;Document Complete - Blue line &lt;span style=&quot;width:12px;background-color:#00f;display:inline-block&quot;&gt; &lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The point where the &lt;code&gt;onload&lt;/code&gt; event has fired and all the static image content has loaded. Content changes that are triggered by JavaScript execution may not be included.&lt;/p&gt;&lt;h2 id=&quot;horizontal-timings&quot;&gt;Horizontal timings&lt;/h2&gt;&lt;p&gt;So let&#39;s now concentrate on the request timeline (3). What do the horizontal blocks mean and what metrics do they refer too? Well, if you click on an individual request you will see a popup with a lot more information, as seen in the example below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;By clicking on an individual request in WebPageTest it will give you much more information including timing details, request details, response details and if enabled when testing, the original response body.&quot; decoding=&quot;async&quot; height=&quot;380&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vZtIumNT1r-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vZtIumNT1r-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vZtIumNT1r-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vZtIumNT1r-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So lets take a look at a few of the requests from &lt;a href=&quot;https://www.webpagetest.org/result/190927_0Q_e2e2601287e531bd66e88b1dd325f809/1/details/#waterfall_view_step1&quot;&gt;this waterfall view&lt;/a&gt;, as it gives us a number of quite varied requests to look at.&lt;/p&gt;&lt;h3 id=&quot;request-1-the-html&quot;&gt;Request 1 - The HTML&lt;/h3&gt;&lt;p&gt;Here the browser is requesting the HTML document, so at this point in time it is also having to setup the connection to the server. In the request details we are given the following timings:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Discovered: 0.011 s&lt;/li&gt;&lt;li&gt;Request Start: 0.116 s&lt;/li&gt;&lt;li&gt;DNS Lookup: 27 ms&lt;/li&gt;&lt;li&gt;Initial Connection: 25 ms&lt;/li&gt;&lt;li&gt;SSL Negotiation: 43 ms&lt;/li&gt;&lt;li&gt;Time to First Byte: 315 ms&lt;/li&gt;&lt;li&gt;Content Download: 40 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;ve annotated the request to show what each of these timings mean:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Request number one with timings annotations added, including all the timings listed above.&quot; decoding=&quot;async&quot; height=&quot;286&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4g8jrbasBM-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4g8jrbasBM-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4g8jrbasBM-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4g8jrbasBM-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Adding the DNS, Initial Connection, SSL negotiation, Time to First Byte (TTFB) and the Content download times gives you the 450ms that is displayed directly after the request finishes.&lt;/p&gt;&lt;p&gt;It&#39;s worth noting that WPT follows a specific convention in the request details panel:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If the time corresponds to a duration, it is measured is milliseconds (ms), e.g. the DNS lookup took 27ms.&lt;/li&gt;&lt;li&gt;If the time corresponds to a starting point, it is measured in seconds (s), e.g. the request started at 0.116s.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;request-7-a-third-party-javascript-file&quot;&gt;Request 7 - A third-party JavaScript file&lt;/h3&gt;&lt;p&gt;This request is different from the other requests examined because the file is coming from a different domain. The request details give the following timings:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Discovered: 0.473 s&lt;/li&gt;&lt;li&gt;Request Start: 0.702 s&lt;/li&gt;&lt;li&gt;DNS Lookup: 28 ms&lt;/li&gt;&lt;li&gt;Initial Connection: 39 ms&lt;/li&gt;&lt;li&gt;SSL Negotiation: 153 ms&lt;/li&gt;&lt;li&gt;Time to First Byte: 48 ms&lt;/li&gt;&lt;li&gt;Content Download: 9 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Notice how the browser needs to go through the whole connection negotiation again (DNS, Connection, SSL negotiation) because the file exists on a different domain. This adds a fair chunk of time to the request (28 + 39 + 153 = 220ms).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Request 7 loads a JavaScript file from a third-party domain. This causes a new TCP connection to be established. 200ms after the JavaScript is downloaded the graph shows JavaScript execution that is also visible on the Browser main thread.&quot; decoding=&quot;async&quot; height=&quot;269&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/24miAQFjEj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/24miAQFjEj-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/24miAQFjEj-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/24miAQFjEj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The other interesting point about this request is the script executes around 200ms after the download is complete. There&#39;s no information about this execution in the details panel, but you can see it in the waterfall as light pink lines after the request and orange in the &quot;Browser Main Thread&quot; panel (6) which signifies &quot;Script parsing, evaluation and execution&quot;.&lt;/p&gt;&lt;h3 id=&quot;request-15-a-sponsor-png&quot;&gt;Request 15 - A sponsor PNG&lt;/h3&gt;&lt;p&gt;With this request the browser has discovered a PNG image and requests it from the server. In the request details we are given the following timings:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Discovered: 0.652 s&lt;/li&gt;&lt;li&gt;Request Start: 0.824 s&lt;/li&gt;&lt;li&gt;Time to First Byte: 214 ms&lt;/li&gt;&lt;li&gt;Content Download: 28 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Wait time is calculated by subtracting the discovered time from the request start time. The wait time is the time taken from when the browser first finds the asset, to the time when it has the capacity to send a request to the server.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Request 15 downloads a PNG image. The waterfall now includes the new waiting period from when the asset was discovered by the page, to when the browser makes the request.&quot; decoding=&quot;async&quot; height=&quot;217&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CMYX3j76tg-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CMYX3j76tg-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CMYX3j76tg-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CMYX3j76tg-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The duration after this request is the time taken from the request being made, to when the request is completed (Time to First Byte + Content Download). Since a connection to the domain has already been established, there&#39;s no need for DNS, Connect, SSL negotiation.&lt;/p&gt;&lt;h3 id=&quot;request-23-gif-file-moved&quot;&gt;Request 23 - GIF file moved&lt;/h3&gt;&lt;p&gt;Although request 23 looks quite unremarkable, there are a couple of things going on. The background of the request is yellow. This is to signify a server response status code that isn&#39;t the usual &lt;code&gt;200&lt;/code&gt;. In this instance it is a &lt;code&gt;302&lt;/code&gt; status code, which signifies the GIF file has been temporarily moved. In fact all responses with a &lt;code&gt;3xx&lt;/code&gt; status code will have a yellow background. The request details show the following information:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Error/Status Code: 302&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;Request 23 shows how WPT displays 302 redirects in the form of a highlighted bar in yellow and a different status code after the request has completed.&quot; decoding=&quot;async&quot; height=&quot;175&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/qDub6gWUkS-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/qDub6gWUkS-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/qDub6gWUkS-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/qDub6gWUkS-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice how request 23 doesn&#39;t require a TCP connection to be established. This is because it has already happened for this domain on request 20.&lt;/p&gt;&lt;p&gt;Error status codes &lt;code&gt;4xx&lt;/code&gt; and &lt;code&gt;5xx&lt;/code&gt; are displayed in a similar way, only the background is red, like in the example below (note this image is from a different test):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Request 7 shows what happens on a WebPageTest waterfall graph when it encounters a status code starting with a 4xx (error) or 5xx (Internal Server Error). The background of the request is coloured red, and the status code at the end displays 404.&quot; decoding=&quot;async&quot; height=&quot;258&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/VTAGeJXfRZ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/VTAGeJXfRZ-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/VTAGeJXfRZ-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/VTAGeJXfRZ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The request details show the following information:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Error/Status Code: 404&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Notice the colour of the returned resource response in this instance. Rather than being the expected purple colour for an image, it is blue signifying that it is an HTML page. Or, in other words, it is the server responding with the 404 page because the asset can&#39;t be found.&lt;/p&gt;&lt;h3 id=&quot;download-chunks&quot;&gt;Download chunks&lt;/h3&gt;&lt;p&gt;Another visual peculiarity you may be curious about is the vertical stripes in each of the requests. As mentioned earlier, the lighter colour signifies the request has been made and the browser is waiting for a response. The darker colour indicates that bytes for the specific resource are being delivered to the browser. Now sometimes this doesn&#39;t happen all at the same time. This results in what looks like zebra striping, where the browser is being drip-fed bytes over time. These are called download chunks (red arrows).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Download chunks can be seen in requests and they look like zebra striping. This indicates that the browser is being drip-fed bytes for each asset. Chunking usually occurs when lots of assets are competing for bandwidth, or the HTML early flush technique is being used.&quot; decoding=&quot;async&quot; height=&quot;409&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5KETlZROb5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5KETlZROb5-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5KETlZROb5-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5KETlZROb5-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This is very visible when an &lt;a href=&quot;https://www.stevesouders.com/blog/2009/05/18/flushing-the-document-early/&quot;&gt;HTML early flush&lt;/a&gt; technique is being used (request 2 - see &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#html-early-flush&quot;&gt;scenario below&lt;/a&gt; for more details) or if a large number of assets are being downloaded in parallel and are competing for resources (requests 3-9). Note the bandwidth graph at the bottom of the UI is maxed out from 1.6 to 2.5 seconds.&lt;/p&gt;&lt;p&gt;You may be asking &quot;but this chunking also occurs &lt;em&gt;after&lt;/em&gt; the bandwidth usage drops (2.6 seconds+), so what is happening there?&quot;. Well the number of parallel connections has dropped so less is downloading in parallel. But the connections that have been created in requests 12 - 15 are still in the &lt;a href=&quot;https://en.wikipedia.org/wiki/TCP_congestion_control#Slow_start&quot;&gt;TCP slow start&lt;/a&gt; phase, so the the assets are still competed for (now limited) bandwidth.&lt;/p&gt;&lt;h3 id=&quot;inline-script-execution&quot;&gt;Inline script execution&lt;/h3&gt;&lt;p&gt;In the above section about &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-7---a-third-party-javascript-file&quot;&gt;A third-party JavaScript file&lt;/a&gt; I already mentioned how the JavaScript execution is visible if you look along a request row and identify the pink lines. But in that example a third-party script was used, which was loaded via a script tag pointing to another domain. But say for example you had a number of inline scripts in your page (Google Analytics, Dreamweaver MM_preloadImages etc). How would they be visualised?&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Inline JavaScript execution can be seen as thin pink lines within the HTML download stage of the request. Said execution can also be seen on the browser main thread.&quot; decoding=&quot;async&quot; height=&quot;200&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CujqGoieTD-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CujqGoieTD-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CujqGoieTD-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/CujqGoieTD-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we see the browser downloading chunks of HTML, then in very close proximity to these downloads we see JavaScript execution lines (in pink). This is from JavaScript that is inline in the page that is executing. If you look very closely in the browser main thread you will see both HTML parsing (blue), and very small slithers of yellow to indicate the JavaScript execution.&lt;/p&gt;&lt;p&gt;This waterfall is also a prime example of HTML early flush which I go into more detail &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#html-early-flush&quot;&gt;later in this blog post&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;updated-javascript-execution-visualisation&quot;&gt;Updated JavaScript execution visualisation&lt;/h3&gt;&lt;p&gt;A recent update to WebPageTest has made the JavaScript visualisation on the waterfall chart even more useful. If we take a look at how it used to look:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The JavaScript execution was seen as a solid block of pink previously, which didn&#39;t really tell us the level of execution that was happening.&quot; decoding=&quot;async&quot; height=&quot;114&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/n573CuGix3-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/n573CuGix3-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/n573CuGix3-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/n573CuGix3-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In this example above we see a solid block of pink execution. But the question is, is this very intensive JavaScript running or just short but fast repeated execution? Both of these would look exactly the same on the previous version of the visualisation. Now if we compare this to the new version:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The JavaScript execution was seen as a solid block of pink previously, which didn&#39;t really tell us the level of execution that was happening.&quot; decoding=&quot;async&quot; height=&quot;114&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/oJr_gqnW7K-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/oJr_gqnW7K-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/oJr_gqnW7K-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/oJr_gqnW7K-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now when JavaScript execution is rendered you don&#39;t always just see a solid block of pink. The height of the pink execution bars indicates how quickly the JavaScript ran. So fast but frequent JavaScript execution will have very short height bars, so they no longer dominate the view.&lt;/p&gt;&lt;h3 id=&quot;different-first-byte-values&quot;&gt;Different First Byte values&lt;/h3&gt;&lt;p&gt;In the details tab of a set of test results there is a summary table right at the top of the page. It displays some of the most important metrics for quick review. These metrics include &#39;Web Vitals&#39;, &#39;Document Complete&#39;, and &#39;Fully Loaded&#39;. To the far left of the table you will also see a &#39;First Byte&#39; column. An eagle eyed reader may also spot a &#39;Time to First Byte&#39; listing when you click on the pages HTML request (see example waterfall). What&#39;s unusual here is that these two values are different, So let&#39;s examine what is happening here.&lt;/p&gt;&lt;p&gt;In our listing panel for the HTML we see:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Discovered: 0.007 s&lt;/li&gt;&lt;li&gt;DNS Lookup: 304 ms&lt;/li&gt;&lt;li&gt;Initial Connection: 303 ms&lt;/li&gt;&lt;li&gt;SSL Negotiation: 334 ms&lt;/li&gt;&lt;li&gt;Time to First Byte: 445 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The &#39;First Byte&#39; value seen in the top table is the sum of all these values (Discovered + DNS + Connect + SSL + TTFB). So it is basically the time taken from &lt;code&gt;navigationStart&lt;/code&gt; to the browser receiving the very first byte of data from the server. It&#39;s also worth noting that if there are any redirects during this time (not seen in the example waterfall), they will also be included in the &#39;First Byte&#39; value. Where as &#39;Time to First Byte&#39; in the listing panel is simply the time from request start, to when the first byte of data was received. It&#39;s easier to explain via an annotated waterfall diagram:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Waterfall graph explaining the &#39;First Byte&#39; column value, and how it is made up of separate metrics from the HTML request listing.&quot; decoding=&quot;async&quot; height=&quot;693&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/mvI9mBSppD-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/mvI9mBSppD-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/mvI9mBSppD-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/mvI9mBSppD-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;common-scenarios&quot;&gt;Common scenarios&lt;/h2&gt;&lt;p&gt;Here&#39;s a list of common patterns seen in a WPT waterfall chart. I&#39;ll add more of these over time as I encounter them.&lt;/p&gt;&lt;h3 id=&quot;dns-prefetch&quot;&gt;DNS-prefetch&lt;/h3&gt;&lt;p&gt;DNS Prefetch is part of the &lt;a href=&quot;https://www.w3.org/TR/resource-hints/#dns-prefetch&quot;&gt;Resource Hints Working Draft&lt;/a&gt;. It gives a developer the ability to tell the browser that a DNS lookup is going to be needed for another domain in the near future. So instead of waiting, start the lookup immediately. By the time the domain is actually required, the browser will only need to complete the TCP handshake, and optional SSL negotiation. It looks similar to the preconnect example below, in that it is &quot;floating&quot; in the timeline. But here only the DNS lookup (green) is visible.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The waterfall shows the dns-prefetch occur quite some distance from where it would usually happen (with the TCP and SSL negotiation). It is also worth noting where it occurred in the timeline, directly after the HTML download completed.&quot; decoding=&quot;async&quot; height=&quot;286&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XiaIDadahD-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XiaIDadahD-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XiaIDadahD-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XiaIDadahD-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice where the &lt;code&gt;dns-prefetch&lt;/code&gt; occurred in the timeline: almost immediately after the HTML has finished downloading and is parsed. It is easy to see the difference if you compare it to the connection negotiations happening in requests 5 and 7, where &lt;code&gt;preconnect&lt;/code&gt; is being used.&lt;/p&gt;&lt;h3 id=&quot;preconnect&quot;&gt;Preconnect&lt;/h3&gt;&lt;p&gt;Preconnect is part of the &lt;a href=&quot;https://www.w3.org/TR/resource-hints/#preconnect&quot;&gt;Resource Hints Working Draft&lt;/a&gt;. It allows a developer to give the browser a &quot;hint&quot; that a domain will need to be connected too in the near future. By connecting to the domain early, the connection won&#39;t need to be established later in the waterfall, thus allowing assets from said domain to be requested and downloaded quicker when they are required.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The image shows the preconnected domain establishing the connection earlier than required, before the asset is requested. This preconnect allows the request to happen immediately.&quot; decoding=&quot;async&quot; height=&quot;134&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ohw509Btt0-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ohw509Btt0-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ohw509Btt0-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ohw509Btt0-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see in the image above, the &lt;code&gt;preconnect&lt;/code&gt; looks to be &quot;floating&quot; in the timeline. It happens way before the actual request for the image is made. This is the browser using the &lt;code&gt;preconnect&lt;/code&gt; hint to connect ahead of time before it is required. For more information on the &lt;code&gt;preconnect&lt;/code&gt; hint I recommend reading &lt;a href=&quot;https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/&quot;&gt;this blog post&lt;/a&gt; by Andy Davies.&lt;/p&gt;&lt;h3 id=&quot;prefetch&quot;&gt;Prefetch&lt;/h3&gt;&lt;p&gt;Prefetch is part of the &lt;a href=&quot;https://www.w3.org/TR/resource-hints/#prefetch&quot;&gt;Resource Hints Working Draft&lt;/a&gt;. It allows a developer to tell the browser to prefetch a resource (e.g. CSS, JS, HTML document) in the current navigation, as it may be required at a later date. For example, if you know the majority of your users navigate to a specific page from your homepage (e.g. maybe your login page), you could decide to &lt;code&gt;prefetch&lt;/code&gt; it so it will already exist in the users browser cache when it is required. In the example below I am prefetching another HTML document that sits along a users journey:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;With Prefetching&lt;/strong&gt; &lt;img alt=&quot;The waterfall shows a request for two HTML files. One for the initial connection (request 1), another for the prefetch we have specified (request 19). The prefetched file is simply cached, it isn&#39;t parsed.&quot; decoding=&quot;async&quot; height=&quot;366&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/JqfDBGwlBK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/JqfDBGwlBK-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/JqfDBGwlBK-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/JqfDBGwlBK-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The &lt;code&gt;prefetch&lt;/code&gt; is visible at request 19 in blue (HTML). It is worth noting that this prefetched HTML is simply stored in the browser cache. It isn&#39;t parsed by the browser. You can verify this in the waterfall chart UI by looking at the browser main thread graph. At the point the the HTML is prefetched, there&#39;s no main thread activity registered.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Without Prefetching&lt;/strong&gt; &lt;img alt=&quot;Only one HTML request can be seen on the waterfall chart because prefetch isn&#39;t being used.&quot; decoding=&quot;async&quot; height=&quot;369&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YPYVufRmbm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YPYVufRmbm-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YPYVufRmbm-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YPYVufRmbm-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;WebPageTest gives us some information in the popup to let us know it is a &lt;code&gt;prefetch&lt;/code&gt; hint:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;Priority: IDLE&lt;/code&gt; (under the details tab)&lt;/li&gt;&lt;li&gt;&lt;code&gt;purpose: prefetch&lt;/code&gt; (under the request tab)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&#39;s important to remember the priority of a &lt;code&gt;prefetch&lt;/code&gt;. In WebPageTest, when testing in Chrome it is listed as priority &lt;code&gt;IDLE&lt;/code&gt;. This maps to &lt;code&gt;Lowest&lt;/code&gt; priority in DevTools (according to the &lt;a href=&quot;https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit&quot;&gt;Resource Fetch Prioritization and Scheduling in Chromium&lt;/a&gt; document). So a &lt;code&gt;prefetch&lt;/code&gt; is an &lt;a href=&quot;https://www.w3.org/TR/preload/#link-type-preload&quot;&gt;optional and often low-priority fetch&lt;/a&gt; and will be loaded as late as possible by the browser. This differs from &lt;code&gt;preload&lt;/code&gt; which is a mandatory fetch and gets a &lt;code&gt;High&lt;/code&gt; priority in the browser. A resource loaded using &lt;code&gt;preload&lt;/code&gt; is layout blocking, so use it sparingly else it could actually slow down perceived performance.&lt;/p&gt;&lt;h3 id=&quot;prerender&quot;&gt;Prerender&lt;/h3&gt;&lt;p&gt;Prerender is part of the &lt;a href=&quot;https://www.w3.org/TR/resource-hints/#prefetch&quot;&gt;Resource Hints Working Draft&lt;/a&gt;. It gives a developer the ability to tell a browser what a users likely next navigation could be (assuming the developer is tracking this in some form of analytics). In December 2017, with the release of Chrome 63, Google overhauled how &lt;code&gt;prerender&lt;/code&gt; worked. Here&#39;s a brief before and after explanation:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Pre-Chrome 63&lt;/strong&gt;: Chrome would look for the &lt;code&gt;&amp;lt;link rel=&quot;prerender&quot;&gt;&lt;/code&gt; element and create a hidden page for the URL listed by it. Unseen by the user, this page would be downloaded including all dependant subresources and any JavaScript would be executed. Should the user happen to navigate to the page, the prerendered page would be swapped out with the current tab, giving the user the impression of an instant page load. There were a &lt;a href=&quot;https://developers.google.com/web/updates/2018/07/nostate-prefetch#motivation&quot;&gt;few negatives to this method&lt;/a&gt;. The first, memory usage was high due to having to maintain the state of the hidden page. This made it unsuitable to low-end devices. And second, since user-facing and stateful actions should not occur with a &lt;code&gt;prerender&lt;/code&gt; (since the user can&#39;t see them), it was a very complex task to achieve this when the whole point of a &lt;code&gt;prerender&lt;/code&gt; was to &lt;em&gt;render&lt;/em&gt; the page. So the decision was taken to &lt;a href=&quot;https://groups.google.com/a/chromium.org/forum/#!msg/Blink-dev/0nSxuuv9bBw/EMfaclV-AwAJ&quot;&gt;depreciate and remove&lt;/a&gt; the then current implementation.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Chrome 63+&lt;/strong&gt;: Since the release of Chrome 63, the &lt;code&gt;prerender&lt;/code&gt; hint is still recognised and followed by Chrome, only it is handled in a much different way. Chrome now uses a technique called &quot;NoState Prefetch&quot; when it sees a &lt;code&gt;prerender&lt;/code&gt; link element. If seen it will be followed only if these two conditions are met: the user is not on a low-end device, and they are not on a mobile connection. If followed, the page will be downloaded and scanned for subresources to download. These will be downloaded and cached at the lowest priority possible (IDLE). No JavaScript will be executed on the prerendered page, and should the user actually navigate to the page, the browser will need to load the cached resources into a new browser window (not swapped out as before). A lot more information about &quot;NoState Prefetch&quot; can be &lt;a href=&quot;https://developers.google.com/web/updates/2018/07/nostate-prefetch&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So what does this &lt;code&gt;prerender&lt;/code&gt; look like in a WPT waterfall chart:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;With Prerendering&lt;/strong&gt; &lt;img alt=&quot;You can see the request for the prerendered HTML page, and the subsequent subresources loading. Many of these subresources already exist in the browser cache since they were download as part of the original homepage request.&quot; decoding=&quot;async&quot; height=&quot;615&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/yctkwhk_49-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/yctkwhk_49-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/yctkwhk_49-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/yctkwhk_49-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the waterfall chart you can see the usual page and resources loading from request 1 through to 19. Request 16 is where the &lt;code&gt;prerender&lt;/code&gt; occurs. Here you can see a request for a second HTML page. Once completed, this then triggers requests 20 through to 29. Notice how many of these requests have a yellow background with a &lt;code&gt;304&lt;/code&gt; status code. This is telling us they are identical to a resource that already exist in the browsers cache. They exist here because the homepage HTML above them (request 1) put them there only a few 100ms before. Notice how there&#39;s very little happening in the browser main thread graph (other than the homepage parsing which is happening because request 30 (CSS) completed). This confirms that the prerendered assets and subresources are simply being stored in the browser cache for later use.&lt;/p&gt;&lt;p&gt;As with &lt;code&gt;prefetch&lt;/code&gt;, WPT gives us a little information in the popup for each &lt;code&gt;prerender&lt;/code&gt; resource to let us know the requests aren&#39;t from a standard user navigation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;Priority: IDLE&lt;/code&gt; (under the details tab)&lt;/li&gt;&lt;li&gt;&lt;code&gt;purpose: prefetch&lt;/code&gt; (under the request tab)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: It doesn&#39;t explicitly tell us it comes from a &lt;code&gt;prerender&lt;/code&gt; hint, only from a &lt;code&gt;prefetch&lt;/code&gt;. Since &quot;NoState Prefetch&quot; is now being used, this actually makes sense.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Without Prerendering&lt;/strong&gt; &lt;img alt=&quot;Without the prerender link element you get your standard page load. No requests are made to other pages or subresources.&quot; decoding=&quot;async&quot; height=&quot;463&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/b2xH_e-KPT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/b2xH_e-KPT-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/b2xH_e-KPT-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/b2xH_e-KPT-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Without the &lt;code&gt;prerender&lt;/code&gt; link element you get your standard page waterfall chart. Only requests to the current page subresources can be seen.&lt;/p&gt;&lt;h3 id=&quot;preloading&quot;&gt;Preloading&lt;/h3&gt;&lt;p&gt;Preloading is a &lt;a href=&quot;https://www.w3.org/TR/preload/&quot;&gt;W3C Candidate Recommendation&lt;/a&gt; and is used to increase the loading priority of selected assets. A developer can tell the browser: &quot;this resource will absolutely be needed soon, so load it right away&quot;. This technique is often used when loading web fonts.&lt;/p&gt;&lt;p&gt;Without &lt;code&gt;preload&lt;/code&gt;, when loading a web font the browser first needs to download the HTML and CSS, then parse both to create the render tree. Only at this point can the browser request the font. This can lead to what is known as a &lt;a href=&quot;https://www.malthemilthers.com/font-loading-strategy-acceptable-flash-of-invisible-text/&quot;&gt;Flash of Invisible Text (FOIT) and Flash of Unstyled Text (FOUT)&lt;/a&gt;. A way around this issue is to request the web font file immediately using the &lt;code&gt;preload&lt;/code&gt; directive.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;With Preloading&lt;/strong&gt; &lt;img alt=&quot;A preloaded request comes directly after the HTML download starts (request number 2), when the browser parses the &lt;head&gt;. The request for this asset is started as soon as it is discovered.&quot; decoding=&quot;async&quot; height=&quot;258&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/e7L9t8Nqqy-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/e7L9t8Nqqy-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/e7L9t8Nqqy-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/e7L9t8Nqqy-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Without Preloading&lt;/strong&gt; &lt;img alt=&quot;The usual font loading occurs after the CSS has downloaded and is parsed. You can see the font has request number 11, verses number 2 when preloaded.&quot; decoding=&quot;async&quot; height=&quot;258&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/g6Hs6MTmxm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/g6Hs6MTmxm-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/g6Hs6MTmxm-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/g6Hs6MTmxm-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If you compare both of the images above you will see the request for the preloaded WOFF2 font is made as soon as the HTML starts to be downloaded at request number 2 (dark blue strip). The browser parsed the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag, saw the &lt;code&gt;preload&lt;/code&gt; directive and made the file request immediately.&lt;/p&gt;&lt;p&gt;Compare this to the second image, where the browser downloads the font after waiting for the HTML and CSS to be downloaded and parsed. Only at this point can the WOFF2 font request be made. As you can see from the image, when preloading isn&#39;t used the font is at request number 11. I&#39;ve written more about &lt;a href=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/&quot;&gt;font preloading here&lt;/a&gt; if you are interested.&lt;/p&gt;&lt;h3 id=&quot;http-1-1-vs-http-2&quot;&gt;HTTP/1.1 vs HTTP/2&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://http2.github.io/&quot;&gt;HTTP/2&lt;/a&gt; is the next iteration of the HTTP protocol after HTTP/1.1. Due to the fact that HTTP/2 uses a single TCP connection and multiplexes files over this single connection, it is easy to spot the difference in the resulting waterfall charts:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;HTTP/1.1&lt;/strong&gt; &lt;img alt=&quot;HTTP/1.1 waterfall showing the stepped image requests from the page due to multiple TCP connections having no knowledge of each other.&quot; decoding=&quot;async&quot; height=&quot;341&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/x0CnCsrzFV-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/x0CnCsrzFV-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/x0CnCsrzFV-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/x0CnCsrzFV-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;HTTP/2&lt;/strong&gt; &lt;img alt=&quot;HTTP/2 waterfall showing the simultaneous image requests by the browser. The browser will stream the image data back via a single connection.&quot; decoding=&quot;async&quot; height=&quot;341&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/G_Z54Ka_4c-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/G_Z54Ka_4c-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/G_Z54Ka_4c-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/G_Z54Ka_4c-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;A browser using HTTP/1.1 requests images via separate TCP connections, and this tends to happen at slightly different times (hence the stepped nature of the waterfall). A browser using HTTP/2 on the other hand requests all the images at the same time. It is the server that decides when the images will be sent back to the browser, and in what order.&lt;/p&gt;&lt;h3 id=&quot;ocsp&quot;&gt;OCSP&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol&quot;&gt;Online Certificate Status Protocol&lt;/a&gt; (OCSP) is an internet protocol used for obtaining the revocation status of SSL certificates. One way for a browser to certify a certificate is to connect to an OCSP server for verification. When this happens WebPageTest will show you in the waterfall as seen below:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;With OCSP&lt;/strong&gt; &lt;img alt=&quot;OCSP requests seen in Web Page Test appear before the request for the HTML. These request add a large amount of time to the SSL negotiation and the HTML download.&quot; decoding=&quot;async&quot; height=&quot;189&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BQ4bKd2ROX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BQ4bKd2ROX-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BQ4bKd2ROX-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BQ4bKd2ROX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This OCSP check is bad for performance. The verification requires a DNS lookup and an initial connection to the OCSP server. Only once the certificate has been verified, can the SSL negotiation take place to the original domain. As you can see in the image, the whole waterfall is being pushed back. It takes almost 2 seconds before the HTML page can even be requested!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Without OCSP&lt;/strong&gt; &lt;img alt=&quot;When the OCSP doesn&#39;t happen request number 1 is the request direct to the web server, setting up the connection (DNS, Connect, TLS), then requesting the HTML file.&quot; decoding=&quot;async&quot; height=&quot;189&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bMkNC2Q-Vk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bMkNC2Q-Vk-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bMkNC2Q-Vk-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bMkNC2Q-Vk-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If you compare the with &amp; without OCSP waterfalls, you can see that the length of the SSL negotiation is much shorter for without OCSP (300ms instead of 1000ms+) and therefore the request for the HTML file happens much quicker (at 1 second verses 1.95 seconds). The OCSP check adds 950ms to the initial HTML request on a 3G Fast connection. That&#39;s a huge number!&lt;/p&gt;&lt;p&gt;If you notice this on your WebPageTest timelines you should look into enabling &lt;a href=&quot;https://en.wikipedia.org/wiki/OCSP_stapling&quot;&gt;OCSP stapling&lt;/a&gt; on your server. &lt;strong&gt;Note&lt;/strong&gt;: If you are using &lt;a href=&quot;https://en.wikipedia.org/wiki/Extended_Validation_Certificate&quot;&gt;Extended Validation certificates&lt;/a&gt; (EV), OCSP stapling won&#39;t fully solve the issue, see this technical &lt;a href=&quot;https://twitter.com/sleevi_/status/1184221227944202242&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter thread&lt;/a&gt; for more details on this. Update: I&#39;ve now published a whole blog post about certificate revocation that you can &lt;a href=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/&quot;&gt;view here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;firefox-enhanced-tracking-protection&quot;&gt;Firefox enhanced tracking protection&lt;/h3&gt;&lt;p&gt;Firefox enabled enhanced tracking protection by default as of &lt;a href=&quot;https://blog.mozilla.org/blog/2019/06/04/firefox-now-available-with-enhanced-tracking-protection-by-default/&quot;&gt;version 69&lt;/a&gt; (June 2019). The agents on WebPageTest updated around the same time. In some &lt;a href=&quot;https://github.com/WPO-Foundation/wptagent/issues/284&quot;&gt;rare cases&lt;/a&gt; the tracking protection requests could be seen in the WPT waterfalls (request 1-3):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Firefox enabled enhanced tracking protection in version 69. A DNS lookup and the requests could be seen before the initial connection in some rare cases.&quot; decoding=&quot;async&quot; height=&quot;175&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/3HEBfELJGR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/3HEBfELJGR-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/3HEBfELJGR-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/3HEBfELJGR-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;According to Pat Meenan these requests should now be filtered out by default, so they will never be seen in the waterfall charts.&lt;/p&gt;&lt;h3 id=&quot;service-worker-precaching&quot;&gt;Service worker precaching&lt;/h3&gt;&lt;p&gt;The use of service workers is &lt;a href=&quot;https://httparchive.org/reports/progressive-web-apps#swControlledPages&quot;&gt;gradually increasing&lt;/a&gt;, and one of the many features they allow is a fine-grain control on what assets are cached and for how long. They also give a developer the &lt;a href=&quot;https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker#storing_resources&quot;&gt;ability to precache files&lt;/a&gt; for future use (e.g. for offline capabilities). An important detail to remember when precaching assets using a service worker, is the browser may need to download the same files twice. Once for the HTTP cache (the standard browser cache), and again for the service worker cache (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Cache&quot;&gt;Cache API&lt;/a&gt;). Theses are two totally separate caches, and don&#39;t share assets. These duplicate requests can be seen in a WebPageTest waterfall chart:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The two sets of identical assets are downloaded twice, once for the HTTP cache then again for the service worker cache.&quot; decoding=&quot;async&quot; height=&quot;438&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rjcfLBJBCC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rjcfLBJBCC-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rjcfLBJBCC-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rjcfLBJBCC-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In requests 17 and 18 you can see the service worker JavaScript being requested, downloaded, and initialised. Immediately after this, the service worker looks through it&#39;s precache JSON file and requests any assets listed. &lt;strong&gt;Note:&lt;/strong&gt; In the example above the &lt;a href=&quot;https://developers.google.com/web/tools/workbox&quot;&gt;Workbox&lt;/a&gt; library is being used to simplify the service worker workflow.&lt;/p&gt;&lt;h3 id=&quot;identifying-requests-not-from-the-main-page-document&quot;&gt;Identifying requests not from the main page document&lt;/h3&gt;&lt;p&gt;I mentioned directly above that you could (potentially) identify service worker requests by examining the shape of the waterfall. In the example above there is an obvious &#39;step&#39; where the main document requests finish, and the service worker precache requests start. But waterfalls from some sites are complex beasts and this shape isn&#39;t always visible. So a new feature has just been added, where requests that don&#39;t originate from the main page document (e.g. &lt;code&gt;&amp;lt;iframe&gt;&lt;/code&gt; and service worker requests) are now highlighted in blue (Chrome only). So it is now incredibly easy to spot when these requests are being made! I believe we all have &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;) (and of course &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;) to thank for this new functionality.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Resuests that don&#39;t come from the main thread are now highlighted in blue.&quot; decoding=&quot;async&quot; height=&quot;300&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xTkzsyb3H2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xTkzsyb3H2-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xTkzsyb3H2-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xTkzsyb3H2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above example we see the simple waterfall from the excellent &lt;a href=&quot;https://squoosh.app/&quot;&gt;Squoosh&lt;/a&gt; image compression progressive web application. Request 10-12 are now highlighted in blue as they all have a different &#39;Document&#39; value (Document: https://squoosh.app/serviceworker.js) in the &#39;Details&#39; tab when you click on each request.&lt;/p&gt;&lt;h3 id=&quot;chrome-stair-step&quot;&gt;Chrome stair-step&lt;/h3&gt;&lt;p&gt;Chrome has included a prioritisation technique that is named due to the pattern it creates in waterfall charts. It involves Chrome examining the assets in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag (before the page even has a &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt;), and requesting, downloading, and parsing these requests first. The browser even goes so far as to delay requests for assets in the body until the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; requests are complete. It is easier to see this stepping in a HTTP/1.1 graph, as seen in the example below (although it also occurs in HTTP/2):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome stair-stepping occurs when the browser delays requests from the body element in favour of completing the requests from the head element. This can be seen in the WPT waterfall as a distinct step in the timings.&quot; decoding=&quot;async&quot; height=&quot;411&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/sDv-CjfH2K-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/sDv-CjfH2K-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/sDv-CjfH2K-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/sDv-CjfH2K-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above image from the BBC News website, 8 out of the first 9 requests are made for assets in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt;, with only 1 request to a JavaScript file located in the &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt;. The &quot;step&quot; isn&#39;t very long in terms of duration, only around 200ms. But it gives the browser enough time to concentrate all CPU and bandwidth on downloading and parsing these assets, so the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; is then setup and ready to go before the &lt;code&gt;&amp;lt;body&gt;&lt;/code&gt; assets are downloaded and parsed.&lt;/p&gt;&lt;p&gt;Not much has been written about this &quot;layout-blocking&quot; phase in Chrome, but it can be seen in detail in the &lt;a href=&quot;https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit&quot;&gt;Resource Fetch Prioritization and Scheduling in Chromium&lt;/a&gt; document by Pat Meenan, and also in Chrome&#39;s &lt;a href=&quot;https://cs.chromium.org/chromium/src/services/network/resource_scheduler/resource_scheduler.cc?type=cs&amp;q=layout-blocking&amp;sq=package:chromium&amp;g=0&amp;l=993&quot;&gt;resource scheduler source code&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;html-early-flush&quot;&gt;HTML early flush&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://www.stevesouders.com/blog/2009/05/18/flushing-the-document-early/&quot;&gt;HTML early flush&lt;/a&gt; was mentioned above in the download chunks section. It is when a web server sends a small slice of the HTML document, before the whole HTML response is ready. What this then allows the browser to do is parse the HTML it has received, and look for assets that it can then request early (compared to waiting for the whole HTML document to download, parse, and then request).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;With early flush&lt;/strong&gt; &lt;img alt=&quot;With early flush the server sends a small chunk of HTML to be parsed. This parse then triggers additional requests much earlier than when it would usually happen (and therefor speed up the website).&quot; decoding=&quot;async&quot; height=&quot;369&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p7wpcJwHPF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p7wpcJwHPF-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p7wpcJwHPF-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p7wpcJwHPF-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example above, the small download chunk of HTML the browser receives (request 2) contains the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag which links to JavaScript, fonts, JSON, &lt;code&gt;preload&lt;/code&gt; directives, and &lt;code&gt;dns-prefetch&lt;/code&gt; resource hints. This HTML is parsed and 16 requests are almost immediately triggered very close to each other. &lt;strong&gt;Note&lt;/strong&gt;: Notice how I didn&#39;t list CSS. CNN.com has inlined the CSS in a &lt;code&gt;&amp;lt;style&gt;&lt;/code&gt; tag (so no request to a CSS file is required). They then trigger CSS requests via JavaScript later in the waterfall once the JavaScript has loaded and parsed.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Without early flush&lt;/strong&gt; &lt;img alt=&quot;Without early flush the browser must wait for the whole HTML document to be downloaded before it can be parsed, and then additional requests can be made. Notice how the HTML download complete lines up vertically with the request being made directly after it.&quot; decoding=&quot;async&quot; height=&quot;189&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/U_u8yD7aht-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/U_u8yD7aht-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/U_u8yD7aht-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/U_u8yD7aht-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If you compare the waterfalls for with &amp; without early flush (from different sites unfortunately): With flush you will notice how the requests for assets is made during the HTML download response. Compared to without, where the browser must wait for the whole HTML document response to complete. Only then can it be parsed and requests made to other page assets.&lt;/p&gt;&lt;p&gt;Flushing allows the browser to make requests earlier in the waterfall, and therefore makes the page load and feel faster (if implemented correctly).&lt;/p&gt;&lt;h3 id=&quot;long-dom-content-loaded-dcl-time&quot;&gt;Long DOM content loaded (DCL) time&lt;/h3&gt;&lt;p&gt;The vertical lines on the waterfall chart are usually very thin, maybe only a couple of pixels thick. This shows they take very little time to actually happen. But in some cases the DOM content loaded line takes up a large chunk of time, and you will very easily see it on your waterfall chart! Take a look at the two (different) charts below:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Normal DCL&lt;/strong&gt; &lt;img alt=&quot;DOM Content Loaded (DCL) vertical line is very thin, maybe only 1-2 pixels wide showing it took very little time.&quot; decoding=&quot;async&quot; height=&quot;336&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Okk9RRqyhI-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Okk9RRqyhI-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Okk9RRqyhI-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Okk9RRqyhI-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The pink DOM Content Loaded (DCL) vertical line is usually almost instantaneous. Very thin and barely visible on the waterfall chart as it usually occurs around the same time as other events like DOM Interactive &amp; Start Render. This is what a healthy DCL looks like.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Long DCL&lt;/strong&gt; &lt;img alt=&quot;DOM Content Loaded (DCL) vertical line is very thin, maybe only 1-2 pixels wide showing it took very little time.&quot; decoding=&quot;async&quot; height=&quot;377&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p-LlMbA1dC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p-LlMbA1dC-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p-LlMbA1dC-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/p-LlMbA1dC-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now compare the previous waterfall chart to the one above and you can easily spot the issue. The DCL line is covering almost 1 second on the waterfall chart! That&#39;s not a healthy looking line. In the chart you will see some unusual goings on that can help explain what is happening. The light pink line on request 4 is showing a lot of JavaScript execution is happening, and it just so happens to be the exact same width as the expanded DCL line. The browser main thread is solid yellow, which confirms that a script is parsing. The CPU graph is at around 80-90% capacity over the same time period and the page is blocked. Curiously the &quot;Bandwidth In&quot; graph is hardly registering anything, so whatever is causing the issue has already downloaded and is being parsed.&lt;/p&gt;&lt;p&gt;So what is actually happening? Well, there&#39;s some very expensive JavaScript running in the file at request 4. Often this is caused by JavaScript attached to the jQuery &lt;code&gt;$(document).ready()&lt;/code&gt; method (or some equivalent). So if you ever see a DCL line eating a large chunk of your waterfall chart, you now know where to look.&lt;/p&gt;&lt;h3 id=&quot;the-never-ending-waterfall&quot;&gt;The never-ending waterfall&lt;/h3&gt;&lt;p&gt;Occasionally if you have a third party script setup incorrectly, you can end up in a situation where a test will take a very long time to stop, as can be seen in the waterfall chart below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A third party script can keep the network open and increase the amount of requests by thousands.&quot; decoding=&quot;async&quot; height=&quot;503&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xpvmMB-lKJ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xpvmMB-lKJ-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xpvmMB-lKJ-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xpvmMB-lKJ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Yes you are seeing that correctly, 6150 requests logged by WebPageTest! So many in fact that my browser struggles to render all the DOM elements on the page. This is where the ability to customise the waterfall really helps, as you can limit the number of visible requests to a reasonable number.&lt;/p&gt;&lt;p&gt;So what exactly is happening? Well there&#39;s a clue in the network activity graph. If you look closely you will observe that it never reaches zero. WebPageTest waits for there to be &lt;a href=&quot;https://web.archive.org/web/20210125234102/https://www.webpagetest.org/forums/showthread.php?tid=13774&amp;pid=26244#pid26244&quot;&gt;no network activity 2 seconds&lt;/a&gt;, at which point it ends the test. Unfortunately this third party script is executing every 30ms (request 4) and downloading a mere 43 bytes of data each time. Just enough to keep the network alive. It&#39;s also worth noting the CPU utilisation graph, which is maxed out over the time period (120 seconds). That&#39;s what happens to a device when a script is executing 33 times per second. I&#39;ve quoted what pat said in multiple comments below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;It just uses the state of the page at the end of the test you are running and considers that the end state. The description in the SpeedIndex docs are intentionally vague because it doesn&#39;t apply just to WebPageTest page load tests and could in theory be used for arbitrary videos. Under normal conditions: after the onload event fires for the page, it waits for there to be 2 seconds of no network activity.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;You can also change it to stop at the onload event if you check the option in the advanced settings.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;There are also other settings that can cause it to wait longer (minimum test duration, waiting for a specific request, etc).&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;At 120 seconds WebPageTest automatically forces the test to stop. There are a couple of ways to force a test to stop yourself (other than fixing the issue):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;by clicking the &quot;Stop Measurement at Document Complete&quot; check box on the advanced page before you start the test, this will stop the test &lt;a href=&quot;https://web.archive.org/web/20200606170757/https://www.webpagetest.org/forums/showthread.php?tid=12394&amp;pid=21445#pid21445&quot;&gt;at the onLoad event&lt;/a&gt;&lt;/li&gt;&lt;li&gt;by using the &quot;&lt;a href=&quot;https://www.webpagetest.org/forums/showthread.php?tid=12004&amp;pid=21003#pid21003&quot;&gt;setDomElement&lt;/a&gt;&quot; script command to &lt;a href=&quot;https://web.archive.org/web/20200604223200/https://www.webpagetest.org/forums/showthread.php?tid=12874&amp;pid=23176#pid23176&quot;&gt;tell the test to wait for a specific DOM element&lt;/a&gt; to show in the DOM. I&#39;ve quoted Pat&#39;s responses from the links above, below:&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;WebPagetest will generally wait for 2 seconds of no network activity after the onload event before it considers a page done. It looks like the pins may load after the load event and are loaded by ajax so if there is a delay before that happens then the test will end before they get loaded. You can force it to wait by setting a DOM Element for it lo look for in the advanced settings (assuming the pins have a unique DOM element). In that case the test will keep running until the specified element shows up on the DOM (if there is a specific network request you can also tell it to wait for a network request instead of a DOM element).&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;WebPagetest is mostly driven by network activity (and the onload event). It doesn&#39;t know anything about your javascript running.&lt;/p&gt;&lt;p&gt;There is a &quot;setDomElement&quot; script command that you can use to tell it to wait for a specific DOM element to show up (though not sure how well that works these days - been a while since I tested it).&lt;/p&gt;&lt;p&gt;You can also use the &quot;Minimum Test Duration&quot; (time) in the advanced settings to force it to wait for say 60 seconds to see if that gives you the data you need.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So if you see unusually long tests running, check to see what your third party scripts are doing to the network graph.&lt;/p&gt;&lt;h3 id=&quot;rogue-image-downloads-http-2-with-sri&quot;&gt;Rogue image downloads (HTTP/2 with SRI)&lt;/h3&gt;&lt;p&gt;I personally ran into this issue very recently on GOV.UK. The waterfall was showing an image download that at first glance shouldn&#39;t actually be able to be downloaded:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;On request number 2, the image is downloading from the assets domain before the connection has been established.&quot; decoding=&quot;async&quot; height=&quot;326&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZIJV5Sm-YP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZIJV5Sm-YP-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZIJV5Sm-YP-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZIJV5Sm-YP-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Request number 2 shows an image downloading before the connection to the domain has been negotiated. I&#39;ve now written a whole blog post &lt;a href=&quot;https://nooshu.com/blog/2019/12/17/http2-and-sri-dont-always-get-on/&quot;&gt;&quot;HTTP/2 and Subresource Integrity don&#39;t always get on&quot;&lt;/a&gt; about this curious case if you want to delve into the details. But a quick TL;DR for you here is: the connection to the &quot;assets&quot; domain in request 2 is using &lt;a href=&quot;https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/&quot;&gt;HTTP/2 connection coalescing&lt;/a&gt;, so it is using the same TCP connection from request number 1. The requests 3-10 are waiting for an &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; connection because Subresource Integrity is enabled on the CSS and JavaScript, hence why it looks strange to be receiving a file from the domain, and making the connection at the same time.&lt;/p&gt;&lt;h3 id=&quot;requests-with-no-response-body-error-status-code-1-and-2&quot;&gt;Requests with no response body (Error/Status Code: -1 &amp; -2)&lt;/h3&gt;&lt;p&gt;There&#39;s more status codes that those mentioned in the &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-23---gif-file-moved&quot;&gt;Request 23 - GIF file moved&lt;/a&gt; section, another two available are codes &lt;code&gt;-1&lt;/code&gt; &amp; &lt;code&gt;-2&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Waterfall requests that return with no response body come back with an error code of minus one.&quot; decoding=&quot;async&quot; height=&quot;134&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/tlGT4qhlQD-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/tlGT4qhlQD-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/tlGT4qhlQD-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/tlGT4qhlQD-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice how the above image is requesting assets from the third party domain then immediately failing. It is also possible for this to happen on requests from the same domain, as seen below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Waterfall requests that return with no response body come back with an error code of minus two as well.&quot; decoding=&quot;async&quot; height=&quot;175&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ECTIE1bv-6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ECTIE1bv-6-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ECTIE1bv-6-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ECTIE1bv-6-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;When a request is made but WPT sees no response, it is given the error code of minus 1 or minus 2. This can either mean the response was never sent back by the server, or WPT simply didn&#39;t detect it. This can also be seen if you click on the individual requests:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Error/Status Code: -2&lt;/li&gt;&lt;li&gt;Content Download: 0 ms&lt;/li&gt;&lt;li&gt;Bytes In (downloaded): 0 B&lt;/li&gt;&lt;li&gt;Uncompressed Size: 0 B&lt;/li&gt;&lt;li&gt;Bytes Out (uploaded): 0.5 KB&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There a little more information from Pat in these forum posts &lt;a href=&quot;https://web.archive.org/web/20200616103523/https://www.webpagetest.org/forums/showthread.php?tid=15418&amp;pid=30957#pid30957&quot;&gt;Error code -1&lt;/a&gt; and &lt;a href=&quot;https://web.archive.org/web/20200609010209/https://www.webpagetest.org/forums/showthread.php?tid=13906&amp;pid=26755#pid26755&quot;&gt;Error code -2&lt;/a&gt; if you are interested. Pat&#39;s responses are also quoted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;-1 is a default error code for when a request is sent but there was no response (or the response wasn&#39;t detected by WPT).&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;-1 and -2 are what the error codes are initialized to and basically mean the request was never completed (there was no explicit error).&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;third-party-blocking-javascript&quot;&gt;Third party blocking JavaScript&lt;/h3&gt;&lt;p&gt;Back in the early 2010&#39;s there was a recommendation that you should use a third party CDN provider to serve common JavaScript libraries (jQuery, Backbone, Underscore etc). The theory being that as a user navigates the web, these common libraries would be cached by the browser and reused for different websites since they are coming from the same third party domain. Unfortunately things are never that simple. With multiple CDN providers, a huge number of library versions available, aggressive &lt;a href=&quot;https://www.stevesouders.com/blog/2012/03/22/cache-them-if-you-can/&quot;&gt;browser&lt;/a&gt; &lt;a href=&quot;https://engineering.fb.com/web/web-performance-cache-efficiency-exercise/&quot;&gt;caching&lt;/a&gt;, and &lt;a href=&quot;https://andydavies.me/blog/2018/09/06/safari-caching-and-3rd-party-resources/&quot;&gt;double-keyed HTTP cache&lt;/a&gt;, the theory never really panned out. You can see the effect of this externally hosted JavaScript quite clearly in some waterfalls:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A third party script can keep the network open and increase the amount of requests by thousands.&quot; decoding=&quot;async&quot; height=&quot;546&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BpVs13tAub-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BpVs13tAub-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BpVs13tAub-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/BpVs13tAub-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There&#39;s a fair amount going on in this waterfall chart. Request 13 is the important one to focus on. This is the browser requesting a version of jQuery from the &lt;a href=&quot;https://developers.google.com/speed/libraries&quot;&gt;Google Hosted Libraries&lt;/a&gt; CDN. The connection takes a while to negotiate, then the download takes a chunk of time because bandwidth is being used for other requests (11, 12, 14). Only once the script has downloaded does a whole bunch of other activity start. The JS is executed and there&#39;s a 2nd HTML parse by the browser, which suddenly triggers lots of requests for images. Notice where the &quot;Start render&quot;, &quot;RUM first paint&quot;, and &quot;Page is Interactive&quot; green graph appear on the chart. They are all being pushed back along the waterfall because of this single third party script request.&lt;/p&gt;&lt;p&gt;And that&#39;s why it is now recommended you &lt;a href=&quot;https://csswizardry.com/2019/05/self-host-your-static-assets/&quot;&gt;Self-Host Your Static Assets&lt;/a&gt;. This example was sent over by &lt;a href=&quot;https://twitter.com/mikeherchel&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Mike Herchel&lt;/a&gt; and if you are interested in what happens once you remove the third party JS, check out &lt;a href=&quot;https://twitter.com/mikeherchel/status/1205135212885225473&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;his thread here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;http-2-prioritisation&quot;&gt;HTTP/2 prioritisation&lt;/h3&gt;&lt;p&gt;HTTP/2 is different to HTTP/1.1 in the fact that it aims to use a minimal number of TCP connections (1 or 2). Whereas for &#39;optimal&#39; HTTP/1.1 performance the browser opens 6 TCP connections to a single domain, thus allowing 6 assets to be downloaded at a time, HTTP/2 is different. It introduces multiplex streams over a single TCP connection. A very simple explanation of this is:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The browser opens a TCP connection to the server and downloads the page HTML&lt;/li&gt;&lt;li&gt;Browser parses the HTML and looks for all other page assets (CSS, JS, Fonts, Images etc)&lt;/li&gt;&lt;li&gt;Browser sends a list of assets it needs to load the page to the server, and the priority it would like them in&lt;/li&gt;&lt;li&gt;It is then up to the server how it delivers these assets to the browser and in what order (ideally using the browser prioritisation as a reference). This is where HTTP/2 prioritisation comes in&lt;/li&gt;&lt;li&gt;Server then sends multiple assets at the same time via streams across a single TCP connection&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;How the assets are prioritised depends on how the server has implemented the &lt;a href=&quot;https://tools.ietf.org/html/rfc7540#page-24&quot;&gt;HTTP/2 specification&lt;/a&gt;. A lot more details about H2 server / CDN prioritisation check out Andy Davies&#39; &lt;a href=&quot;https://github.com/andydavies/http2-prioritization-issues&quot;&gt;&#39;http2-prioritization-issues&#39; &lt;/a&gt;repository which is used to track results from Pat Meenan&#39;s &lt;a href=&quot;https://github.com/pmeenan/http2priorities/tree/master/stand-alone&quot;&gt;&#39;HTTP/2 prioritization test&#39;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The test works by first warming up the TCP connection (seen in request 2 &amp; 3). &lt;a href=&quot;https://en.wikipedia.org/wiki/TCP_congestion_control#Slow_start&quot;&gt;TCP slow start&lt;/a&gt; is part of the congestion control strategy used by the TCP protocol. It is used to stop the server flooding the TCP connection. The server gradually ramps up the speed, while looking to see if any packets are lost along the way. If packets are lost, it drops the speed.&lt;/p&gt;&lt;p&gt;Once the connection is up to speed the browser then requests 30 images concurrantly at a low priority (request 4 to 33). When two low priority images have downloaded (and therefore the network buffers are filled), then it requests a high priority image. The priority of the image is sent to the server along with the request (if the browser supports it), and is decided by the browser using the rules set out in the &lt;a href=&quot;https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit&quot;&gt;&#39;Resource Fetch Prioritization and Scheduling in Chromium&#39;&lt;/a&gt; document. Looking at this document we can see that in Chrome an &#39;Image (in viewport)&#39; in the &#39;Load in layout-blocking phase&#39; is given a &#39;High&#39; priority. Once one high priority image has downloaded, then the browser requests another.&lt;/p&gt;&lt;p&gt;The test has been designed to see how the server copes with H2 prioritisation when it is given a higher priority file to send, while already sending low priority assets. A server that prioritises correctly should divert resources from low priority assets, to high priority assets (so the high priority assets are loaded first).&lt;/p&gt;&lt;p&gt;So let&#39;s take a look at a waterfall from good and bad prioritisations, and discuss what is happening:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Good - Fastly&lt;/strong&gt; &lt;img alt=&quot;In the waterfall we can see the Fastly CDN reprioritizing the stream when the browser notifies it of high priority images (request 34 and 35).&quot; decoding=&quot;async&quot; height=&quot;563&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-Rgcw1LB5O-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-Rgcw1LB5O-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-Rgcw1LB5O-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-Rgcw1LB5O-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So what exactly is happening in this waterfall? Requests 2 and 3 are warming up the TCP connection. Requests 4 - 33 are images requested with a priority of &#39;lowest&#39;. Keep in mind that the lower the request number, the faster it was discovered and requested by the browser, e.g. image at request number 4 was seen, and requested before the image at request 20. If the server didn&#39;t bother with any form of prioritisation and simply sent images on a first come, first served basis, images later in the waterfall would &lt;em&gt;always&lt;/em&gt; be delivered last, since they would always be at the back of the queue. But what we are actually seeing is the server sees a request for the high priority images and it stops streaming the &#39;lowest&#39; priority data in favour of &#39;high&#39; priority data, so the high priority images are completed sooner.&lt;/p&gt;&lt;p&gt;Now if you compare this to a CDN with poor HTTP/2 prioritisation:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Bad - Level 3&lt;/strong&gt; &lt;img alt=&quot;In the waterfall we see all the lowest priority images download first, before high priority images are downloaded (request 34 and 35).&quot; decoding=&quot;async&quot; height=&quot;563&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/WeQ0jRq70h-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/WeQ0jRq70h-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/WeQ0jRq70h-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/WeQ0jRq70h-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we can see the server essentially ignoring the information it&#39;s been given in the request about the high priority images. The two high priority images are added to the end of the download queue. The server continues to send low priority data and makes no allowances for the fact that by doing so, it is delivering a suboptimal experience to the user. You can actually see this in the waterfall by looking at the green &#39;start render&#39; line: for good prioritisation this is at approximatly 9 seconds, for bad prioritisation it is almost 21 seconds!&lt;/p&gt;&lt;p&gt;For more information on H2 prioritisation and many other H2 topics check out &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&#39;s&lt;/a&gt; book &lt;a href=&quot;https://www.manning.com/books/http2-in-action&quot;&gt;&#39;HTTP/2 in Action&#39;&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;large-time-to-first-byte-ttfb&quot;&gt;Large Time to First Byte (TTFB)&lt;/h3&gt;&lt;p&gt;Time to First Byte is the time it takes from the browser requesting the first asset, to the time at which the server sends back the first byte of data to the browser. It consists of:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Browser requesting the asset from the server (after DNS + Connect + SSL)&lt;/li&gt;&lt;li&gt;Time taken for the packets to travel from the browser to the server&lt;/li&gt;&lt;li&gt;Server recieves the request. It then processes and constructs the response and transmits it back&lt;/li&gt;&lt;li&gt;Time taken for the response packets to travel from the server to the browser&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The time taken to travel from the browser to the server is known as &lt;a href=&quot;https://hpbn.co/primer-on-latency-and-bandwidth/#the-many-components-of-latency&quot;&gt;network latency&lt;/a&gt;. A data travelling there and back again is known as a Round Trip (RTT). So how do you use WebPageTest to identify when you have a large TTFB?&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the graph the TTFB is large, at 3.1 seconds. With very little happening on the CPU graph, and nothing happening on the bandwidth graph.&quot; decoding=&quot;async&quot; height=&quot;297&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_jJ0Nf93_5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_jJ0Nf93_5-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_jJ0Nf93_5-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_jJ0Nf93_5-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The above test was run on a standard &#39;Cable&#39; connection as &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/blob/master/www/settings/connectivity.ini.sample#L1&quot;&gt;defined by WebPageTest&lt;/a&gt;, so the connection has a RTT of 28ms. Now if you compare this value to the TTFB, which is 3.1 seconds, we can see there&#39;s an issue here. By looking at the connect negotiation time (in orange on request 3), this gives you an indication of how quickly packets can travel across the network (32 ms in this case). So it&#39;s clear to see that the TTFB delay isn&#39;t caused by congestion over the network. There is zero activity over the network according to the bandwidth graph, and fairly minimal activity happening on the CPU. To the device is waiting on the server to respond before it can do anything else.&lt;/p&gt;&lt;p&gt;In this case it&#39;s the processing time on the server that is causing the delay. Whatever the server is doing to construct the response is taking approximatly 3 seconds. That&#39;s a huge amount of time to construct the HTML. There are far too many reasons to list as to why this could be happening on the server, but a good starting place would be to look at the databases queries, hosting settings, server resources available, and the server software that is running. Whatever is causing the issue needs to be identified and fixed, as this will be having a huge impact on the sites users. So if you see a WebPageTest waterfall that looks like this, examine your server setup and try to reduce this time. As &lt;a href=&quot;https://twitter.com/csswizardry&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Harry Roberts&lt;/a&gt; mentions in his article &lt;a href=&quot;https://csswizardry.com/2019/08/time-to-first-byte-what-it-is-and-why-it-matters/&quot;&gt;&#39;Time to First Byte: What It Is and Why It Matters&#39;&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;While a good TTFB doesn&#39;t necessarily mean you will have a fast website, a bad TTFB almost certainly guarantees a slow one.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;inactivity-after-ssl-negotiation&quot;&gt;Inactivity after SSL negotiation&lt;/h3&gt;&lt;p&gt;This is quite an unusual waterfall. We see a large Time to First Byte on request 3, but we see a huge gap with apparently no activity at all:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An unusual waterfall in the fact that there is a huge gap between the initial SSL negotiation and the HTML request.&quot; decoding=&quot;async&quot; height=&quot;269&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/07SKNcEqp2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/07SKNcEqp2-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/07SKNcEqp2-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/07SKNcEqp2-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;What is interesting about this waterfall is we can see the DNS + connect + SSL negotiation happen very quickly (34ms + 33ms + 80ms respectivly), then there&#39;s zero activity on the waterfall, CPU and bandwidth graphs over this time period. This indicates that the device is idle and waiting on work to do. Right towards the end of this period of inactivity we see the browser instigate two OCSP revocation checks. But according to the waterfall the SSL negotiation has completed by this point in time.&lt;/p&gt;&lt;p&gt;I can&#39;t be 100% sure why this is happening (it&#39;s an old test with no tcpdump log to examine the network activity), but if I were to guess I&#39;d say that there is something unusual happening with the sites certificate. It could be there&#39;s an additional OCSP check happening that isn&#39;t being displayed on the waterfall, or maybe the SSL negotiation hasn&#39;t completed properly and the browser is trying to recover from the error. But given the fact that nothing is happening on either the CPU graph or the bandwidth graph, whatever is happening isn&#39;t very work intensive on the device. It is worth noting that the site currently uses an Extended Validation (EV) certificate that Chrome will always conduct an OCSP check on. If anyone has any other ideas about what is happening in this waterfall, I&#39;d love to hear them, so &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;missing-component-times-dns-connect-tls&quot;&gt;Missing component times (DNS, Connect, TLS)&lt;/h3&gt;&lt;p&gt;This should no longer be an issue on WebPageTest, but it is worth pointing out if you ever look at waterfall test results from iOS devices pre-iOS 10. iOS versions before 10 didn&#39;t report any of the connection component times back to WebPageTest. This was a limitation of what iOS reported through the Safari developer tools interface. Heres a test from 2016 on an iPhone 5c running iOS 9.3:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Older versions of iOS didn&#39;t report DNS, Connect and SSL times back to WebPageTest.&quot; decoding=&quot;async&quot; height=&quot;106&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-8OJhtaLnZ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-8OJhtaLnZ-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-8OJhtaLnZ-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-8OJhtaLnZ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see the information about DNS, Connect, and SSL are all missing from the waterfall. Note that although not reported individually, the times are included in the TTFB metric so it is still correct. Now compare this to a test run in 2020 on an iPhone 5c running iOS 10.3 (the &lt;a href=&quot;https://en.wikipedia.org/wiki/IOS_version_history#10.3.3&quot;&gt;last version supported&lt;/a&gt; on this device):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The iPhone 5c on 10.3 now reports the individual component times.&quot; decoding=&quot;async&quot; height=&quot;174&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kZnauK5EtX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kZnauK5EtX-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kZnauK5EtX-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kZnauK5EtX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;All iOS devices currently running on WebPageTest report the DNS, Connect, and TLS times correctly. All of them are running iOS &gt;= 10.3. NOTE: There currently (Jan 2020) seems to be a rendering issue on WPT with the &lt;a href=&quot;https://www.webpagetest.org/result/200112_ND_8627ab7bab88a1fd453c587256648251/&quot;&gt;iPhone 6&lt;/a&gt; and &lt;a href=&quot;https://www.webpagetest.org/result/200112_74_3c072e15f3ff7efb4c4552366461744e/&quot;&gt;iPad Mini 2&lt;/a&gt; that was introduced in iOS 12.2, whereby the test is rendered midway through the waterfall chart. But by looking at the two tests listed it is possible to see some component connection information rendered. So at least that isn&#39;t broken...&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Jan 2021 update&lt;/strong&gt;: So iOS 14 fixed a few issues with the test runs, but has now introduced more. At the time of this update DNS / Connect / SSL information isn&#39;t seen on real or simulated iOS devices for the first connection. But it will be there for later connections. In summary, Safari is a bit of a mess, so you may or may not get connection timing information!&lt;/p&gt;&lt;h3 id=&quot;the-impact-of-a-cpu-bottleneck&quot;&gt;The impact of a CPU bottleneck&lt;/h3&gt;&lt;p&gt;This is an old test but it gives you an example as to what can happen once the CPU becomes a bottleneck. You can see the &lt;a href=&quot;https://www.webpagetest.org/result/140929_9M_13KR/1/details/&quot;&gt;full WebPageTest run here&lt;/a&gt;. It is filled with lots of sporadic short bursts of activity across the whole page load. I&#39;ve focussed on the first 20 requests in the diagram below as the full chart is far too long:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the waterfall we can see connections fully negotiated then activity simply stops. Looking at the bandwidth graph we see very little network activity, but the CPU activity is at 100%. This is killing all network activity.&quot; decoding=&quot;async&quot; height=&quot;435&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rQHdZ9Bm7p-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rQHdZ9Bm7p-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rQHdZ9Bm7p-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/rQHdZ9Bm7p-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see from the graph, there are multiple TCP connections negotiated in requests 2-5, 8-10. At the same time take a look at the bandwidth graph, it starts to increase then suddenly drops out. If you look at the CPU graph you can see it jump from 10% to 100% in ~200ms at around the same time, then it stays at pretty much 100% for the rest of the page load. This CPU usage is stopping all other process&#39; from doing their work. You can read the full response as to what exactly is happening in &lt;a href=&quot;https://web.archive.org/web/20200608091746/https://www.webpagetest.org/forums/showthread.php?tid=12970&amp;pid=24770&quot;&gt;this forum post&lt;/a&gt;, but a TL;DR is: a combination of multiple SSL negotiations at the same time (they are CPU resource intensive) and heavy CPU usage caused by the parsing of applications JavaScript. Both combined with a testing machine that is underpowered leads to waterfalls that look like the one &lt;a href=&quot;https://www.webpagetest.org/result/140929_9M_13KR/1/details/&quot;&gt;seen here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The relevant forum information is quoted below:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;The OCSP stapling happens at the CloudFront edge level so every node from an edge location needs to do it. The first request returns immediately a non-stapled answer and the node fires an OCSP request to our CA and then caches the answer which is then used for the subsequent requests hitting that edge node. (The nodes currently don&#39;t share these caches, but I filed a feature request so that they try to use a shared storage for that content, like they do for static content.)&lt;/p&gt;&lt;p&gt;During our test we only executed a relatively small number of requests, so we never hit the same edge node twice and that&#39;s why it appeared the stapling was broken. When testing you need to fire a few hundreds of requests until consistently getting stapled responses.&lt;/p&gt;&lt;p&gt;As for explaining the rest of the gap from our waterfall graph, we saw that it often happens that the CPU is maxed out while loading our app, for multiple reasons:&lt;/p&gt;&lt;p&gt;sometimes due to multiple SSL connections started at the same time, since they are quite heavy during the negotiation phase while other gaps can be explained due to heavy CPU use during the parsing of Javascript and CSS content (our webapp code is around 400K when minified and compressed)&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;When these overlap, the gap can grow even up to a few seconds, and it&#39;s exacerbated if the testing machine is not powerful enough.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;misconfigured-cors-headers&quot;&gt;Misconfigured CORS headers&lt;/h3&gt;&lt;p&gt;I originlly wrote about this issue in my blog post on the Web Performance Calendar 2020: &#39;&lt;a href=&quot;https://calendar.perfplanet.com/2020/a-font-display-setting-for-slow-connections/&quot;&gt;A font-display setting for slow connections&lt;/a&gt;&#39;. In the example below a connection is used that is severly limited, only 600 Kbps max bandwidth in.&lt;/p&gt;&lt;p&gt;Fonts on a separate domain require an &#39;anonymous&#39; CORS connection when being fetched. You can find out more about this font Fetch requirement in the &lt;a href=&quot;https://www.w3.org/TR/css-fonts-4/#font-fetching-requirements&quot;&gt;CSS Fonts specification&lt;/a&gt;. So when you fonts are served on the other domain, you should also inclide the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header, either set to the origin domain the HTML is served from, or the wildcard value &lt;code&gt;*&lt;/code&gt;. This is basically saying that the response (e.g. the font) can be shared with the origin domain.&lt;/p&gt;&lt;p&gt;So what happens if this header isn&#39;t set? Well these missing headers trigger the browser to send &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests&quot;&gt;preflight requests&lt;/a&gt; for each of the fonts being served. Preflight requests are very small &lt;code&gt;OPTIONS&lt;/code&gt; method requests of approximately 2 KB in size. The preflights allow the server to see details of the font request before it decides if the browser should send the request for the actual fonts. In a case for the fonts the browser is essentially asking the server permission to be allowed to send the actual font &lt;strong&gt;requests&lt;/strong&gt;. These preflight requests can be seen in the waterfall below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Explanation as to what is happening with the preflights in the waterfall chart.&quot; decoding=&quot;async&quot; height=&quot;350&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/jTwNcYZ80S-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/jTwNcYZ80S-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/jTwNcYZ80S-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/jTwNcYZ80S-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There&#39;s a lot going on in the waterfall above, so let&#39;s step through it. Requests 15-18 are the preflights sent from the browser to the server. Due to the limited (and maxed out) bandwidth these take 8 seconds to complete. What makes matters worse is there&#39;s a whole TCP connection negotiation setup in there too. Only when the browser has received permission back from the server will it send the actual font requests. These then take another 2-3 seconds to completely download. During this time the browser is almost idle, waiting for something to do.&lt;/p&gt;&lt;p&gt;To summarise, misconfigured CORS headers could be effectivly adding a large TTFB to your font requests under certain conditions.&lt;/p&gt;&lt;h3 id=&quot;consequences-of-a-301-redirect-when-changing-a-url&quot;&gt;Consequences of a 301 redirect when changing a URL&lt;/h3&gt;&lt;p&gt;You sometimes come aross sites that like to either remove the &lt;code&gt;www&lt;/code&gt; from the start of their URL, or others like to add it back in. It&#39;s easy to spot this on a WebPageTest waterfall chart as it usually happens in the top few requests, highlighted in yellow with a status code after the total time of the request (see the section above &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#request-23---gif-file-moved&quot;&gt;&#39;Request 23 - GIF file moved&#39;&lt;/a&gt; for more information). Below you can see the impact this can have on a waterfall chart for &lt;a href=&quot;https://twitter.com/&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Twitter.com&lt;/a&gt;. If it is present, Twitter will remove the &lt;code&gt;www&lt;/code&gt; from the URL when you hit the site. Both tests were using Chrome on a 4G connection:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;With WWW&lt;/strong&gt; &lt;img alt=&quot;The user has entered &#39;www.twitter.com&#39;, in response the browser gets a 301 redirect to remove the &#39;www&#39;, then continue as normal.&quot; decoding=&quot;async&quot; height=&quot;217&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/IE64ziy8WK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/IE64ziy8WK-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/IE64ziy8WK-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/IE64ziy8WK-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above waterfall the user has requested &#39;https://www.twitter.com&#39;. Request number 1 is the browser requesting &#39;www.twitter.com&#39;, the Twitter server responds back and tells the browser to remove the &#39;www&#39; and then try again. With the connection already established, the HTML is requested as soon as it recieves the &lt;code&gt;301&lt;/code&gt; redirect. Notice the times highlighted in the image:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;HTML requested at 800 ms&lt;/li&gt;&lt;li&gt;RUM First Paint at 1165 ms&lt;/li&gt;&lt;li&gt;Start Render at 1200 ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So what does a waterfall look like when a user enters &#39;https://twitter.com&#39; directly into the browser address bar (or it is auto-completed)?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Without WWW&lt;/strong&gt; &lt;img alt=&quot;The user enters the URL as &#39;twitter.com&#39;, so no 301 redirect can be seen and the HTML downloads much quicker.&quot; decoding=&quot;async&quot; height=&quot;217&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5B_5wGPaB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5B_5wGPaB-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5B_5wGPaB-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5B_5wGPaB-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;When the user enters the URL that the server agrees not to modify, no &lt;code&gt;301&lt;/code&gt; redirect can be seen in the waterfall chart. Request number 1 follows the usual connection process (DNS + Connect + SSL). Because there&#39;s no wait time for the server to respond with a &lt;code&gt;301&lt;/code&gt;, this has quite an impact on the web performance metrics:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;HTML requested at 536 ms (264 ms quicker)&lt;/li&gt;&lt;li&gt;RUM First Paint at 900 ms (265 ms quicker)&lt;/li&gt;&lt;li&gt;Start Render at 1000 ms (200 ms quicker)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It just goes to show, there is a performance impact associated with &lt;code&gt;301&lt;/code&gt; redirects, so keep that in mind when using them.&lt;/p&gt;&lt;p&gt;The above waterfall charts are testing using a modern browser (Chrome). Let&#39;s see what the same waterfall chart looks like in a legacy browser. Things don&#39;t look quite as performant:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;IE10 does a full connection negotiation for both the &#39;www&#39; and the &#39;non-www&#39; domains. Mix that together with a redirect and a OCSP revocation check and you have a pretty poor Time to First Byte (TTFB).&quot; decoding=&quot;async&quot; height=&quot;245&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/iwMJ9YHTCd-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/iwMJ9YHTCd-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/iwMJ9YHTCd-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/iwMJ9YHTCd-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Here we see IE10 on a 4G connection. There&#39;s a whole bunch going on with request number 3. First we have a full connection negotiation (DNS + Connect + SSL), then we have a pause for the &lt;a href=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/&quot;&gt;OCSP revocation check&lt;/a&gt; (requests 1 &amp; 2), the browser then waits for a response back from the server to remove the &#39;www&#39;. Then quite unexpectedly (to me) it does &lt;em&gt;another&lt;/em&gt; full connection negotiation, this time to &#39;https://twitter.com&#39;! The browser seems to see &#39;www.twitter.com&#39; and &#39;twitter.com&#39; as totally different websites. Not very optimal for web performance. Thankfully IE10 usage is now pretty much non-existant and the issue looks to have been &lt;a href=&quot;https://www.webpagetest.org/result/200119_33_4bd04f8e34362ae6a04eca98ffb98c9e/1/details/#waterfall_view_step1&quot;&gt;fixed in IE11&lt;/a&gt;!&lt;/p&gt;&lt;h3 id=&quot;the-missing-intermediate-certificate&quot;&gt;The missing intermediate certificate&lt;/h3&gt;&lt;p&gt;How can you tell when one of your intermediate certificates from your &lt;a href=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/#the-chain-of-trust&quot;&gt;Chain of Trust&lt;/a&gt; is missing? Well, assuming your SSL certificate contains &lt;a href=&quot;http://www.pkiglobe.org/auth_info_access.html&quot;&gt;Authority Information Access (AIA)&lt;/a&gt; information, you will be able to tell from a waterfall chart:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;If the intermediate certificate is missing from the chain of trust, the browser will attempt to go and fetch it before the SSL negotiation can complete. This can be seen on request number 1, with a file request for a *.crt file.&quot; decoding=&quot;async&quot; height=&quot;253&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/uPRA8xX8F9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/uPRA8xX8F9-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/uPRA8xX8F9-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/uPRA8xX8F9-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Usually you would host the intermediate certificate along with your leaf certificate. But if for some reason you forget, the browser will attempt to retrieve the missing certificate from the information provided in the sites leaf certificate. As you might expect this isn&#39;t great for security or web performance. If the certificate can&#39;t be retrieved, then the browser may need to &#39;soft-fail&#39; and just assume that the leaf certificate is legitimate. Also, while the intermediate certificate is being retrieved (a process that is synchronous and blocking) the SSL negotiation can&#39;t complete. During this period the user sees a blank page. So remember, always check that your sites certificate chain is complete using a tool like &lt;a href=&quot;https://www.ssllabs.com/ssltest/&quot;&gt;SSLLabs&lt;/a&gt;. If you want to know more about certificate revocation in browsers, I&#39;ve written a &lt;a href=&quot;https://nooshu.com/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/&quot;&gt;blog post here&lt;/a&gt; that you may find interesting.&lt;/p&gt;&lt;h3 id=&quot;a-vary-unusual-waterfall-empty-gaps-between-download-data&quot;&gt;A &lt;code&gt;vary&lt;/code&gt; unusual waterfall (empty gaps between download data)&lt;/h3&gt;&lt;p&gt;This was such an interesting and frustraiting issue, I&#39;ve actually written a blog post all about it which you can &lt;a href=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/&quot;&gt;find here&lt;/a&gt;. But a TL;DR; version is: notice how the requests 6 and 7 have a weird set of gaps between the data chunks:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Annotated waterfall, with exactly what is happening (see following paragraph)&quot; decoding=&quot;async&quot; height=&quot;146&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/slMhwE_e_c-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/slMhwE_e_c-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/slMhwE_e_c-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/slMhwE_e_c-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Esentially what is happening is the browser is recieving data from a CDN that has been poisoned (it is missing the CORS headers needed for the font fetch). Lets step through the waterfall:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;First the WOFF2 font files are requested by the browser after a new TCP connection is established&lt;/li&gt;&lt;li&gt;A very thin slither of data is received (most likely the headers)&lt;/li&gt;&lt;li&gt;The browser immediately cancels the request as there are no required CORS headers. This explains the &#39;empty space&#39; between the data.&lt;/li&gt;&lt;li&gt;The browser immediately requests the next fonts in the &lt;code&gt;@font-face&lt;/code&gt; declaration (requests 8 &amp; 9)&lt;/li&gt;&lt;li&gt;The browser still receives chunks of WOFF2 data because this data was already in transit over the network or sitting in one of many buffers along the way.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;And what&#39;s the solution to this issue? Make sure you are &lt;a href=&quot;https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches&quot;&gt;serving &lt;code&gt;vary: Origin&lt;/code&gt; along with your font responses&lt;/a&gt;. Simple fix when you know that huh!&lt;/p&gt;&lt;h3 id=&quot;examining-undefined-204-status-codes&quot;&gt;Examining undefined 204 status codes&lt;/h3&gt;&lt;p&gt;In this chart we see multiple requests going out to the server, but as you can see the responses back are quite unusual. The colours for requests 14, 16-21 are all grey indicating they are of the type &#39;other&#39;. Clicking on each of the requests shows &lt;code&gt;Error/Status Code: 204&lt;/code&gt; and the bytes coming in were 0. Looking up the status code &lt;code&gt;204&lt;/code&gt; details, that makes sense. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204&quot;&gt;Status code 204&lt;/a&gt; indicates that there&#39;s no content. The request has been successfully completed by the server and there&#39;s no aditional content to send in the response payload body. In fact if you &lt;a href=&quot;https://tools.ietf.org/html/rfc7231#section-6.3.5&quot;&gt;read the specification&lt;/a&gt; a 204 response cannot have a response body. The response terminates once the first empty line is recieved after the headers.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Waterfall showing undefined requests to the server that are recieving a 204 status code. These are icon requests that are failing because they aren&#39;t on the server.&quot; decoding=&quot;async&quot; height=&quot;423&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_wAqZJ4xtU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_wAqZJ4xtU-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_wAqZJ4xtU-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_wAqZJ4xtU-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So what&#39;s happening here? In hindsight, once you know the answer it&#39;s quite obvious, but very few clues are actually given in the waterfall responses, so it can be very frustraiting to debug. Examining the &#39;start&#39; and &#39;end&#39; responses that are top and bottom of the &#39;undefined&#39; responses, shows the browser requesting icons and the server is responding accordingly. But the icons being requested between the successful responses are coming back as &#39;undefined&#39;, as it could be that the icons don&#39;t exist on the server. Now I have struggled to reproduce this waterfall in my own tests. I can get a &lt;code&gt;404&lt;/code&gt; response by removing an icons &lt;code&gt;src&lt;/code&gt; key in the &lt;code&gt;manifest.json&lt;/code&gt; file, and a &lt;code&gt;304&lt;/code&gt; response by setting the &lt;code&gt;src&lt;/code&gt; to &lt;code&gt;&quot;&quot;&lt;/code&gt; or &lt;code&gt;&quot;/&quot;&lt;/code&gt;. But a &lt;code&gt;204&lt;/code&gt; is something I haven&#39;t managed. So &lt;em&gt;why&lt;/em&gt; exactly this particular server setup responded with a &lt;code&gt;204&lt;/code&gt; rather than a &lt;code&gt;404&lt;/code&gt; is a mystery to me. Any thoughts or ideas please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;tweet me&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;So if you see this pattern in one of your waterfalls make sure your icons are all referenced correctly and exist on the server.&lt;/p&gt;&lt;h3 id=&quot;service-worker-precache-slowing-a-page-load&quot;&gt;Service Worker precache slowing a page load&lt;/h3&gt;&lt;p&gt;Sometimes a Service Worker can impact a pages web performance and actually make it slower. Service workers have the ability to precache assets to make future navigations more performant, or allow a website to work offline. But you need to be careful where this precaching occurs in the standard page load process. In the waterfall below we can see the service worker registering too soon and instructing the browser to load additional assets into the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Cache&quot;&gt;Cache API&lt;/a&gt;. This is a different cache compared to the standard HTTP Cache.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Waterfall showing what can happen when a service worker isn&#39;t loaded at the optimal time, leading to page assets competing for resources.&quot; decoding=&quot;async&quot; height=&quot;779&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nXY7YtSy5a-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nXY7YtSy5a-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nXY7YtSy5a-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nXY7YtSy5a-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Lets step through these requests as there&#39;s a lot going on in this waterfall:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Requests 1-13 are initialised by the main HTML page (text in black).&lt;/li&gt;&lt;li&gt;Requests 14-15 are downloading the pages service worker (text in blue). Once registered, the service worker queue&#39;s up additional assets to be downloaded (21, 23, 25-28, 37-41).&lt;/li&gt;&lt;li&gt;Notice how request 38 is downloading at the same time as the original page assets in requests 42-49. These assets are now competing for the limited device resources.&lt;/li&gt;&lt;li&gt;Between 2.4 - 2.6 seconds the device is both CPU and bandwidth limited. The script from row 38 is causing huge amounts of script execution, as seen in the &#39;Browser Main Thread&#39; graph at the bottom.&lt;/li&gt;&lt;li&gt;Notice how request 3 and request 38 have identical filenames and similar JavaScript execution patterns. This is the same file being downloaded once into the HTTP Cache and then again into the Cache API.&lt;/li&gt;&lt;li&gt;There are even more request rows after 54, so even at this point the page has yet to complete loading.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The fact that the pages service worker has fired too early has caused the browser to reprioritise its asset download queue in such a way that it will slow down a page load due to limited bandwidth and CPU time. The service worker assets are stealing resources from page assets before the page has finished downloading!&lt;/p&gt;&lt;p&gt;So what&#39;s the fix for this issue? Well it&#39;s actually quite simple and is mentioned in this &lt;a href=&quot;https://developers.google.com/web/fundamentals/primers/service-workers/registration&quot;&gt;service worker registration primer&lt;/a&gt;. Wrap your service worker registration code in an event listener and delay registration until after the pages load event fires. This delay allows the pages assets to download, which should allow the page to (hopefully) render quickly for a user. At this point the user can start interacting with the page and the service worker can register and precache any assets in the background.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;serviceWorker&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;load&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceWorker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/service-worker.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In regards to the repeated download into two sets of caches (request 3 and request 38), if you are using &lt;a href=&quot;https://developers.google.com/web/tools/workbox&quot;&gt;Workbox&lt;/a&gt;, check out the &lt;code&gt;ignoreURLParametersMatching&lt;/code&gt; setting on &lt;a href=&quot;https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build&quot;&gt;this page&lt;/a&gt;. The option lets you configure a pattern for hashed files that can safely be served from the HTTP cache (if present), which lets you avoid double-downloading them. Thanks &lt;a href=&quot;https://twitter.com/jeffposnick&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Jeff Posnick&lt;/a&gt; for the tip!&lt;/p&gt;&lt;p&gt;Credit for bringing this waterfall to my attention goes to &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;, who recently spoke about this issue. Slides for his talk can be &lt;a href=&quot;https://noti.st/andydavies/Tw5Q3B&quot;&gt;seen here&lt;/a&gt; (slide 50 to 62). I hear there&#39;s a whole blog post all about this issue in the works too, so watch out for that.&lt;/p&gt;&lt;h3 id=&quot;http-1-1-and-the-connection-close-header&quot;&gt;HTTP/1.1 and the &lt;code&gt;Connection: close&lt;/code&gt; header&lt;/h3&gt;&lt;p&gt;When is HTTP/1.1 actually HTTP/1.0? Answer: When you use it in conjunction with the &lt;code&gt;Connection: close&lt;/code&gt; response header. One of the major features added to HTTP/1.0 was the ability to persist connections across asset downloads. Under HTTP/1.0 an asset would download then at completion the TCP connection would be closed. At this point a whole new TCP connection would need to be negotiated before the next asset could be downloaded. What a waste of resources for both client and server! TCP connections are expensive! Lets look for the tell-tail signs of this configuration in a waterfall chart:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;HTTP/1.1 + &lt;code&gt;Connection: close&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;This waterfall chart shows 9 TCP connections for 10 page requests.&quot; decoding=&quot;async&quot; height=&quot;257&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/MPxer7cTTT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/MPxer7cTTT-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/MPxer7cTTT-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/MPxer7cTTT-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example waterfall above we see a total of 10 requests over a HTTP/1.1 connection, but almost every one of these requires its own brand new TCP connection. NOTE: The fonts were on a separate domain with HTTP/2 enabled, hence a persistant connection across 2 fonts. Now this example is from a really simple site with a small number of page assets. Imagine if a page load required 74 requests. Why 74? Well that&#39;s what the median web page requires in the &lt;a href=&quot;https://almanac.httparchive.org/en/2019/page-weight#introduction&quot;&gt;2019 Web Almanac&lt;/a&gt;. Under these conditions you&#39;d have 74 TCP connection negotiations! Each of those includes multiple RTT&#39;s for the connection and TLS negotiations. That&#39;s a lot of wasted network packets and CPU time on the device and server.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;HTTP/1.1 + &lt;code&gt;Connection: keep-alive&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;This waterfall chart shows 4 TCP connections for 10 page requests.&quot; decoding=&quot;async&quot; height=&quot;257&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-yz0Tp0zpr-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-yz0Tp0zpr-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-yz0Tp0zpr-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/-yz0Tp0zpr-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now if you compare the first waterfall to the one above. It&#39;s the exact same page only with the &lt;code&gt;Connection: keep-alive&lt;/code&gt; response header used instead. Here you can see the persistant TCP connections have done their job. Multiple requests coming down the same connection (one after the other) so only 4 are required. 2 of those TCP connections seen are for the font requests which are on a separate domain. So it&#39;s actually only 2 TCP connections working in parallel that are required to download all the other page assets.&lt;/p&gt;&lt;p&gt;Note that the default behavior for HTTP/1.1 is to have &lt;a href=&quot;https://tools.ietf.org/html/rfc7230#section-6.3&quot;&gt;persistant connections enabled&lt;/a&gt;, so something has to actively add the &lt;code&gt;Connection: close&lt;/code&gt; response header for the above scenario to happen at all. I actually &lt;a href=&quot;https://nooshu.com/blog/2020/11/01/fixing-a-legacy-http11-server-configuration-with-cloudflare-workers/&quot;&gt;have a separate blog post&lt;/a&gt; all about this scenario for those who are interested in finding out more information.&lt;/p&gt;&lt;h3 id=&quot;content-autofill-on-mobile-devices&quot;&gt;Content autofill on mobile devices&lt;/h3&gt;&lt;p&gt;Here&#39;s a single request that I bumped into by chance after conducting some mobile testing using the default Chrome browser on the Moto G4 devices that sit in Pat&#39;s basement in Dulles. On request 23 you will see a request that goes out to &lt;code&gt;content-autofill.googleapis.com&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;This waterfall chart shows 4 TCP connections for 10 page requests.&quot; decoding=&quot;async&quot; height=&quot;284&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/A-WYQI6ck5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/A-WYQI6ck5-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/A-WYQI6ck5-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/A-WYQI6ck5-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;So what is this request? Well Autofill is functionality that has been in Chrome &lt;a href=&quot;https://developers.google.com/web/updates/2015/06/checkout-faster-with-autofill&quot;&gt;since mid-2015&lt;/a&gt;. It&#39;s there to make filling in forms easier on mobile devices. Chrome will fire off a request to the Google servers and request relevant information needed in the form that is stored in a users Google Play Store account. Depending on the page markup, the browser can then autofill this information for the user, meaning less typing and quicker purchases.&lt;/p&gt;&lt;p&gt;It&#39;s worth noting, from the waterfall you can see that this functionality has been built to have no impact on a web pages performance. The request is only &#39;created&#39; after the DOM Content Loaded event fires (which makes sense as the browser will need to know if a form element exists on the page). The actual connection to the Google Servers only starts at the time the the Document Complete (onload) event fires. An observant reader may also notice that there&#39;s no DNS lookup time for this connection, even though it is to a third-party. My guess here is that the device has the IP address for this domain already stored internally, so there&#39;s no need for it to be looked up by the browser.&lt;/p&gt;&lt;h3 id=&quot;identifying-a-cross-origin-resource-sharing-cors-connection&quot;&gt;Identifying a Cross-Origin Resource Sharing (CORS) connection&lt;/h3&gt;&lt;p&gt;So if there&#39;s one thing we can probably all agree on it&#39;s that literally nobody understands &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&quot;&gt;CORS&lt;/a&gt;. Given that, at some point while examining a waterfall chart you may want to know when a CORS connection is being used. So how would you go about this in WebPageTest? The first thing you can examine is the &lt;code&gt;sec-fetch-mode&lt;/code&gt; HTTP Request Header, which you can find under the &#39;Request&#39; tab when you click on an individual request as seen below:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Clicking on a request and looking under the request tab will give you the values of the sec-fetch-mode header.&quot; decoding=&quot;async&quot; height=&quot;656&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/izal29FX_d-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/izal29FX_d-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/izal29FX_d-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/izal29FX_d-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If &lt;code&gt;sec-fetch-mode: cors&lt;/code&gt; is seen this may indicate a CORS connection is being used. There&#39;s also the &lt;code&gt;sec-fetch-site: cross-site&lt;/code&gt; header which also indicates that this may be a CORS connection. This header (that the browser sets) is telling the server hosting the font to handle this request as a &lt;code&gt;cross-site&lt;/code&gt; request, so handle it with care (due to possible security concerns). Notice how I have also highlighted the separate TCP connection that is being negotiated for this request. This &lt;em&gt;could&lt;/em&gt; also be an indicator that a CORS connection is being used (but not always).&lt;/p&gt;&lt;p&gt;Again using GOV.UK as an example but with the font now hosted on the same domain as the origin using HTTP/2, no separate TCP connection is seen but &lt;code&gt;sec-fetch-mode: cors&lt;/code&gt; is still set.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;A font coming from the same domain as the origin is CORS but no separate TCP connection.&quot; decoding=&quot;async&quot; height=&quot;591&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YWW2yD-CHs-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YWW2yD-CHs-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YWW2yD-CHs-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/YWW2yD-CHs-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above example we know for sure that this asset is being downloaded using CORS because it is a requirement when fetching fonts that is defined in the &lt;a href=&quot;https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements&quot;&gt;CSS Fonts Module Level 3 specification&lt;/a&gt;. The key difference here is the &lt;code&gt;sec-fetch-site&lt;/code&gt; request header. Instead of the browser attaching a &lt;code&gt;cross-site&lt;/code&gt; value, it is attaching a &lt;code&gt;same-origin&lt;/code&gt; value. So although it is listed as a CORS connection according to &lt;code&gt;sec-fetch-mode&lt;/code&gt;, it isn&#39;t actually being requested across origins (since it is &lt;code&gt;same-origin&lt;/code&gt;), so basically ignore CORS and download it anyway.&lt;/p&gt;&lt;p&gt;So is &lt;code&gt;sec-fetch-mode: cors&lt;/code&gt; a reliable way to see if CORS connection is being used? I&#39;m afraid not. This header comes with a couple of issues:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;In Chrome the Fetch Metadata feature is &lt;a href=&quot;https://www.chromestatus.com/feature/5155867204780032&quot;&gt;still in development&lt;/a&gt; with a &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=843478&quot;&gt;ticket from 2018&lt;/a&gt; still open. So it can sometimes be wrong.&lt;/li&gt;&lt;li&gt;Browser support &lt;a href=&quot;https://caniuse.com/mdn-http_headers_sec-fetch-mode&quot;&gt;isn&#39;t great&lt;/a&gt;, with Safari (iOS and MacOS) and Firefox notable browsers that don&#39;t support it.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;So on it&#39;s own it can be an &lt;em&gt;indicator&lt;/em&gt; of a CORS connection, but it&#39;s not 100% reliable. Given that another strategy would be to examine the WebPageTest connection view. Within this view if you happen to see that you have over 6 connections to the same domain on HTTP/1.1, or more than 1 on HTTP/2, there&#39;s a high chance that one of these connections will be CORS. For more information on how to read a connection view chart, check out my &lt;a href=&quot;https://nooshu.com/blog/2019/12/30/how-to-read-a-wpt-connection-view-chart/&quot;&gt;blog post here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Identifying a CORS preflight request&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Related to CORS there is a concept known as a &#39;preflight request&#39;. Some CORS requests don&#39;t trigger a preflight request. These are called &#39;simple requests&#39;. If a request meets all of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests&quot;&gt;these conditions&lt;/a&gt; then it is considered &#39;simple&#39; and a preflight isn&#39;t required.&lt;/p&gt;&lt;p&gt;If a CORS request doesn&#39;t meet all the conditions from &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests&quot;&gt;here&lt;/a&gt;, then it will require a preflight request. I like to think of a preflight request as if you were to contact an online retailer to see if they have something in stock before you go ahead and order it online. The preflight request is checking with the server to understand if the actual request is safe to send back to the client (e.g. checking the &lt;code&gt;Access-Control-Allow-*&lt;/code&gt; header values the server supports). These preflight requests are sent using the &lt;code&gt;OPTIONS&lt;/code&gt; method (e.g. allow the client to understand what communication options are available from the server). The response from the server &lt;a href=&quot;https://tools.ietf.org/html/rfc7231#section-4.3.7&quot;&gt;isn&#39;t cachable&lt;/a&gt; and may not contain a response body (it will set the &lt;code&gt;Content-Length: 0&lt;/code&gt; header when this is the case).&lt;/p&gt;&lt;p&gt;It is possible to identify these preflight requests in WebPageTest:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;CORS preflight request seen in a WebPageTest waterfall, a very simple example with a single request.&quot; decoding=&quot;async&quot; height=&quot;554&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vA9IcSM1il-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vA9IcSM1il-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vA9IcSM1il-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vA9IcSM1il-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The above request didn&#39;t meet the conditions &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests&quot;&gt;set out here&lt;/a&gt; since it is asking to use the &lt;code&gt;DELETE&lt;/code&gt; method. So the rquest isn&#39;t classed as &#39;simple&#39; so the browser needs to check with the server to see if it supports &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If you are looking for examples of where CORS requests are triggered, &lt;a href=&quot;https://cors-client.digi.ninja/&quot;&gt;this is an excellent site&lt;/a&gt; which includes if a &#39;preflight&#39; is required too.&lt;/p&gt;&lt;h3 id=&quot;eager-prefetches&quot;&gt;Eager prefetches&lt;/h3&gt;&lt;p&gt;HTTP/2 prioritisation is incredibly important if you want pages to load optimally. Unfortunatly &lt;a href=&quot;https://github.com/andydavies/http2-prioritization-issues#cdns--cloud-hosting-services&quot;&gt;there are very few CDN&#39;s and cloud hosting providers that do it well&lt;/a&gt;. This is especially important when a browser is loading assets for the &lt;em&gt;next&lt;/em&gt; navigation in order to speed up a users journey once the current page has loaded. If done badly this pre-emptive loading can actually impact on the current page loading:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The prefetch turns out to be a prerender in Chrome, meaning the browser will fetch additional assets during the critical loading phase.&quot; decoding=&quot;async&quot; height=&quot;407&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8Q4ODpa9lY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8Q4ODpa9lY-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8Q4ODpa9lY-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8Q4ODpa9lY-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the above image (using Chrome) we see the the &lt;code&gt;&amp;lt;link rel=&quot;next&quot; href=&quot;https://example.com/page/2&quot; /&gt;&lt;/code&gt; in the page head triggering a prefetch. This &lt;code&gt;prefetch&lt;/code&gt; isn&#39;t a mistake, it&#39;s actually in the &lt;a href=&quot;https://html.spec.whatwg.org/multipage/links.html#link-type-next&quot;&gt;HTML Living Standard document&lt;/a&gt;. But what should be considered a mistake is where it is taking place during the page load. It&#39;s way too eager. But it&#39;s actually worse than a single request to a HTML page. Looking further down the waterfall we can see this isn&#39;t just a &lt;code&gt;prefetch&lt;/code&gt;. It&#39;s actually a &lt;code&gt;prerender&lt;/code&gt;. And as I mentioned earlier in this post a &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#prerender&quot;&gt;&lt;code&gt;prerender&lt;/code&gt;&lt;/a&gt; will also parse the HTML and load the assets from the page listed in the &lt;code&gt;rel=&quot;next&quot;&lt;/code&gt; link tag. This can be seen in the following waterfall:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The prefetch turns out to be a prerender in Chrome, meaning the browser will fetch additional assets during the critical loading phase.&quot; decoding=&quot;async&quot; height=&quot;511&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/K9v5UIhXTx-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/K9v5UIhXTx-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/K9v5UIhXTx-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/K9v5UIhXTx-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The critical point is that these requests are being made for the &#39;next&#39; page while the current page is still loading. So what happens if a user never actually navigates to this page? Well, that means the current page performance (and a users data) has been impacted for no reason. If these requests were made once the network were idle (i.e. the initial page has loaded), then this could be a useful feature.&lt;/p&gt;&lt;p&gt;It isn&#39;t only Chrome that does this, Safari does too, which &lt;a href=&quot;https://twitter.com/andydavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; discusses in his &#39;&lt;a href=&quot;https://andydavies.me/blog/2020/07/08/rel-equals-prefetch-and-the-importance-of-effective-http-slash-2-prioritisation/&quot;&gt;Rel=prefetch and the Importance of Effective HTTP/2 Prioritisation&lt;/a&gt;&#39; blog post. Chrome 88 due for release very soon (at the time of writing) actually &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=1158209&quot;&gt;fixes this issue&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;One last point on this functionality: it&#39;s &lt;a href=&quot;https://www.contentkingapp.com/academy/pagination/&quot;&gt;currently considered good practice&lt;/a&gt; to include &lt;code&gt;rel=&quot;next&quot;&lt;/code&gt; and &lt;code&gt;rel=&quot;prev&quot;&lt;/code&gt; links in pagination pages for SEO purposes. It&#39;s certainly worth considering &amp; testing the web performance impact of these before adding them to your page.&lt;/p&gt;&lt;h3 id=&quot;the-performance-impact-of-font-face-source-failures&quot;&gt;The performance impact of &lt;code&gt;@font-face&lt;/code&gt; source failures&lt;/h3&gt;&lt;p&gt;Below you can see the annotated waterfall for a site that requests the WOFF2 font first but it fails. This test was run on a real Moto G4 / 3G Fast connection:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the waterfall we see lots happening where by both sets of WOFF and WOFF2 fonts are loaded over the lifecycle of the page.&quot; decoding=&quot;async&quot; height=&quot;326&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bj1JSgOfup-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bj1JSgOfup-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bj1JSgOfup-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/bj1JSgOfup-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There&#39;s a fair amount going on in this waterfall so let&#39;s step through it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;On request 37 we see the TCP negotiation happen for the font domain. Once completed the font request goes out for the WOFF2 font. This whole process takes 2 seconds to complete. It takes ~865 ms to fail. That includes a full round trip to the server and back.&lt;/li&gt;&lt;li&gt;On request 41 it takes a full 1.7 seconds to fail. Notice how once failed the browser almost immediately requests the fallback WOFF font (request 46).&lt;/li&gt;&lt;li&gt;Notice how this fail / request pattern for all 4 fonts are aligned. Requests 37 &amp; 43, 39 &amp; 45, 40 &amp; 44, and 41 &amp; 46 are all font pairings (WOFF2 vs WOFF).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So in other words, the browser must receive a font failure before it can request the fallback. This lack of primary font source is adding 2 seconds to the font load in this test case. What&#39;s worse in the test case above is that the browser ends up downloading the larger of the two font file types. Since WOFF files can be 20-30% larger than WOFF2 files. Ultimately the user gets a double whammy in terms of poor performance: added time to make a successful font request, and more data to download.&lt;/p&gt;&lt;p&gt;So how can you check for this? Well, I&#39;d look to the browser console. If you notice &lt;code&gt;404&lt;/code&gt; errors for fonts, I&#39;d put those errors pretty high up your prioritisation list to fix!&lt;/p&gt;&lt;p&gt;This scenario has been copied from my blog post &lt;a href=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/&quot;&gt;&#39;The importance of &lt;code&gt;@font-face&lt;/code&gt; source order when used with preload&#39;&lt;/a&gt; if you would like the full context of this issue.&lt;/p&gt;&lt;h3 id=&quot;double-font-downloads-when-using-the-preload-resource-hint&quot;&gt;Double font downloads when using the &lt;code&gt;preload&lt;/code&gt; resource hint&lt;/h3&gt;&lt;p&gt;In the below waterfall chart we see a huge number of font downloads. The reason why this is, is quite interesting:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the waterfall we can see the preloaded WOFF2 fonts being requested, then later after CSS parsing another 8 font requests to the WOFF versions of the fonts.&quot; decoding=&quot;async&quot; height=&quot;381&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/D3z425AgU4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/D3z425AgU4-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/D3z425AgU4-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/D3z425AgU4-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Notice how the font requests from 2-7 are directly after the HTML download and parse. This is a sure sign that the &lt;code&gt;preload&lt;/code&gt; resource hint is being used. Then further down the the waterfall we can see the CSS being downloaded and parsed (request 9). The result of this CSS parsing are font requests 10-17. The unusual thing about this is that the font &lt;code&gt;preload&lt;/code&gt; is supposed to stop (or at least minimise) font requests happening at this point. By preloading the fonts they are already in the browser cache, ready to be used when needed. So it turns out the issue here is that the source (&lt;code&gt;src&lt;/code&gt;) order in the &lt;code&gt;@font-face&lt;/code&gt; rules are incorrect for this page:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@font-face&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bodytext&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.woff&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* WOFF tried first */&lt;/span&gt;
  		&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.woff2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* If WOFF is successful, WOFF2 is ignored */&lt;/span&gt;
       &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;our-test-font.ttf&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;opentype&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* TTF tried if other not supported. */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With the WOFF fonts placed first, a browser that supports this format will always download them and ignore the WOFF2 versions. But the preloads above are for WOFF2 fonts. This mismatch with the &lt;code&gt;preload&lt;/code&gt; and the source order in the &lt;code&gt;@font-face&lt;/code&gt; causes both types of file to download for any browser that supports WOFF2 (and that&#39;s &lt;a href=&quot;https://caniuse.com/woff2&quot;&gt;quite a few!&lt;/a&gt;). In order to fix this issue make sure your &lt;code&gt;preload&lt;/code&gt; font files match the &lt;code&gt;src&lt;/code&gt; order in your &lt;code&gt;@font-face&lt;/code&gt; rules.&lt;/p&gt;&lt;p&gt;This scenario is a very condensed version of my &lt;a href=&quot;https://nooshu.com/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/&quot;&gt;&#39;The importance of &lt;code&gt;@font-face&lt;/code&gt; source order when used with preload&#39;&lt;/a&gt; blog post. If you want to find out more about this issue I recommend reading it.&lt;/p&gt;&lt;h3 id=&quot;svgs-opening-up-a-new-tcp-connection&quot;&gt;SVG&#39;s opening up a new TCP connection&lt;/h3&gt;&lt;p&gt;Here&#39;s something I recently learned while running a site through WebPageTest. I just happened to see that SVG images were forcing the browser to open a new TCP connection when they were being downloaded from another origin:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In the waterfall we see the browser opening a new TCP connection to download thr SVG images.&quot; decoding=&quot;async&quot; height=&quot;161&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kpVRiFZ8Kj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kpVRiFZ8Kj-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kpVRiFZ8Kj-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/kpVRiFZ8Kj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;On request 19 you can see an additional TCP connection being opened for the SVG files. Notice how a DNS lookup isn&#39;t required. This is because it has already been done on request number 1. So this is a clear indication that this is an &lt;code&gt;anonymous&lt;/code&gt; connection (since HTTP/2 is being used on this domain). Now this isn&#39;t a bug in the browser or WebPageTest. This is all expected behavior and is actually defined in the &lt;a href=&quot;https://www.w3.org/TR/SVG/linking.html#processingURL-fetch&quot;&gt;SVG specifications&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;[In relation to SVG files] When fetching external resources from the Internet, user agents must use a potentially CORS-enabled request as defined in HTML [HTML] with the corsAttributeState...&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;In most cases when you are downloading resources from different servers the cross-origin protection isn&#39;t required, e.g. images, CSS files. Since CORS is a security feature put in place to control if the host site can access the information within the linked file being requested (the SVG file in this case), you may be asking why this is the case for an image file format. Well it turns out that SVG is capable of doing a lot more than being a simple image format, for example you can embed scripts for animation. These scripts can interact with the a document (on the remote server), and it is difficult to shild from this. So in theory an SVG can be customised to include private data about the remote server (which the page downloading the SVG shouldn&#39;t have access too). Because of this, an SVG is required to be loaded over a CORS-enabled TCP request when being loaded cross-origin. There&#39;s &lt;a href=&quot;https://oreillymedia.github.io/Using_SVG/extras/ch10-cors.html&quot;&gt;a whole section in the O&#39;Reilly Using SVG&lt;/a&gt; book all about this.&lt;/p&gt;&lt;p&gt;So if you ever see a browser opening up new TCP connections for SVG files, now you know why.&lt;/p&gt;&lt;h3 id=&quot;more-to-be-added-soon&quot;&gt;More to be added soon...&lt;/h3&gt;&lt;p&gt;As I discover more common waterfall scenarios I will add them here. If you know of any common ones that are missing, please do &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;h2 id=&quot;hidden-gems&quot;&gt;Hidden gems&lt;/h2&gt;&lt;p&gt;So there are features that are hidden in plain sight on the WebPageTest UI that you may not have even noticed before. Here are a few that I&#39;ve found useful:&lt;/p&gt;&lt;h3 id=&quot;how-the-filmstrip-view-and-waterfall-chart-are-related&quot;&gt;How the filmstrip view and waterfall chart are related&lt;/h3&gt;&lt;p&gt;Now it may seem obvious to some, but it is worth pointing out nonetheless. There is a direct relationship between the filmstrip view and the corresponding waterfall chart below it:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The filmstrip and the waterfall chart are related. There&#39;s a red line to the left of the waterfall that corresponds to the red line that moves across the waterfall chart as you scroll. You also see thick orange borders around each image if something has changed on the screen.&quot; decoding=&quot;async&quot; height=&quot;592&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/toaIu3LzKj-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/toaIu3LzKj-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/toaIu3LzKj-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/toaIu3LzKj-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;To the far left of the filmstrip you will see a 1px vertical red line. As you scroll the filmstrip horizontally you will see an identical red line moving across the waterfall chart. These are directly related. Together they show you what the page looked like at that exact point in the waterfall chart (you would not believe how long it took me to notice this feature!).&lt;/p&gt;&lt;p&gt;Another feature that can be seen is the use of a thick orange border around some of the images. This orange border signifies that something has changed on the screen (compared to the previous one). This is very useful if you are trying to identify even minor changes between screenshots (like an icon loading).&lt;/p&gt;&lt;p&gt;You can see both of these features in action in the screenshot. There is a thick orange border around the image at 0.9s, as it shows a major change to the page compared to the image at 0.8s. Looking closer at the waterfall we can see the red line is approaching and the vertical green line (start render). The image at 0.9s is actually the start render time for the page.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update to filmstrip thumbnail size&lt;/strong&gt; You may have noticed in the filmstrip image above that there&#39;s an option in there to control the thumbnail size of the filmstrip images (small, medium, large). Now this doesn&#39;t only control the image as it is seen on your screen, if you decide to export the filmstrip the output image will also be effected. Previously the method for increasing the size of these imaes was to use the &lt;code&gt;thumbSize&lt;/code&gt; URL parameter to a bigger value (it maxed out at 500px).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;There&#39;s now a new option for the filmstrip view. The &#39;Huge&#39; image size option has been added.&quot; decoding=&quot;async&quot; height=&quot;244&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XttCipwNEX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XttCipwNEX-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XttCipwNEX-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XttCipwNEX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Recently a new option has been added, and that is the &#39;Huge&#39; size (see above). By default selecting this option will return the native size captured by the video. But if you have &#39;&lt;a href=&quot;https://nooshu.com/blog/2020/12/31/how-to-run-a-webpagetest-test/#capture-full-size-video&quot;&gt;Capture Full Size Video&lt;/a&gt;&#39; enabled then each of the thumbnail images will be 600px wide.&lt;/p&gt;&lt;h3 id=&quot;what-do-the-filmstrip-thumbnail-border-colours-signify&quot;&gt;What do the filmstrip thumbnail border colours signify&lt;/h3&gt;&lt;p&gt;Over the past month or so (May/June 2020) there&#39;s been an important development for web performance on the web. From 2021, Google will &lt;a href=&quot;https://webmasters.googleblog.com/2020/05/evaluating-page-experience.html&quot;&gt;start evaluating page expereince&lt;/a&gt; and will include the results in how a page is raked by the search engine. They will be evaluating a pages usability using three metrics (at the time of writing). Together they are known as the &#39;&lt;a href=&quot;https://web.dev/vitals/&quot;&gt;core web vitals&lt;/a&gt;&#39;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Largest Contentful Paint (LCP) - Measures loading&lt;/li&gt;&lt;li&gt;First Input Delay (FID) - Measures interactivity&lt;/li&gt;&lt;li&gt;Cumulative Layout Shift (CLS) - Measures visual stabiliy&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You will be excited to hear LCP and CLS are now very easy to spot on the filmstrip view, thanks to &lt;a href=&quot;https://twitter.com/rick_viscomi&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rick Viscomi&lt;/a&gt; who &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/issues/1366&quot;&gt;raised this issue&lt;/a&gt; and of course &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; who implimented it. A key of what each of the borders means can be seen below:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;span style=&quot;width:12px;background-color:none;display:inline-block;border:1px solid #000&quot;&gt; &lt;/span&gt;&lt;strong&gt;No border&lt;/strong&gt;: no visual change occurred.&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;width:12px;background-color:#ffc233;display:inline-block&quot;&gt; &lt;/span&gt;&lt;strong&gt;Yellow border&lt;/strong&gt;: a visual change occured on this frame.&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;width:12px;background-color:red;display:inline-block&quot;&gt; &lt;/span&gt;&lt;strong&gt;Red border&lt;/strong&gt;: Largest Contentful Paint (LCP) occured on this frame.&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;width:2px;background-color:#ffc233;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#000;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#ffc233;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#000;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#ffc233;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#000;display:inline-block&quot;&gt; &lt;/span&gt;&lt;strong&gt;Yellow/black dashed border&lt;/strong&gt;: There was a visual change &lt;strong&gt;and&lt;/strong&gt; a layout shift on this frame.&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;width:2px;background-color:red;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#000;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:red;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#000;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:red;display:inline-block&quot;&gt; &lt;/span&gt;&lt;span style=&quot;width:2px;background-color:#000;display:inline-block&quot;&gt; &lt;/span&gt;&lt;strong&gt;Red/black dashed border&lt;/strong&gt;: Largest Contentful Paint (LCP) &lt;strong&gt;and&lt;/strong&gt; a layout shift occured on this frame.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Or if you prefer, an annotated filmstrip with all border variations can be seen below, generated from &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=200616_G6_62122e52cc92dbba018f0cf585dbcf10%2C200616_SJ_ea6078c9cfc67fd566434ccfe699ca8d&amp;thumbSize=200&amp;ival=100&amp;end=visual&quot;&gt;this comparison view&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The filmstrip view thumbnails will have different borders depending on what metrics changed in that frame.&quot; decoding=&quot;async&quot; height=&quot;321&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/UfhsXLcEYo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/UfhsXLcEYo-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/UfhsXLcEYo-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/UfhsXLcEYo-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;how-to-generate-customised-waterfall-chart-images&quot;&gt;How to generate customised waterfall chart images&lt;/h3&gt;&lt;p&gt;Almost all of the waterfall images in the article you see above have used this feature that is tucked away at the bottom of every waterfall chart. WebPageTest gives you the ability to customise what is included in a waterfall chart:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The &#39;customize waterfall&#39; link leads you to a page with a whole host of options you can use to customise the waterfall image.&quot; decoding=&quot;async&quot; height=&quot;357&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/O9fP9q5TuQ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/O9fP9q5TuQ-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/O9fP9q5TuQ-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/O9fP9q5TuQ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Clicking the &#39;customize waterfall&#39; link directs you to a set of customisation options (see image below):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The &#39;customize waterfall&#39; link gives you the ability to select what requests and graphs you want to include on the output image, the time the waterfall covers, as well as it&#39;s dimensions. These are just a few options available.&quot; decoding=&quot;async&quot; height=&quot;531&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/r8tRlSvGmg-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/r8tRlSvGmg-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/r8tRlSvGmg-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/r8tRlSvGmg-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see in the image above, I have customised the output image. We have:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Set a custom image width&lt;/li&gt;&lt;li&gt;Set a custom time that the whole waterfall chart covers (note this will lead to cropping of requests further down the waterfall)&lt;/li&gt;&lt;li&gt;Only selected certain requests to be shown either individually or as a range (notice the ellipsis between these that identify missing items)&lt;/li&gt;&lt;li&gt;Unchecked &#39;Show CPU Utilization&#39; so the &#39;CPU Utilisation&#39; and &#39;Browser main thread&#39; graphs are hidden&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;All these options allow you to remove much of the noise in the chart so you can focus on the areas you wish to identify and highlight. Once you are happy with the image, simply right click and save the image to your device as you would any other image.&lt;/p&gt;&lt;h3 id=&quot;how-to-examine-detailed-information-about-images-on-a-page&quot;&gt;How to examine detailed information about images on a page&lt;/h3&gt;&lt;p&gt;Here&#39;s another one of those links that is sitting in plain sight below the waterfall chart, the &#39;View all images&#39; link. It doesn&#39;t sound very useful but once you understand how it works you may reconsider that.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The &#39;view all images&#39; link sits under the waterfall chartnext to the &#39;customize waterfall link.&quot; decoding=&quot;async&quot; height=&quot;320&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Q4RsSP7CY0-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Q4RsSP7CY0-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Q4RsSP7CY0-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Q4RsSP7CY0-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once you click on this link you&#39;ll be presented with a full page of images that were loaded in the test by the browser. A couple of key points to mention:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The images a presented in the order in which they were loaded by the browser. This is really useful to spot-check images sizes and the load order.&lt;/li&gt;&lt;li&gt;WebPageTest doesn&#39;t store these images, they are loaded from the original server. This is important to remember, as for older tests these images could break if they are removed from the origin server.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For each of the images you are presented with the following information:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Image URL&lt;/li&gt;&lt;li&gt;Filesize&lt;/li&gt;&lt;li&gt;Image content type&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And if the image happens to be a JPEG, you will also get the following extra information:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Optimized size including new size and the difference in KB (only seen if image can be optimised)&lt;/li&gt;&lt;li&gt;JPEG scan count [1 scan = &#39;Baseline (Renders top-down)&#39;, multiple scans = &#39;Progressive (Renders blurry to sharp)&#39;]&lt;/li&gt;&lt;li&gt;Analyze JPEG link&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Analyze JPEG&lt;/strong&gt; Here&#39;s a hidden easter egg for you within the &#39;View all images&#39; page. Each of the JPEG images can be analysed further by clicking the &#39;Analyze JPEG&#39; link. This takes you to a page with a whole wealth of further information about that specific image:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Image statistics&lt;/li&gt;&lt;li&gt;Original image&lt;/li&gt;&lt;li&gt;Optimised image using lossless compression&lt;/li&gt;&lt;li&gt;Optimised image using lossy compression (at 85%)&lt;/li&gt;&lt;li&gt;Exif data found in the image&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;The jpeginfo page contains a lot of information about a specific JPEG image. This includes any metadata and also optimised image versions.&quot; decoding=&quot;async&quot; height=&quot;749&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Qh1F8GBhha-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Qh1F8GBhha-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Qh1F8GBhha-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Qh1F8GBhha-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This information is all fairly self-explanatory. But there is one point worth noting: &#39;Application Data&#39; is referring to the size of non-image data (metadata) that is embedded within the image file (in a binary format). This can quite often be large if it hasn&#39;t been stripped out. Examining the &lt;a href=&quot;https://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#iptc-core-schema-1-2-specifications&quot;&gt;IPTC Photo Metadata Standard 2019.1 metadata schema&lt;/a&gt; it is easy to see why. There are a huge number of fields that are valid, and each could potentially contain data. So if you are looking optimise an image down to the very last byte you should strip out this data.&lt;/p&gt;&lt;h3 id=&quot;how-to-inspect-the-browser-main-thread-part-1&quot;&gt;How to inspect the &#39;Browser Main Thread&#39; - Part 1&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Only for tests using a Chromium-based browser.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The &#39;Summary&#39; page for the tests that have run you will see a link that says &#39;Timeline (view)&#39; It&#39;s fairly inconspicuious so you may not have noticed it before:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The timeline link highlighted is available on the test summary page and leads to more detailed information as to what the browser main thread is doing.&quot; decoding=&quot;async&quot; height=&quot;495&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5SOaY0YHn-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5SOaY0YHn-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5SOaY0YHn-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/m5SOaY0YHn-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It turns out that this link is actually 2 links:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&#39;Timeline&#39; - downloads a JSON file with all the data&lt;/li&gt;&lt;li&gt;&#39;(view)&#39; - opens a version of Chrome inspector for the data&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I will be concentraiting on the &#39;(view)&#39; link functionality for the rest of this section.&lt;/p&gt;&lt;p&gt;As mentioned in the &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#6---browser-main-thread&quot;&gt;Browser Main Thread&lt;/a&gt; section above, the colourful graph at the bottom of a waterfall chart tells you what the browsers main thread is doing at any point in time. Ranging from 0-100% for &#39;Script parsing, evaluation and execution&#39;, &#39;Layout&#39;, &#39;Painting&#39;, &#39;HTML parsing&#39;, and &#39;processing not accounted for&#39;. The great thing about this small graph is it gives you an indication of where the bottlenecks are in your website. For example, if this graph is solid orange, the device is parsing and executing JavaScript and is likely being limited by the sheer amount of it (or horribly unoptimised code!).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see the browser main thread with two sections of heavy activity. Point 1 mainly layout execution. Point 2 is mostly processing time that doesn&#39;t fit in the other categories (unaccounted for).&quot; decoding=&quot;async&quot; height=&quot;142&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/figmqB2mS8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/figmqB2mS8-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/figmqB2mS8-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/figmqB2mS8-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example above we see two main areas of heavy activity on the browser main thread. We can understand some of what is happening because of the colours used in the graph and the height on the graph, but we have no idea &lt;em&gt;exactly&lt;/em&gt; what is causing this heavy activity. This is where the &#39;(view)&#39; link comes in incredibly useful:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Here we see what looks like the &#39;Performance&#39; tab in Chrome DevTools. It shows lots of detailed information about what was happening on the browsers main thread during the test. The heavy activity points have been highlighted as they match with the image above.&quot; decoding=&quot;async&quot; height=&quot;161&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_J_rEt0tUy-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_J_rEt0tUy-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_J_rEt0tUy-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_J_rEt0tUy-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If you&#39;ve ever used the &#39;Performance&#39; tab in Google Chrome the above image will look very familiar. It is esentially a slightly cut down version of that tab (in terms of data available). Clicking on the link opens up a whole wealth of information related to browser main thread (&lt;code&gt;CrRenderMain&lt;/code&gt;), giving you the ability to see what caused all the activity on the thread (for the test that ran). Above I&#39;ve drilled down into the main thread data. You will see a striking similarity with the graph at the bottom of the waterfall chart, and that&#39;s because this data is what drives the waterfall chart visualisation. Both heavy activity points have been highlighted as an example. This activity is made up of hundreds of thin slices of colour, each one signifying an event that happened on the main thread. You now have the ability to zoom in and click on each one of these slices to find out more information about what is happening. For lots more information on the performance tab in Chrome DevTools check out &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/evaluate-performance&quot;&gt;Get Started With Analyzing Runtime Performance&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/kaycebasques&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Kayce Basques&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;how-to-inspect-the-browser-main-thread-part-2&quot;&gt;How to inspect the &#39;Browser Main Thread&#39; - Part 2&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Only for tests using a Chromium-based browser.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: I recommend reading Part 1 above before reading this section, as I reference parts of it below.&lt;/p&gt;&lt;p&gt;On the &#39;Summary&#39; page there&#39;s a link that sits to the left of the waterfall thubmnail. This time it is called &#39;Trace (view)&#39;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The trace link highlighted is available on the test summary page and leads to more detailed information as to what the browser main thread is doing.&quot; decoding=&quot;async&quot; height=&quot;495&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_YjJVW2oP9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_YjJVW2oP9-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_YjJVW2oP9-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/_YjJVW2oP9-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Again, what looks like a single link is actually 2 separate links:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&#39;Trace&#39; - downloads a JSON file with all the data&lt;/li&gt;&lt;li&gt;&#39;(view)&#39; - opens a tool called Perfetto that allows you to inspect the data&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I will again be concentrating on the &#39;(view)&#39; link functionality for the rest of this section.&lt;/p&gt;&lt;p&gt;When you click on the &#39;(view)&#39; link it opens up a whole new window that looks nothing like WebPageTest:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Perfetto is another UI that allows you to drill down into the browser main thread data.&quot; decoding=&quot;async&quot; height=&quot;552&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vQxTZbXnDX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vQxTZbXnDX-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vQxTZbXnDX-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/vQxTZbXnDX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Now what you may be thinking is: &quot;Well this looks very similar to Chrome inspector mentioned in part 1&quot;. And you would be correct. The same underlying data is the same in both tools. It&#39;s just the UI&#39;s that are different. I&#39;ve even highlighted the heavy activity points as seen earlier so you can see the similarities.&lt;/p&gt;&lt;p&gt;So what&#39;s the point in having 2 different tools displaying the exact same data? Well the simple answer is that Perfetto is capable of displaying a lot more data than what you see in a default WebPageTest run. In a standard WebPageTest run, the data being captured is primarily focused on the browsers interaction with the website in question. Things like assets downloaded, times taken to parse scripts, how the page actually rendered etc. In these tests we aren&#39;t concerned with how well the (Chromium) browser itself performed at a system level. Chrome comes bundled with functionality that allows developers to capture the underlying interactions at a system level and profile the Chrome browser itself. This is called &lt;code&gt;chrome://tracing&lt;/code&gt;. Perfetto is WebPageTest&#39;s new replacement for &lt;code&gt;chrome://tracing&lt;/code&gt;. So given the opportunity, WebPageTest is able to provide you with a lot(!) more system level data. Data like GPU, Compositor, and Network service information. To capture this data simply enable the &#39;Capture Chrome Trace (about://tracing)&#39; option under the &#39;Advanced settings&#39; --&gt; &#39;Chromium&#39; tab when you are setting up your test.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: If you are only interested in web side of the WebPageTest data, &lt;strong&gt;DON&#39;T&lt;/strong&gt; enable this option. Some categories can add a lot of overhead to the tests, which in turn can result in the skewing of overall timings. This setting is primarily targeted at browser engineers, not web developers. For those of you curious about the data you get back, see the image below. When I tested with tracing enabled, WebPageTest generated a 20MB JSON file just for the trace data!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Perfetto can give you an insight into the underlying Chromium profile data if tracing is enabled in the test settings. Image shows a huge number of data points visualised across many different categories.&quot; decoding=&quot;async&quot; height=&quot;814&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/6wRBfrXXR6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/6wRBfrXXR6-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/6wRBfrXXR6-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/6wRBfrXXR6-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see, it&#39;s all very colourful but very much data overkill for page level performance testing!&lt;/p&gt;&lt;h3 id=&quot;how-to-analyse-page-images-for-optimal-compression-and-render-size&quot;&gt;How to analyse page images for optimal compression and render size&lt;/h3&gt;&lt;p&gt;Optimally sizing and compressing images on the web is a difficult problem. There are so many different loading methods and formats available, so how do you choose which format is best to use? Well thankfully WebPageTest comes with a handy link to a tool that will massively simplify this process. The tool is provided by &lt;a href=&quot;https://cloudinary.com/&quot;&gt;Cloudinary&lt;/a&gt;, who are a platform that offer products that automate the optimal delivery of media assets. For every WebPageTest run, it is possible to analyse the images loaded in the test by clicking on the &#39;Image Analysis&#39; link in the test result navigation:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The image analysis link will take you to the Cloudinary tool that will analise the images listed in the test.&quot; decoding=&quot;async&quot; height=&quot;273&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/w_YLLK71pJ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/w_YLLK71pJ-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/w_YLLK71pJ-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/w_YLLK71pJ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This link isn&#39;t the only way to get to the tool. If you know the ID of the WebPageTest result then simply navigate to:&lt;/p&gt;&lt;p&gt;&lt;code&gt;http://webspeedtest.cloudinary.com/results/[WPT_TEST_ID_HERE]&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Cloudinary will extract the information it needs via the WebPageTest API for your test ID. This pattern could be useful if you are generating these links programatically, for example via &lt;a href=&quot;https://github.com/andydavies/WPT-Bulk-Tester&quot;&gt;WPT-Bulk-Tester&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Once you navigate to the tool and it has analised the test results, you will be presented with an overall page score as well as the potential image sizes on the page after optimal compression:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Cloudinary tool offers you information on how you could optimise your page images.&quot; decoding=&quot;async&quot; height=&quot;529&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZFle5_mqCz-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZFle5_mqCz-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZFle5_mqCz-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZFle5_mqCz-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If you scroll down the page the tool will even give you a score from A-F for every image listed in the test, and the size of each image once transcoded to a different image format. By default it tries JPEG, WebP, &lt;a href=&quot;https://jpeg.org/jpegxr/&quot;&gt;JPEG-XR&lt;/a&gt;, and PNG. Thus allowing you to decide which format is the best for the individual image at hand.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important note&lt;/strong&gt;: This tool will only analise the images that are listed in the WebPageTest results. So for example, if you are testing using Chrome and the page images have native lazy-loading attribute (&lt;code&gt;loading=&quot;lazy&quot;&lt;/code&gt;), only the images that Chrome decides to lazy-load will be analysed.&lt;/p&gt;&lt;h3 id=&quot;how-to-visualise-the-requests-from-your-test&quot;&gt;How to visualise the requests from your test&lt;/h3&gt;&lt;p&gt;I&#39;m not sure if this is classed as a &#39;hidden gem&#39;, it&#39;s actually quite prominant being in the secondary navigation for all test results. But I&#39;m going to list it anyway:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;To the far right of the secondary navigation aftur running a test is the &#39;Request Map&#39; link that opens the current test results in the visualisation tool.&quot; decoding=&quot;async&quot; height=&quot;267&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XCK5lOeqhr-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XCK5lOeqhr-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XCK5lOeqhr-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/XCK5lOeqhr-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The request map tool is a great way to visualise all the requests that happened in a WebPageTest test, allowing you to quickly see all the domains that were visited, files downloaded, relative size of the asset transmitted in bytes, as well as many other bits of useful functionality. Once you understand what it is displaying, it becomes an extremly powerful tool for understanding how a website is built, as well as general debugging. Here&#39;s an example request map from everyones favorite web performance poster child: CNN:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An example request map from CNN.com showing lots of requests to many third-party domains.&quot; decoding=&quot;async&quot; height=&quot;585&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5r0H2OFGnv-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5r0H2OFGnv-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5r0H2OFGnv-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/5r0H2OFGnv-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It was created by &lt;a href=&quot;https://twitter.com/simonhearne&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Simon Hearne&lt;/a&gt;. You can see a &lt;a href=&quot;https://requestmap.herokuapp.com/demo/&quot;&gt;live demo here&lt;/a&gt;, and read all about it on &lt;a href=&quot;https://simonhearne.com/2015/find-third-party-assets/&quot;&gt;Simon&#39;s blog here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;how-to-visualise-variance-in-metrics-across-multiple-tests&quot;&gt;How to visualise variance in metrics across multiple tests&lt;/h3&gt;&lt;p&gt;What if I told you there was a way to graph the variance in metrics across test runs in WebPageTest. Until today I had no idea it was possible. There are a few quirks to how this functionality works, but it is possible to get it working. First you must have a test which completed more than one run (you need some variance in the stats after all!). Historically this functionality has been used to plot variance within tests that have 100+ test runs. Within the test summary you should see the &#39;Plot Full Results&#39; link sitting below the &#39;Performance Results&#39; table (see image below):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The &#39;Plot Full Results&#39; link is a window into graphing the variance in metrics between test runs, if you can work out how to get it running.&quot; decoding=&quot;async&quot; height=&quot;215&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Phhoa3cspw-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Phhoa3cspw-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Phhoa3cspw-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Phhoa3cspw-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Clicking the &#39;Plot Full Results&#39; link will initially bring you to a very empty looking page. The reason for this is because you currently only have a single test ID in the urls &lt;code&gt;tests&lt;/code&gt; parameter. Here&#39;s a quick way to generate multiple test ID&#39;s in a url:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Go to the &#39;Test History&#39; (top navigation)&lt;/li&gt;&lt;li&gt;Select the tests you wish to compare (checkbox&#39;s along the far left)&lt;/li&gt;&lt;li&gt;Click &#39;Compare&#39; button at the top of the page&lt;/li&gt;&lt;li&gt;Copy the long &lt;code&gt;tests&lt;/code&gt; parameter in the URL with all your selected test ID&#39;s&lt;/li&gt;&lt;li&gt;Add these to the &lt;code&gt;https://www.webpagetest.org/graph_page_data.php?tests=[ID HERE],[ID HERE]...&lt;/code&gt; url then navigate to the page&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Now you will be presented with approximatly 27 graphs, one for each of the major metrics:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;In this graph we see the variance of the onload event across different test ID&#39;s.&quot; decoding=&quot;async&quot; height=&quot;489&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/lKgO8Tz4CM-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/lKgO8Tz4CM-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/lKgO8Tz4CM-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/lKgO8Tz4CM-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The graph quickly gives you a sense of how stable a metric is across test runs, and allows you to spot outliers easily. There are quite a few other settings you can play with too. I recommend changing the &#39;Statistical Comparison Against&#39; dropdown to one of the test ID&#39;s. This will then give you a whole set of tables where you can see how the other tests varied compared to the selected test ID. If you are interested in statistics related to web performance metrics, you will enjoy this hidden page on WebPageTest.&lt;/p&gt;&lt;h3 id=&quot;how-to-add-custom-marks-using-the-user-timing-api&quot;&gt;How to add custom marks using the User Timing API&lt;/h3&gt;&lt;p&gt;Here&#39;s a useful feature you may not know about. Using the &lt;a href=&quot;https://www.w3.org/TR/user-timing/&quot;&gt;User Timing API&lt;/a&gt; you have the ability to &lt;a href=&quot;https://www.html5rocks.com/en/tutorials/webperformance/usertiming/&quot;&gt;mark certain points&lt;/a&gt; in your page load. These points will be discovered by WebPageTest and displayed accordingly in the results.&lt;/p&gt;&lt;p&gt;For example, if you wanted to know when the browser got to the end of the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag, you could add the following code right before the closing tag:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- head stuff here... --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;performance&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mark&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;mark_head_parsed&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The browser then sets a mark at this point which can be read by WebPageTest. The resulting WebPageTest run will show you results similar to this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Adding custom marks via the User Timing API gives output metrics similar to this in the WebPageTest output.&quot; decoding=&quot;async&quot; height=&quot;60&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4xKY8bZq-o-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4xKY8bZq-o-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4xKY8bZq-o-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/4xKY8bZq-o-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see from the image above I have set four User Timing marks on the page and one of them is called &lt;code&gt;mark_head_parsed&lt;/code&gt;. You can add as many marks as you need, it really depends on what you are trying to measure. Now if you head over to the &#39;customise waterfall&#39; link (as mentioned in the section above), you will see one or more purple triangles and vertical lines on the waterfall chart. These are the User Timing marks we just set:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The custom timing marks can be seen in purple on the waterfall chart if you customise the waterfall image.&quot; decoding=&quot;async&quot; height=&quot;526&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/DJcFjinbqD-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/DJcFjinbqD-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/DJcFjinbqD-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/DJcFjinbqD-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the image above the timing marks are now visible (you get the option to toggle them on and off if required). So you may be thinking: &quot;So why can&#39;t I see these marks on the interactive waterfall chart?&quot;. The reason for this is because they have been disabled by default on the interactive chart. Many third-party JavaScript scripts were including marks in their code, which was polluting the graph with a whole load of noise that were irrelevant to most WebPageTest users. So it was decided to disable the visibility of the marks by default. Marks can now only be seen within the &#39;customise waterfall&#39; link.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you happen to be running a private instance of WebPageTest, you can configure it to display the User Timing marks to be on by default for the interactive waterfall chart.&lt;/p&gt;&lt;h3 id=&quot;how-to-display-the-bandwidth-graph-on-mobile-devices&quot;&gt;How to display the bandwidth graph on mobile devices&lt;/h3&gt;&lt;p&gt;When you run a test on one of the real mobile devices that are sitting in Pat Meenan&#39;s basement in Dulles, VA, by default you don&#39;t get a bandwidth chart like you do on desktop agents. Resulting in an output that looks like this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Without tcpdump enabled the bandwidth graph isn&#39;t visible at the bottom of the UI on mobile devices.&quot; decoding=&quot;async&quot; height=&quot;298&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/j_knxSeTWl-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/j_knxSeTWl-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/j_knxSeTWl-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/j_knxSeTWl-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If you do want to capture this data and display the graph in the resulting test, make sure you check &#39;Capture network packet trace (tcpdump)&#39; under the &#39;Advanced&#39; when you configure your test:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The tcpdump option is located under the &#39;Advanced&#39; tab when you configure your test.&quot; decoding=&quot;async&quot; height=&quot;298&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/190qz4TEFR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/190qz4TEFR-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/190qz4TEFR-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;This will give you a slightly different version of the &#39;Bandwidth In&#39; graph than you see on desktop agents:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Enabling tcpdump now displays the bandwith graph on mobile devices, which gives you information on Bandwidth In and Duplicate (wasted) Data.&quot; decoding=&quot;async&quot; height=&quot;338&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Z8iUBteJR3-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Z8iUBteJR3-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Z8iUBteJR3-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/Z8iUBteJR3-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It now gives you information about the data wasted due to the retransmission of packets over the network (although not actually seen on the example graph). An example with &#39;Duplicate (wasted) Data&#39; can be &lt;a href=&quot;https://github.com/WPO-Foundation/webpagetest/issues/1320#issuecomment-568777858&quot;&gt;seen here&lt;/a&gt; along with a little more information.&lt;/p&gt;&lt;h3 id=&quot;how-to-test-a-404-pages-web-performance&quot;&gt;How to test a 404 pages web performance&lt;/h3&gt;&lt;p&gt;So I had a weird usecase for &lt;a href=&quot;https://github.com/WPO-Foundation/wptagent/issues/349&quot;&gt;wanting this feature in WebPageTest&lt;/a&gt; releated to one of the GOV.UK applications, so there may be others out there wondering about their 404 page performance (right?). So it seems like a &#39;feature&#39; worth documenting. Keep in mind that many automated testing tools will simply abort a test should you happen to run one on a 404 page This makes sense after all as the tool is encountering a 404 status code, why would it continue?&lt;/p&gt;&lt;p&gt;But my thoughts on this are: should your user mistype a single charecter in a URL, they are likely going to be hitting your 404 page. What if you happen to have some strange server / templating configuration for the 404 page that is serving a large amount of data. It could even just be just a large image on the page. Do you really want your users to be wasting their time and bandwidth on a page they don&#39;t even want to be on? A secondary point is that a 404 request will be hitting your origin server too (including the servers hard drive), so you want it performing optimally. And remember to &#39;&lt;a href=&quot;https://love2dev.com/blog/web-performance-tip-make-sure-you-have-a-favicon/&quot;&gt;Make sure you have a favicon&lt;/a&gt;&#39;, else there will be a lot of hits to your 404 page! I&#39;ve now written a blog post all about 404 performance testing which you can &lt;a href=&quot;https://nooshu.com/blog/2020/08/25/you-should-be-testing-your-404-pages-web-performance/&quot;&gt;read here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There is now a workaround in WebPageTest that allows you to do this, either via the user interface, or WPT scripting:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;User Interface&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Force WPT to measure the performance of a 404 page by setting a minimum test duration.&quot; decoding=&quot;async&quot; height=&quot;893&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nxJgU44ElU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nxJgU44ElU-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nxJgU44ElU-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/nxJgU44ElU-813.webp 813w&quot; width=&quot;813&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Scripting&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;setMinimumStepSeconds 2
navigate https://www.example.com/this-is-a-404-page
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simply set a minimum test duration of a number of seconds longer than the 404 page takes to load to gather metrics about your 404 page performance.&lt;/p&gt;&lt;h3 id=&quot;how-to-delve-into-the-download-chunk-data&quot;&gt;How to delve into the download chunk data&lt;/h3&gt;&lt;p&gt;I mentioned the fact that download chunks are visible in each row, and they are identified by the darker colour in the &lt;a href=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#download-chunks&quot;&gt;Download chunks&lt;/a&gt; section before. But lets say you wanted to find out a little more about these chunks. Simply click on the request row you want to find out more about then click on the &#39;Raw Details&#39; tab. Scrolling down a little in the box you will find a &#39;chunks&#39; key within the JSON data. The &#39;chunks&#39; key has an array as a value, and within the array there are separate objects each with information about a single chunk of data.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;There&#39;s detailed information about each requests download chunk data hidden within the &#39;Raw Details&#39; tab when you click on request row.&quot; decoding=&quot;async&quot; height=&quot;599&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8s7Au6N0rJ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8s7Au6N0rJ-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8s7Au6N0rJ-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/8s7Au6N0rJ-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Each chunk object has two keys:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;ts&lt;/code&gt;: stands for timestamp. It&#39;s the point at which this download chunk completed (not started). The time is in milliseconds and relative to the start of the waterfall&lt;/li&gt;&lt;li&gt;&lt;code&gt;bytes&lt;/code&gt;: pretty self-explanitory, the number of bytes that were downloaded in this chunk.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;WebPageTest works backwards from the completed chunk timestamp, looking at the bandwidth available at that point in time and calculates how long it would take to download that number of bytes. This gives a chunk its width on the waterfall. This is all measured from &lt;a href=&quot;https://en.wikipedia.org/wiki/Pcap&quot;&gt;Packet Capture (pcap)&lt;/a&gt; bandwidth information.&lt;/p&gt;&lt;p&gt;Multiple chunks in very close proximity merge together to look like a single large chunk, but this data allows you to drill down into each large chunk to see the individual chunks.&lt;/p&gt;&lt;h3 id=&quot;how-to-view-a-sites-http-2-dependency-graph-chrome&quot;&gt;How to view a sites HTTP/2 dependency graph (Chrome)&lt;/h3&gt;&lt;p&gt;So HTTP resource prioritisation is a notoriously complex beast of a problem (that&#39;s an understaitment!). Don&#39;t believe me? Here&#39;s &lt;a href=&quot;https://twitter.com/programmingart&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Robin Marx&lt;/a&gt; giving a &lt;a href=&quot;https://archive.fosdem.org/2020/schedule/event/webperf_http_prioritization/&quot;&gt;35 minute talk all about it at FOSDEM 2019&lt;/a&gt;. Now what you may not realise is that if a website is using HTTP/2 and you run it through WebPageTest, there is a way to quickly view the HTTP/2 dependency graph for the particular browser used in the test:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The &#39;View HTTP/2 Dependency Graph link is situated directly under the waterfall view chart.&quot; decoding=&quot;async&quot; height=&quot;228&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZZXHY8yK2H-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZZXHY8yK2H-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZZXHY8yK2H-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/ZZXHY8yK2H-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I&#39;m not going to go massively into the details of what is involved in prioritisation, there are plenty of &lt;a href=&quot;https://www.manning.com/books/http2-in-action&quot;&gt;books&lt;/a&gt;, &lt;a href=&quot;https://developer.akamai.com/blog/2019/01/31/http2-discover-performance-impacts-effective-prioritization&quot;&gt;blog posts&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/watch?v=sgjxuhFQktE&quot;&gt;talks&lt;/a&gt; online from folks who know much more about it than I do. But I will try to give a brief overview of how to read a basic chart. It&#39;s worth noting that each browser does this differently:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Legacy Edge and Internet Explorer don&#39;t support prioritisation (they load everything split equally in parallel)&lt;/li&gt;&lt;li&gt;Safari loads all resources in parallel, but splitting available bandwidth depending on the priority as Safari sees it&lt;/li&gt;&lt;li&gt;Firefox builds a complex dependency tree and loads resources according to it&lt;/li&gt;&lt;li&gt;Chrome (and all Chromium-based browsers) construct a linear list and loads according to that list&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There&#39;s an excellent &lt;a href=&quot;https://blog.cloudflare.com/better-http-2-prioritization-for-a-faster-web/&quot;&gt;blog post here&lt;/a&gt; from &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; looking at the impact this prioritisation can have on actual user percieved performance. For the annotated dependency graph below I&#39;ve choosen the Chrome browser and the website GOV.UK as a test subject, since I know it &lt;a href=&quot;https://nooshu.com/blog/2020/07/14/bringing-http2-to-govuk-resources/&quot;&gt;now runs on HTTP/2&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The Dependency graph for GOV.UK from a Chrome browser.&quot; decoding=&quot;async&quot; height=&quot;452&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xjAHIg0NR_-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xjAHIg0NR_-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xjAHIg0NR_-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/xjAHIg0NR_-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This is quite a busy looking diagram so I will pull out a few features:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Each of the boxes corresponds to an individual resource, and the colour of the box corresponds with the key colours defined above the WPT waterfall chart&lt;/li&gt;&lt;li&gt;Top left of each box is a number that corresponds to the request number in the waterfall chart&lt;/li&gt;&lt;li&gt;Each resource is weighted by Chrome. The higher the number, the more important Chrome regards it&lt;/li&gt;&lt;li&gt;Multiple &#39;trees&#39; indicate multiple TCP connections used to download a sites resources&lt;/li&gt;&lt;li&gt;Resources on the left are requested first, moving across to the right for later requests&lt;/li&gt;&lt;li&gt;Resources from top to bottom indicate the dependencies. A file lower down the list depends on those above before downloading&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Where you see a &#39;fork&#39; in the tree (after request 10), this is Chrome shifting priorities as resources are discovered. Chrome uses a linear list to prioritise the download order, and WebPageTest only provides a single (complete) view of the tree. If a higher priority request is discovered it is then inserted into the list by giving it the parent of the next highest priority request. HTTP/2 will then move everything else to be under it because the &#39;Exclusive&#39; flag is set. Chrome is the only browser that uses this flag in this way as it forces the linear list pattern. This &#39;fork&#39; is the way WebPageTest displays changes to the list over time.&lt;/p&gt;&lt;p&gt;The dependency graph gives you some insight into how a browser handles the downloading of page resources. It is a fantastic learning tool, especially if you want to see the different strategies each of the browsers uses.&lt;/p&gt;&lt;h3 id=&quot;how-to-view-a-sites-http-2-dependency-graph-firefox&quot;&gt;How to view a sites HTTP/2 dependency graph (Firefox)&lt;/h3&gt;&lt;p&gt;In the previous section about the HTTP/2 dependency graph, I focused on the Chrome browser. But I also mentioned that &quot;Firefox builds a complex dependency tree and loads resources according to it&quot;. So lets take a look over the Firefox version of this graph and talk about what it is doing under the hood. As a Firefox user it saddens me that Firefox is now at &lt;a href=&quot;https://gs.statcounter.com/&quot;&gt;3.77% global usage&lt;/a&gt; (at the time of writing - Jan 2021), but examining another prioritisation implimentation will give you an idea of how complex this problem is, and how different vendors have choosen to tackle it.&lt;/p&gt;&lt;p&gt;The first thing to mention about this graph is that it is currently incomplete. There seems to be a bug in WebPageTest where it isn&#39;t capturing all the &quot;HTTP/2 Stream&quot; info for all of the assets. This causes the graphs to vary quite significantly between test runs. Because of this I&#39;m just going to focus on the general language and principles of the graph, and what they all mean. Apologies for the scale of the image, I recommend clicking on it to zoom in!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An annotated version of the Firefox H2 dependency graph.&quot; decoding=&quot;async&quot; height=&quot;95&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/0ZCrcwT_Zt-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/0ZCrcwT_Zt-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/0ZCrcwT_Zt-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/0ZCrcwT_Zt-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;First thing to mention is how diffirent it looks to the Chrome version (seen in the section above). Whereas Chrome has the &#39;Exclusive bit&quot; set for each of the resources (meaning bandwidth isn&#39;t shared between streams), Firefox does something very different. Firefox places each of the resources under a group with a priority weight assigned to it. So:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;UrgentStart: Highest priority group (weight: 240). Used for downloading the HTML.&lt;/li&gt;&lt;li&gt;Leader: Next highest group (weight: 200). Used for blocking CSS&lt;/li&gt;&lt;li&gt;Follow: Sub-group of leader that inherits its bandwidth allocation once leader assets have finished downloading. Used for images and fonts.&lt;/li&gt;&lt;li&gt;Other: This group has half the allocated bandwidth of leader (weight: 100). Used for JavaScript.&lt;/li&gt;&lt;li&gt;Background &amp; speculative: lowest priority groups (weight: 0). Once the network is idle the browse can use these for prefetches (so they don&#39;t interfere with the current page load).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The next important point to consider is about the relative weights of resources in the groups. So assets within groups will be downloaded concurrently, but an asset with a higher &lt;code&gt;weight&lt;/code&gt; value will be allocated more bandwidth (from what the group has already been allocated). So as you can probably tell, this prioritisation model gives incredibly fine grained control of the page load. FOr more information about HTTP/2 prioritisation (including Firefox) check out these blog posts from &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;: &lt;a href=&quot;https://calendar.perfplanet.com/2018/http2-prioritization/&quot;&gt;HTTP/2 Prioritization&lt;/a&gt; and &lt;a href=&quot;https://blog.cloudflare.com/better-http-2-prioritization-for-a-faster-web/&quot;&gt;Better HTTP/2 Prioritization for a Faster Web&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;how-to-view-the-console-log-information-from-a-test&quot;&gt;How to view the &lt;code&gt;console.log&lt;/code&gt; information from a test&lt;/h3&gt;&lt;p&gt;So when a test runs via a WebPageTest it woud be useful to know what is output in the browser &lt;code&gt;console&lt;/code&gt;. Very useful for debugging issues that may be seen in the resulting waterfall chart. There are two methods you can use.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;New method: Test result screenshot&lt;/strong&gt; So technically this isn&#39;t a new method. It used to be available in WebPageTest but was broken. Thanksfully Pat recently fixed it, so it makes this whole process much simpler. Thanks to &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for pointing this out.&lt;/p&gt;&lt;p&gt;On your test result page, simply navigate to the &#39;Screenshot` page in the secondary navigation. You will now be presented with a &#39;Console Log&#39; table at the bottom that lists the Source, Level, Message, URL, and Line number for each message in the console.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The console log panel gives information about massages in the console from the test.&quot; decoding=&quot;async&quot; height=&quot;253&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/PzSmgVbfHP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/PzSmgVbfHP-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/PzSmgVbfHP-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/PzSmgVbfHP-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Old method: Inject script + Custom metric&lt;/strong&gt; So I covered this method in my &lt;a href=&quot;https://nooshu.github.io/blog/2020/02/23/improving-perceived-performance-with-the-css-font-display-property/#debugging-a-webpagetest-run&quot;&gt;blog post about &#39;Improving perceived performance with the CSS &lt;code&gt;font-display&lt;/code&gt; property&#39;&lt;/a&gt;, but I will copy it across to this page for ease too.&lt;/p&gt;&lt;p&gt;If you want to see what the console output is for a browser in a test. Paste the following code into the &#39;Inject Script&#39; textarea:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// hijack the console.log method, allowing you to log all console massages&lt;/span&gt;
window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_consoleMessages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;_consoleMessages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;m&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// specifically used for this font example to &#39;copy&#39; the `FontFace` object&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; key &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        result&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the following into the &#39;Custom Metrics&#39; box under the &#39;Custom&#39;. Modify as needed:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;consoleMessages&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; font &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// push results into the hijacked console&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// could easily return a simple array though if required&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_consoleMessages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img alt=&quot;The custom metrics tab allows you to capture custom metrics from a test.&quot; decoding=&quot;async&quot; height=&quot;278&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/KpCOcAopRX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/KpCOcAopRX-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/KpCOcAopRX-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/KpCOcAopRX-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This will give you an output that looks similar to this:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The custom metrics can be found inside the test run, and they give a list of outputs captured during the test.&quot; decoding=&quot;async&quot; height=&quot;285&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/7ZdXhmj1Oo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/7ZdXhmj1Oo-300.webp 300w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/7ZdXhmj1Oo-600.webp 600w, https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/7ZdXhmj1Oo-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;This is very useful if you are looking for specific information about a browser in a test run.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;So there you have it, a brain dump into a blog post about some of the aspects of WebPageTest that I&#39;ve found a little mysterious. As this has been a learning exercise for me too, if there is anything I have misinterpreted, please do &lt;a href=&quot;https://hachyderm.io/@TheRealNooshu&quot;&gt;Mastodon me&lt;/a&gt; and let me know!&lt;/p&gt;&lt;p&gt;If you would like to learn a lot more about WebPageTest, I highly recommend the book: &quot;&lt;a href=&quot;http://shop.oreilly.com/product/0636920033592.do&quot;&gt;Using WebPageTest&lt;/a&gt;&quot; by &lt;a href=&quot;https://twitter.com/rick_viscomi&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rick Viscomi&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/marcelduran&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Marcel Duran&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;02/10/19: Initial post published.&lt;/li&gt;&lt;li&gt;11/10/19: Added waterfall for &#39;Firefox enhanced tracking protection&#39; scenario.&lt;/li&gt;&lt;li&gt;12/10/19: Added &#39;service worker precaching&#39; scenario.&lt;/li&gt;&lt;li&gt;13/10/19: Added &#39;Chrome Stair-Step&#39; scenario.&lt;/li&gt;&lt;li&gt;14/10/19: Added the &lt;code&gt;dns-prefetch&lt;/code&gt; scenario.&lt;/li&gt;&lt;li&gt;15/10/19: Added note about OCSP stapling and EV certificates (thanks &lt;a href=&quot;https://twitter.com/RyanTownsend&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Ryan Townsend&lt;/a&gt; for flagging).&lt;/li&gt;&lt;li&gt;16/10/19: Added information about download chunks (thanks &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for raising and checking). Clarified the error code presentation and added a &quot;Without OCSP&quot; waterfall chart for comparison (thanks &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;17/10/19: Added the &#39;HTML early flush&#39; scenario.&lt;/li&gt;&lt;li&gt;18/10/19: Added &lt;code&gt;prefetch&lt;/code&gt; scenario.&lt;/li&gt;&lt;li&gt;20/10/19: Added &lt;code&gt;prerender&lt;/code&gt; scenario (thanks &lt;a href=&quot;https://twitter.com/simonhearne&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Simon Hearne&lt;/a&gt; &amp; &lt;a href=&quot;https://twitter.com/RyanTownsend&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Ryan Townsend&lt;/a&gt; for the input).&lt;/li&gt;&lt;li&gt;24/10/19: Added new section called &#39;Hidden Gems&#39;, including &#39;How the filmstrip view and waterfall chart are related&#39;, &#39;How to generate customised waterfall chart images&#39;, and &#39;How to add custom marks using the User Timing API&#39; (Thanks again to &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for clarification with this).&lt;/li&gt;&lt;li&gt;08/12/19: Added the Long DOM content loaded (DCL) time scenario. Originally written for my article &#39;&lt;a href=&quot;https://calendar.perfplanet.com/2019/reading-a-webpagetest-waterfall-chart/&quot;&gt;Reading a WebPageTest Waterfall Chart&lt;/a&gt;&#39; on the &lt;a href=&quot;https://calendar.perfplanet.com/2019/&quot;&gt;Web Performance Calendar 2019&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;12/12/19: Added the never-ending waterfall scenario (thanks to &lt;a href=&quot;https://twitter.com/boostmarks&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Boris Schapira&lt;/a&gt; for the example).&lt;/li&gt;&lt;li&gt;20/12/19: Added a table of contents for easier navigation.&lt;/li&gt;&lt;li&gt;21/12/19: Added &#39;Rogue image downloads (HTTP/2 with SRI)&#39;, &#39;Requests with no response body&#39; (thanks &lt;a href=&quot;https://twitter.com/josephscott&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Joseph Scott&lt;/a&gt;), and &#39;Third party blocking JavaScript&#39; (thanks &lt;a href=&quot;https://twitter.com/mikeherchel&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Mike Herchel&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;03/01/20: Added information about good and bad HTTP/2 prioritisation (thanks &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/simevidas&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Šime Vidas&lt;/a&gt; for feedback), and &#39;How to display the bandwidth graph on mobile devices&#39; hidden gem (Thanks &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; &amp; &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;11/01/20: Added the &#39;Large Time to First Byte (TTFB)&#39; scenario.&lt;/li&gt;&lt;li&gt;12/01/20: Added the &#39;Inactivity after SSL negotiation&#39; scenario.&lt;/li&gt;&lt;li&gt;13/01/20: Added the &#39;Missing component times (DNS, Connect, TLS)&#39; scenario (thanks again &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for his input), and &#39;The impact of a CPU bottleneck&#39; scenario.&lt;/li&gt;&lt;li&gt;30/01/20: Added &#39;Consequences of a 301 redirect when changing a URL&#39; and &#39;The missing intermediate certificate&#39; scenarios. Thanks again to &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for the test and information about the latter.&lt;/li&gt;&lt;li&gt;22/06/20: Added information about the filmstrip borders, as layout shift is now highlighted (thanks &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; &amp; &lt;a href=&quot;https://twitter.com/rick_viscomi&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rick Viscomi&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;12/08/20: Added &#39;How to test a 404 page&#39; hidden gem. Added the &#39;Vary unusual waterfall&#39; scenario along with a link to my &lt;a href=&quot;https://nooshu.com/blog/2020/08/12/a-vary-unusual-waterfall/&quot;&gt;blog post&lt;/a&gt; with a more detailed examination.&lt;/li&gt;&lt;li&gt;13/08/20: Added information &#39;Inline script execution&#39; visualisation, and &#39;How to delve into the download chunk data&#39; (thanks to &lt;a href=&quot;https://twitter.com/radumicu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Radu Micu&lt;/a&gt; &amp; &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for fleshing out the details).&lt;/li&gt;&lt;li&gt;05/09/20: Added &#39;Identifying requests not from the main page document&#39; section.&lt;/li&gt;&lt;li&gt;22/09/20: Added &#39;How to view a sites HTTP/2 dependency graph&#39; (Thanks &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for clarification on the finer points), added &#39;Examining undefined 204 status codes&#39; scenario, and &#39;Service Worker precache slowing a page load&#39; (thanks &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt;). Update: Added tip from &lt;a href=&quot;https://twitter.com/jeffposnick&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Jeff Posnick&lt;/a&gt; on how to avoid the double cache download when using Workbox.&lt;/li&gt;&lt;li&gt;03/10/20: Added explanation of the difference between &#39;First Byte&#39; in the summary table, and the &#39;Time to First Byte&#39; metric listed in a HTML request details panel. Thanks go to &lt;a href=&quot;https://twitter.com/centminmod&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;George Liu&lt;/a&gt;, for the question and &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for the explanation.&lt;/li&gt;&lt;li&gt;08/11/20: Added the &#39;HTTP/1.1 and the &lt;code&gt;Connection: close&lt;/code&gt; header&#39; scenario and the autofill request information. Also added the &#39;Identifying a Cross-Origin Resource Sharing (CORS) connection&#39; scenario.&lt;/li&gt;&lt;li&gt;11/11/20: Added &#39;How to examine detailed information about images on a page&#39;, &#39;How to inspect the &#39;Browser Main Thread Part 1 &amp; 2&#39;, &#39;How to visualise the requests from your test&#39;, &#39;How to visualise variance in metrics across multiple tests&#39; hidden gems. Added the gray colour explanation into the &#39;Browser Main Thread&#39; section. Many thanks to &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for the suggestions and fielding all the random technical questions I had!&lt;/li&gt;&lt;li&gt;16/01/21: Added an update to the &#39;Missing component times (DNS, Connect, TLS)&#39; section. If you are missing connection information on Safari, read this section (Thanks &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;). Added the &#39;How to view the &lt;code&gt;console.log&lt;/code&gt; information from a test&#39; hidden gem (thanks &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for pointing it out to me).&lt;/li&gt;&lt;li&gt;17/01/21: Added update about the change to the JavaScript execution visualisation under the &#39;Updated JavaScript execution visualisation&#39; section. Added update referencing the addition of the &#39;Huge&#39; image size for the filmstrip view. Added information about the effect of &#39;Misconfigured CORS headers&#39;, and how it can be seen on a waterfall.&lt;/li&gt;&lt;li&gt;18/01/21: Added &#39;Eager prefetched&#39; scenario. Thanks to &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; for this information via &lt;a href=&quot;https://twitter.com/AndyDavies/status/1339137805503655936&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;this Twitter thread&lt;/a&gt;. Added &#39;How to analyse page images for optimal compression and render size&#39; section. Thanks to &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for pointing this out to me.&lt;/li&gt;&lt;li&gt;19/01/21: Added additional details about Chromes eager prefetch and how it is due to be fixed in Chrome 88. Thanks to &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for the feedback.&lt;/li&gt;&lt;li&gt;25/01/21: Added the &#39;The performance impact of &lt;code&gt;@font-face&lt;/code&gt; source failures&#39; scenario. Added the &#39;Double font downloads when using the &lt;code&gt;preload&lt;/code&gt; resource hint&#39; scenario. Added the &quot;SVG&#39;s opening up a new TCP connection&quot; scenario. Added the &#39;How to view a sites HTTP/2 dependency graph (Firefox)&#39; section (thanks &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for clarification on some points).&lt;/li&gt;&lt;li&gt;26/01/21: Added note that &quot;How to inspect the ‘Browser Main Thread&#39; - Part 1&quot; and &quot;How to inspect the ‘Browser Main Thread&#39; - Part 2&quot; only work in tests run in a Chromium-based browser (thanks &lt;a href=&quot;https://twitter.com/AndyDavies&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Andy Davies&lt;/a&gt; &amp; &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;09/02/21: Updated the timing value behind the &quot;Page is Interactive&quot; metric from 100ms to 50ms to match what is defined in the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Long_Tasks_API&quot;&gt;Long Tasks API&lt;/a&gt;. Thanks to &lt;a href=&quot;https://twitter.com/boostmarks&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Boris Schapira&lt;/a&gt; for raising and &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for confirming.&lt;/li&gt;&lt;li&gt;22/03/21: Updated the &#39;Vertical lines&#39; section as &#39;RUM First Paint&#39; was replased with &#39;First Contentful Paint&#39; and lines for CLS and LCP have been added. Thanks again to &lt;a href=&quot;https://twitter.com/patmeenan&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Pat Meenan&lt;/a&gt; for the clarifications.&lt;/li&gt;&lt;li&gt;05/01/23: Since the WebPageTest forum was shut down, links to it were broken. I&#39;ve now relinked them all to the &lt;a href=&quot;https://archive.org/web/&quot;&gt;WayBackMachine&lt;/a&gt;. Thanks to &lt;a href=&quot;https://twitter.com/rnebhwani&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Rockey Nebhwani&lt;/a&gt; for letting me know this was an issue!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Speeding up the web with the Save-Data header</title>
    <link href="https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/" />
    <updated>2019-09-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/</id>
    <content type="html">&lt;p&gt;One of the advantages of running a small blog that is hardly read by anyone is the ability to experiment with new technology and not have to worry about the potential impact on avid readers (hah!). So this weekend I decided to have a play with the not so new &lt;code&gt;Save-Data&lt;/code&gt; header (initial specification August 2015).&lt;/p&gt;&lt;h2 id=&quot;what-is-it&quot;&gt;What is it?&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;Save-Data&lt;/code&gt; header field is a request header that indicates a client&#39;s preference for reduced data usage. Essentially it is a setting that a user can enable that allows less data to be transferred over the connection (assuming a website is setup to adapt to it). A request from a client looks like this:&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;GET /image.jpg HTTP/1.0
Host: example.com
Save-Data: on&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note the &lt;code&gt;Save-Data&lt;/code&gt; header. A lot of what can happen at this point is up to the server. It sees the preference and can adapt accordingly.&lt;/p&gt;&lt;p&gt;According to &lt;a href=&quot;https://twitter.com/brend0&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Brendan Abbott&lt;/a&gt;, a developer at Shopify: &quot;20% of Indian/Brazilian requests contain this header&quot;. Those are two huge markets. That&#39;s a lot of people requesting to use less data while browsing the web!&lt;/p&gt;&lt;h2 id=&quot;adapting-using-javascript&quot;&gt;Adapting using JavaScript&lt;/h2&gt;&lt;p&gt;Now it is possible for the server to serve completely different markup depending on if the header is on or off (more on this later). But what about if you don&#39;t have that much control over the server setup? This is the situation I am in at the moment as I&#39;m using &lt;a href=&quot;https://pages.github.com/&quot;&gt;GitHub Pages&lt;/a&gt; for hosting. Thankfully there is a way to detect this user preference using JavaScript (thanks go to &lt;a href=&quot;https://timkadlec.com/&quot;&gt;Tim Kadlec&lt;/a&gt; for &lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/save-data/&quot;&gt;pointing me in the right direction&lt;/a&gt; with this!)&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;connection&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;saveData &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documentElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;classList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;save-data&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Add the above snippet of code in your &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; and when a browser has &quot;Data Saving Mode&quot; enabled, the &lt;code&gt;save-data&lt;/code&gt; class will be appended to the &lt;code&gt;&amp;lt;html&gt;&lt;/code&gt; element. If not enabled it is simply ignored.&lt;/p&gt;&lt;p&gt;So this is exactly what I have done with this website. If you enable &quot;Lite mode&quot; in Google Chrome or &quot;Turbo&quot; in Opera, less data will be sent when viewing the site.&lt;/p&gt;&lt;h2 id=&quot;testing&quot;&gt;Testing&lt;/h2&gt;&lt;p&gt;Before I tell you what I changed lets have a look at the difference it has made. All tests were conducted using &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;Web Page Test&lt;/a&gt; as it comes with a setting to include the header (Advanced settings --&gt; Chromium --&gt; Enable Data Reduction). The tests were conducted on a 3G Fast connection using a real &lt;a href=&quot;https://www.gsmarena.com/motorola_moto_g4-8103.php&quot;&gt;Moto G4 phone&lt;/a&gt;. This is a well known phone in the web performance community. It&#39;s now considered &quot;legacy&quot; (in the First World), but it still has a very large user base across the globe. So it is a great device to use as a performance baseline.&lt;/p&gt;&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;&lt;h3 id=&quot;homepage&quot;&gt;Homepage&lt;/h3&gt;&lt;p&gt;Key metrics pulled out from the WPT results for the homepage:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Without Save-Data&lt;/th&gt;&lt;th&gt;With Save-Data&lt;/th&gt;&lt;th&gt;diff (%)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Visually Complete (ms)&lt;/td&gt;&lt;td&gt;2,007&lt;/td&gt;&lt;td&gt;1,607&lt;/td&gt;&lt;td&gt;-20&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fully Loaded (ms)&lt;/td&gt;&lt;td&gt;3,900&lt;/td&gt;&lt;td&gt;2,940&lt;/td&gt;&lt;td&gt;-25&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Number of Requests&lt;/td&gt;&lt;td&gt;25&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;-60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bytes transferred&lt;/td&gt;&lt;td&gt;240,008&lt;/td&gt;&lt;td&gt;149,095&lt;/td&gt;&lt;td&gt;-38&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id=&quot;blog-post&quot;&gt;Blog post&lt;/h3&gt;&lt;p&gt;Key metrics pulled out from the WPT results for a &lt;a href=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/&quot;&gt;blog post&lt;/a&gt;:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Without Save-Data&lt;/th&gt;&lt;th&gt;With Save-Data&lt;/th&gt;&lt;th&gt;diff (%)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Visually Complete (ms)&lt;/td&gt;&lt;td&gt;3,398&lt;/td&gt;&lt;td&gt;1,640&lt;/td&gt;&lt;td&gt;-52&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fully Loaded (ms)&lt;/td&gt;&lt;td&gt;5,263&lt;/td&gt;&lt;td&gt;3,748&lt;/td&gt;&lt;td&gt;-29&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Number of Requests&lt;/td&gt;&lt;td&gt;30&lt;/td&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;-50&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bytes transferred&lt;/td&gt;&lt;td&gt;490,917&lt;/td&gt;&lt;td&gt;399,936&lt;/td&gt;&lt;td&gt;-19&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id=&quot;observations&quot;&gt;Observations&lt;/h3&gt;&lt;p&gt;As you can probably see there are some huge percentage differences in the results. The time for visually complete and fully loaded dropped in the range of 20 to 52%! The number of bytes transferred dropped for the homepage and blog post by 38% and 19% respectively.&lt;/p&gt;&lt;p&gt;One of the more impressive observations (for me) is when you switch the Web Page Test comparison filmstrip view to use an interval of 60fps, the &lt;code&gt;Save-Data&lt;/code&gt; enabled view renders completely in a single frame (or ~16.6ms). That&#39;s compared to a large number of frames when disabled (see image below):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Save-Data enabled rendering a page in 1 frame (16.6ms)&quot; decoding=&quot;async&quot; height=&quot;92&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/C6s8DBIowm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/C6s8DBIowm-300.webp 300w, https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/C6s8DBIowm-600.webp 600w, https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/C6s8DBIowm-814.webp 814w&quot; title=&quot;Rendering the page view in 1 frame (~16.6ms)&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;so-what-was-changed&quot;&gt;So what was changed?&lt;/h2&gt;&lt;p&gt;By detecting the &lt;code&gt;Sava-Data&lt;/code&gt; preference using JavaScript and appending a &lt;code&gt;save-data&lt;/code&gt; class to the &lt;code&gt;&amp;lt;html&gt;&lt;/code&gt; tag, we now have a few options available to optimise the website using only CSS changes.&lt;/p&gt;&lt;h3 id=&quot;no-more-background-images&quot;&gt;No more background images&lt;/h3&gt;&lt;p&gt;All background images were removed (or they were replaced with a solid colour). It&#39;s a simple task:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.example-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&#39;images/example-background.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; top left no-repeat&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.save-data .example-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By setting the background to &lt;code&gt;none&lt;/code&gt; with a more specific selector, the image won&#39;t be downloaded by the browser (since it is no longer needed). I had a number of background images all over the site so it saved a fair few bytes and requests.&lt;/p&gt;&lt;h3 id=&quot;remove-the-web-font&quot;&gt;Remove the web font&lt;/h3&gt;&lt;p&gt;A large asset in many sites is the web font(s) used. This is especially true if using older versions like WOFF or EOT. Thankfully you can disable a font download using CSS alone:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.example-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Dosis&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Geneva&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Tahoma&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Verdana&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.save-data .example-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Geneva&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Tahoma&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Verdana&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By removing the reference to the web font, the browser will no longer download it (even though it is still defined using &lt;code&gt;@font-face&lt;/code&gt;). The key to getting this to work is to make sure you don&#39;t have a single reference to the font anywhere on the page. As soon as it is referenced by a &lt;code&gt;font-family&lt;/code&gt; property, it will be downloaded!&lt;/p&gt;&lt;h3 id=&quot;disable-the-service-worker&quot;&gt;Disable the Service Worker&lt;/h3&gt;&lt;p&gt;I have a &lt;a href=&quot;https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/&quot;&gt;service worker running on this site&lt;/a&gt; that is automatically generated using &lt;a href=&quot;https://developers.google.com/web/tools/workbox/&quot;&gt;Workbox.js&lt;/a&gt;. The way I have the service worker setup is to precache a whole bunch of assets when a user first hits the page (font &amp; images mainly).&lt;/p&gt;&lt;p&gt;One of the major downsides in doing this is a user will potentially be doubling up on the number of requests and bytes downloaded. The reason for this is because the browsers HTTP cache and the service worker cache are totally different, and they don&#39;t share the assets downloaded.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Service worker precache doubling requests for assets&quot; decoding=&quot;async&quot; height=&quot;456&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/AnZP1vro8S-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/AnZP1vro8S-300.webp 300w, https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/AnZP1vro8S-600.webp 600w, https://nooshu.com/blog/2019/09/01/speeding-up-the-web-with-save-data-header/AnZP1vro8S-814.webp 814w&quot; title=&quot;Precache doubling the requests and bytes to specified assets.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The first set of downloads primes the standard browser (HTTP) cache. Once the service worker registers and activates the service worker cache is primed (hence duplicating requests).&lt;/p&gt;&lt;p&gt;So by wrapping the service worker registration in a similar JavaScript check we used when adding the CSS classname, we can stop the service worker from registering:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;serviceWorker&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* by default we want to load the SW */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; loadTheSW &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/* check for connection support and if they want to save data */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;connection&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; navigator &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;saveData &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;/* they support connection, and want to save data, don&#39;t load the SW */&lt;/span&gt;
        loadTheSW &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/* decide what to do with the SW */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;loadTheSW&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;load&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceWorker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/sw.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;registration&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;SW registered: &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; registration&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;registrationError&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;SW registration failed: &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; registrationError&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The registration is stopped therefore the activation and preloading of assets. This also has the added advantage of stopping the Workbox production JavaScript from downloading too (since it is listed as an &lt;code&gt;importScripts()&lt;/code&gt; in the &lt;code&gt;sw.js&lt;/code&gt; file).&lt;/p&gt;&lt;h3 id=&quot;remove-tracking-and-any-additional-javascript&quot;&gt;Remove tracking (and any additional JavaScript)&lt;/h3&gt;&lt;p&gt;This isn&#39;t something I have done yet, as I&#39;ve just added a custom dimention in Google Analytics to check the percentage of users coming to the website with the &lt;code&gt;Save-Data&lt;/code&gt; header present. But in a couple of months time I will wrap the GA code in the same check as the service worker above. Users who want to save data will no longer be required to download any tracking code (plus the additional DNS / TCP / TLS overhead involved).&lt;/p&gt;&lt;p&gt;This can easily be extended to any JavaScript on a page. Assuming you have a good no-JavaScript fallback for a component (you are using Progressive Enhancement after all aren&#39;t you?), you could just &lt;a href=&quot;https://stackoverflow.com/a/7719185&quot;&gt;not load the JavaScript&lt;/a&gt; it requires.&lt;/p&gt;&lt;h3 id=&quot;serve-smaller-images&quot;&gt;Serve smaller images&lt;/h3&gt;&lt;p&gt;Another tactic that I haven&#39;t adopted due to the limitations of GitHub pages, but you could quite easily serve a different set of smaller images to users who want to save data.&lt;/p&gt;&lt;p&gt;One way to do this would be to serve a totally different HTML file for users wanting less data. For example using an Apache &lt;code&gt;.htaccess&lt;/code&gt; file you could do this (Thanks go to &lt;a href=&quot;https://twitter.com/tunetheweb&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Barry Pollard&lt;/a&gt; for this code):&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;RewriteCond &quot;%{HTTP:save-data}&quot; &quot;on&quot; [NC]
RewriteCond /www/htdocs/%{REQUEST_URI}/index.html.small -f
RewriteRule ^(.*)/$ &quot;$1/index.html.small&quot; [END]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Within the &lt;code&gt;index.html.small&lt;/code&gt; you could have references to a completely different set of (smaller) images. There are other ways of doing this too, so I recommended reading &lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/save-data/&quot;&gt;Delivering Fast and Light Applications with Save-Data&lt;/a&gt; for alternative methods.&lt;/p&gt;&lt;h2 id=&quot;browser-support&quot;&gt;Browser support&lt;/h2&gt;&lt;p&gt;So this question comes up for every &quot;new&quot; feature. What does the browser support look like? Well &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data#Browser_compatibility&quot;&gt;according to MDN&lt;/a&gt;, at the time of writing it is mainly Chrome and Opera (on Desktop and Android) that support the &lt;code&gt;Save-Data&lt;/code&gt; header. &lt;a href=&quot;https://www.samsung.com/uk/apps/samsung-internet/&quot;&gt;Samsung Internet&lt;/a&gt; is also listed as a &quot;Yes&quot;, but I couldn&#39;t see a way of enabling it in the options when I looked on my phone (version 9.4). I&#39;d love to know if this option is available or coming soon to SI.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;So with a little bit of JavaScript, backend code and some thought, it is possible to deliver your website using less data. This is extremely important for the &lt;a href=&quot;https://www.blog.google/technology/next-billion-users/&quot;&gt;next billion users&lt;/a&gt; i.e. users on older devices and expensive data plans who want to access your content but simply can&#39;t due to the &lt;a href=&quot;https://httparchive.org/reports/state-of-the-web#bytesTotal&quot;&gt;ever expanding bloat&lt;/a&gt; on the web today. So why not give a user the option to use less data for your website? After all, &lt;a href=&quot;https://timkadlec.com/remembers/2019-08-30-less-data-doesnt-mean-a-lesser-experience/&quot;&gt;less data doesn&#39;t mean a lesser experience&lt;/a&gt;!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Lighthouse score of 100: not always for the better</title>
    <link href="https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/" />
    <updated>2019-08-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/</id>
    <content type="html">&lt;p&gt;Back in December of 2018 &lt;a href=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/&quot;&gt;I wrote a blog&lt;/a&gt; post about joining an entirely made-up club for websites that achieve a score of 100 for performance in &lt;a href=&quot;https://developers.google.com/web/tools/lighthouse/&quot;&gt;Google Lighthouse&lt;/a&gt;. So I&#39;m now writing a follow-up post about removing myself from said super-exclusive club. Here&#39;s why.&lt;/p&gt;&lt;h2 id=&quot;that-warm-and-fuzzy-feeling&quot;&gt;That warm and fuzzy feeling&lt;/h2&gt;&lt;p&gt;Achieving a perfect score, 100/100 is always a good feeling. But you need to be cautious with automated tools like Lighthouse. What you think is the best you can achieve because a set of computer algorithms has crunched the numbers and output a score, may not be the case at all. It&#39;s always a good idea to do some manual checking yourself. After all, computers can sometimes be wrong (shock horror)!&lt;/p&gt;&lt;p&gt;What tools like Lighthouse do really well is identify where you may have potential problems. The low-hanging fruit that go against best practice. If these are fixed it &lt;em&gt;should&lt;/em&gt; improve your websites performance. If you are interested in how Lighthouse calculates the scores for each of the audits, you can see it all here in the &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1up5rxd4EMCoMaxH8cppcK1x76n6HLx0e7jxb0e0FXvc/edit#gid=0&quot;&gt;Lighthouse v5 Score Weighting spreadsheet&lt;/a&gt;. Using this spreadsheet you can easily identify where your site is failing to meet the criteria for a certain score and try to improve it. It&#39;s worth noting the weighting attached to each metric too, so you can concentrate on the metric with the most weight (for performance it is Time to Interactive at the time of writing).&lt;/p&gt;&lt;h2 id=&quot;a-throne-of-lies&quot;&gt;A throne of lies!&lt;/h2&gt;&lt;p&gt;The above header is very harsh. But it is entirely possible to game the system. Manuel Matuzovic wrote a blog post in May about &lt;a href=&quot;https://www.matuzo.at/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/&quot;&gt;how to build the most inaccessible website with a perfect Lighthouse score&lt;/a&gt;. The basic idea is to make sure you meet the criteria the automated tool is testing for, but implement them entirely incorrectly. The automated test gives you a perfect score (you tick all the boxes after all), but the poor users trying to use the website who are doing your manual testing discover the truth. The throne of lies!&lt;/p&gt;&lt;p&gt;As Manuel mentions in his post, this isn&#39;t an issue at all with Lighthouse. You can do this with many automated testing tools. It&#39;s about us as developers not blindly accepting the results of automated tools. Use them as a guide, but also do some manual testing to make sure you are interpreting the scores correctly.&lt;/p&gt;&lt;h2 id=&quot;my-issue-font-preloading&quot;&gt;My issue: Font preloading&lt;/h2&gt;&lt;p&gt;Preloading allows a developer to tell a browser there are assets on the page that are very high priority. These assets should be loaded as soon as possible. At some point in the page lifecycle these assets are going to be used, so load them right now. This technique is very often used with WOFF2 webfonts like so:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/assets/font.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The font is bumped up the priority list and is loaded immediately after the HTML. Without the preload, the browser needs to download the CSS, parse it, create the CSSOM, and only then the webfont is downloaded. At this point the page is rendered. In theory preloading should be quicker in terms of rendering the font to the page (since the font is already in the cache when it is needed). But preloading has the side-effect of essentially adding bytes to the browsers critical render path, and by doing this you could actually be slowing your page performance down!&lt;/p&gt;&lt;p&gt;So I tested the homepage of this website with and without webfont preloading used.&lt;/p&gt;&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;&lt;p&gt;When auditing a page using Lighthouse it can throttle both the CPU and the network to emulate a mobile device on a slower network. I couldn&#39;t replicate the CPU throttling in Web Page Test, but I could use the same network settings. These are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;562.5ms HTTP Round Trip Time (RTT)&lt;/li&gt;&lt;li&gt;1,474.6 Kbps download speed&lt;/li&gt;&lt;li&gt;675 Kbps upload speed&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Although not perfect, this will allow for a closer comparison of the results between Lighthouse and Web Page Test.&lt;/p&gt;&lt;h2 id=&quot;results-without-font-preloading&quot;&gt;Results without font preloading&lt;/h2&gt;&lt;p&gt;With font preloading removed the site only gets a score of 99/100 in Lighthouse.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Lighthouse score without font preloading: 99/100&quot; decoding=&quot;async&quot; height=&quot;270&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/3fpL0EQIIb-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/3fpL0EQIIb-300.webp 300w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/3fpL0EQIIb-600.webp 600w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/3fpL0EQIIb-814.webp 814w&quot; title=&quot;99/100 for performance, well that isn&#39;t good is it!&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You can see the full results for this &lt;a href=&quot;https://googlechrome.github.io/lighthouse/viewer/?gist=8f4ecb04773f3d4f453836e703bd7d57&quot;&gt;audit without font preloading here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Running the site through Web Page Test you can see where the font would usually be downloaded by the browser (request number 11). After the HTML, CSS and parsing is complete.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Standard font download priority can be seen at request number 11.&quot; decoding=&quot;async&quot; height=&quot;234&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/UtYC4qXl66-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/UtYC4qXl66-300.webp 300w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/UtYC4qXl66-600.webp 600w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/UtYC4qXl66-814.webp 814w&quot; title=&quot;Font download is number 11 in the waterfall when preload isn&#39;t used.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You can see the full results for this &lt;a href=&quot;https://www.webpagetest.org/result/190817_V4_8dd2a77c48d2f3823da3220bc4d4a0cc/8/details/&quot;&gt;Web Page Test without font preloading here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;results-with-font-preloading&quot;&gt;Results with font preloading&lt;/h2&gt;&lt;p&gt;With font preloading the website gets a perfect score of 100/100 for performance in Lighthouse.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Lighthouse score with font preloading: 100/100&quot; decoding=&quot;async&quot; height=&quot;270&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/ktzz0GmxlK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/ktzz0GmxlK-300.webp 300w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/ktzz0GmxlK-600.webp 600w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/ktzz0GmxlK-814.webp 814w&quot; title=&quot;100/100 for performance, what a great result!&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You can see the full results for this &lt;a href=&quot;https://googlechrome.github.io/lighthouse/viewer/?gist=53289e3e0b9b7774ca4b3e0dbde68ed0&quot;&gt;audit with font preloading here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Again, running the site through Web Page Test it is possible to see the font preloading in action. Request number 2 (directly after the HTML has downloaded and parsed), is the font preloading in action.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Preload can be seen in request 2.&quot; decoding=&quot;async&quot; height=&quot;139&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/2iiuJwi5Kv-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/2iiuJwi5Kv-300.webp 300w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/2iiuJwi5Kv-600.webp 600w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/2iiuJwi5Kv-814.webp 814w&quot; title=&quot;Font download is number 2 in the waterfall.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;You can see the full results for this &lt;a href=&quot;https://www.webpagetest.org/result/190817_7J_4782ef059bab11b287c1378512ce32ef/9/details/&quot;&gt;Web Page Test with font preloading here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;comparing-results&quot;&gt;Comparing results&lt;/h2&gt;&lt;p&gt;So lets pull some key metrics out from these tests and compare the results:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Without Preload (ms)&lt;/th&gt;&lt;th&gt;With Preload (ms)&lt;/th&gt;&lt;th&gt;diff (%)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Start render&lt;/td&gt;&lt;td&gt;3200&lt;/td&gt;&lt;td&gt;3834&lt;/td&gt;&lt;td&gt;+19.8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;First Contentful Paint&lt;/td&gt;&lt;td&gt;3199&lt;/td&gt;&lt;td&gt;3819&lt;/td&gt;&lt;td&gt;+19.4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Speed Index&lt;/td&gt;&lt;td&gt;3411&lt;/td&gt;&lt;td&gt;3962&lt;/td&gt;&lt;td&gt;+16.2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Document complete&lt;/td&gt;&lt;td&gt;4551&lt;/td&gt;&lt;td&gt;4914&lt;/td&gt;&lt;td&gt;+8.0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fully Loaded&lt;/td&gt;&lt;td&gt;7062&lt;/td&gt;&lt;td&gt;7422&lt;/td&gt;&lt;td&gt;+5.1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Visually complete&lt;/td&gt;&lt;td&gt;7700&lt;/td&gt;&lt;td&gt;8200&lt;/td&gt;&lt;td&gt;+6.5&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Lighthouse Score&lt;/td&gt;&lt;td&gt;99/100&lt;/td&gt;&lt;td&gt;100/100&lt;/td&gt;&lt;td&gt;+1&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;As you can most likely see from the table, all the metrics go up, not down. By preloading the font we&#39;ve actually made page performance worse. The start render time for example is 20% slower (+600ms)!&lt;/p&gt;&lt;p&gt;You can clearly see this in the &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=190817_7J_4782ef059bab11b287c1378512ce32ef%2C190817_V4_8dd2a77c48d2f3823da3220bc4d4a0cc&amp;thumbSize=200&amp;ival=100&amp;end=visual&quot;&gt;Web Page Test side-by-side comparison&lt;/a&gt;. The filmstrip &quot;without preloading&quot; starts way before &quot;with preloading&quot;. And it is easy to see what is happening if you compare the waterfalls:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Two waterfall graphs compared when preload is used.&quot; decoding=&quot;async&quot; height=&quot;586&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/Y1IGSQ0Ti1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/Y1IGSQ0Ti1-300.webp 300w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/Y1IGSQ0Ti1-600.webp 600w, https://nooshu.com/blog/2019/08/18/lighthouse-score-100-not-always-for-the-better/Y1IGSQ0Ti1-814.webp 814w&quot; title=&quot;Font preloading bumps all other requests further along the waterfall.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It&#39;s exactly as described above. Preloading the font has added bytes to the critical render path and all other assets have been bumped along the waterfall (requests 6-11). This means the CSS is delayed by approximately 600ms, and all other metrics suffer because of this delay.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;So in this particular case, font preloading actually makes performance worse, even though Lighthouse suggests otherwise. Comparing the two audits against the &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1up5rxd4EMCoMaxH8cppcK1x76n6HLx0e7jxb0e0FXvc/edit#gid=0&quot;&gt;Lighthouse v5 Score Weighting sheet&lt;/a&gt;, it looks to lose 1 point because Time to Interactive jumps from 1.7 seconds (preload) to 2.2 seconds (no preload). This pushes it into the &quot;Score of 99&quot; bracket. Since TTI counts for 33.3% of the category weight it makes quite a difference to the overall score. What&#39;s more confusing is Web Page Test suggests the exact opposite. Note the green bar at the bottom of waterfalls (Page is Interactive):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;WPT TTI no preload ~ 3200ms&lt;/li&gt;&lt;li&gt;WPT TTI with preload ~ 3800ms&lt;/li&gt;&lt;li&gt;Lighthouse TTI no preload ~ 2200ms&lt;/li&gt;&lt;li&gt;Lighthouse TTI with preload ~ 1700ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;m unsure which value is actually correct in this case. I would lean towards WPT if I were pushed to make a decision. If anyone has any information on what is going on here let me know &lt;a href=&quot;https://twitter.com/TheRealNooshu&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;on Twitter&lt;/a&gt;, as I&#39;d love to know!&lt;/p&gt;&lt;p&gt;Since this is all synthetic testing, it&#39;s hard to tell if this is this is true for all devices and connection combinations. RUM testing would give much more information to draw wider conclusions, but that seems a little overkill for my little blog though. So I will be removing font preloading from these pages.&lt;/p&gt;&lt;p&gt;As mentioned earlier, this isn&#39;t an issue in the way that Lighthouse is reporting the score. It&#39;s how the score is being interpreted. It&#39;s important to remember that automated testing tools can only take you so far. Don&#39;t take them as gospel, they aren&#39;t perfect. Use them to check a large number of pages quickly to look for obvious issues. But also remember to do some manual testing too, ideally with users and the devices they use.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Why we focus on frontend performance</title>
    <link href="https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/" />
    <updated>2019-04-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This post was originally posted on the &lt;a href=&quot;https://gdstechnology.blog.gov.uk/&quot;&gt;GDS Technology blog&lt;/a&gt;. It can be &lt;a href=&quot;https://technology.blog.gov.uk/2019/04/18/why-we-focus-on-frontend-performance/&quot;&gt;viewed here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;img alt=&quot;GOV.UK website viewed on a laptop.&quot; decoding=&quot;async&quot; height=&quot;542&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/ixiLAj960t-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/ixiLAj960t-300.webp 300w, https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/ixiLAj960t-600.webp 600w, https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/ixiLAj960t-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;We&#39;ve recently updated the &lt;a href=&quot;https://www.gov.uk/service-manual/technology/how-to-test-frontend-performance&quot;&gt;Service Manual guidance&lt;/a&gt; to help developers across government identify and test performance issues before they become a problem for users.&lt;/p&gt;&lt;p&gt;It&#39;s part of an ongoing effort to make sure GOV.UK is as inclusive as possible. Research by Google in 2016 found &lt;a href=&quot;https://www.thinkwithgoogle.com/marketing-resources/data-measurement/mobile-page-speed-new-industry-benchmarks/&quot;&gt;53% of users abandoned&lt;/a&gt; a mobile site if it took more than 3 seconds to load. For government, GOV.UK is often the only place a user can get information. If the website were to perform badly, we become a single point of failure.&lt;/p&gt;&lt;h2 id=&quot;knowing-more-about-our-users&quot;&gt;Knowing more about our users&lt;/h2&gt;&lt;p&gt;To help people get what they need from GOV.UK as quickly as possible, we need to understand the context and environments our users are in.&lt;/p&gt;&lt;h3 id=&quot;different-devices&quot;&gt;Different devices&lt;/h3&gt;&lt;p&gt;Not all users can afford high-end devices and we&#39;ve found cheaper devices with entry-level hardware already struggle with today&#39;s web. We regularly assess what &lt;a href=&quot;https://www.gov.uk/service-manual/technology/designing-for-different-browsers-and-devices&quot;&gt;devices and browsers people use to access GOV.UK&lt;/a&gt; so we can make sure our frontend works effectively on all of them.&lt;/p&gt;&lt;h3 id=&quot;varying-connectivity&quot;&gt;Varying connectivity&lt;/h3&gt;&lt;p&gt;Even if a user does have a modern device, connection speeds across the UK vary.&lt;/p&gt;&lt;p&gt;While the &lt;a href=&quot;https://www.cable.co.uk/broadband/speed/worldwide-speed-league/&quot;&gt;UK average broadband speed in 2018 was 18.57 Mbps&lt;/a&gt; there can be a substantial variation in the speed of internet connections. For example, even residents in London, a city with global connections and infrastructure, can have poor internet connections. In so-called ‘not-spots&#39;, residents have reported &lt;a href=&quot;https://www.london.gov.uk/sites/default/files/digital_connectivity_report_final.pdf&quot;&gt;connection speeds as slow as 0.26Mbps&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There are also users who cannot even rely on their mobile data coverage in these situations as &lt;a href=&quot;https://www.ofcom.org.uk/__data/assets/pdf_file/0024/108843/summary-report-connected-nations-2017.pdf&quot;&gt;15% of indoor spaces in the UK have no data coverage&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;And, even if a user can get data coverage, lots of people in the UK are often excluded from accessing websites because the pages have lots of assets, which causes them to load and run slowly. By compressing assets we can reduce the data transfer needed and better serve users with low connectivity and older devices.&lt;/p&gt;&lt;h3 id=&quot;data-use-can-be-expensive&quot;&gt;Data use can be expensive&lt;/h3&gt;&lt;p&gt;In addition, &lt;a href=&quot;https://www.ofcom.org.uk/__data/assets/pdf_file/0030/113898/pricing-report-2018.pdf&quot;&gt;Ofcom statistics show the average monthly mobile data plan&lt;/a&gt; in the UK costs £18.36. This plan gives users 1.9GB of data per month. Considering the &lt;a href=&quot;https://httparchive.org/reports/page-weight&quot;&gt;median mobile page weight is almost 1.7MB&lt;/a&gt;, this data allowance does not stretch far over this time period.&lt;/p&gt;&lt;p&gt;By reducing the size of webpages we can improve the data consumption and associated battery life of the user&#39;s device.&lt;/p&gt;&lt;h3 id=&quot;user-stress&quot;&gt;User stress&lt;/h3&gt;&lt;p&gt;On top of this, poor website performance can increase a users stress level and damage their productivity. A study by Glasgow Caledonian University found that &lt;a href=&quot;http://www.tecnostress.it/wp-content/uploads/2010/02/final_webstress_survey_report_229296.pdf&quot;&gt;participants had to concentrate 50% more&lt;/a&gt; when trying to complete a simple task on a website using a slow connection. This extra concentration needed to perform a task led to an increase in overall stress levels.&lt;/p&gt;&lt;p&gt;When GOV.UK is the only place our users can get government information and services, slow web performance is unacceptable.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;GOV.UK register to vote viewed on a mobile.&quot; decoding=&quot;async&quot; height=&quot;542&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/BVJEncVTYu-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/BVJEncVTYu-300.webp 300w, https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/BVJEncVTYu-600.webp 600w, https://nooshu.com/blog/2019/04/18/why-we-focus-on-frontend-performance/BVJEncVTYu-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;what-we-re-doing-to-improve-performance&quot;&gt;What we&#39;re doing to improve performance&lt;/h2&gt;&lt;p&gt;As well as providing guidance on how developers across government can improve the performance of their frontend work today, we&#39;re also looking at wider technology choices for all GOV.UK frontend development that will help in future.&lt;/p&gt;&lt;p&gt;To do this, we&#39;re running a series of investigations to find out what technologies have a significant impact on web performance so we can prioritise future work.&lt;/p&gt;&lt;p&gt;We&#39;ve previously discussed &lt;a href=&quot;https://technology.blog.gov.uk/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/&quot;&gt;our work on font loading&lt;/a&gt;, and we&#39;re also looking into strategies such as:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;using HTTP/2, which is now supported by all major browsers to optimise asset delivery to users&#39; devices&lt;/li&gt;&lt;li&gt;updating fonts, for better overall compression to reduce data usage&lt;/li&gt;&lt;li&gt;applying modern compression methods, such as &lt;a href=&quot;https://github.com/google/brotli&quot;&gt;Brotli&lt;/a&gt;, to reduce the size of our pages&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We plan to blog about our findings as the investigations progress and will update our &lt;a href=&quot;https://www.gov.uk/service-manual/technology/how-to-test-frontend-performance&quot;&gt;frontend performance guidance&lt;/a&gt; as we learn more. In doing so, we hope developers across government can learn from what we&#39;ve started and help us identify what to research next.&lt;/p&gt;&lt;h3 id=&quot;performance-is-everyones-responsibility&quot;&gt;Performance is everyone&#39;s responsibility&lt;/h3&gt;&lt;p&gt;Web performance can mean the difference in someone being able to get what they need from government or abandoning their task completely.&lt;/p&gt;&lt;p&gt;Performance is therefore an integral part of the service we provide and every member of a service team should be involved in the optimisation process. Even minor changes can make a huge difference for our users.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Joining the 100 club (#webperf)</title>
    <link href="https://nooshu.com/blog/2018/12/06/joining-the-100-club/" />
    <updated>2018-12-06T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2018/12/06/joining-the-100-club/</id>
    <content type="html">&lt;p&gt;So a few months back we made a couple of changes to GOV.UK to &lt;a href=&quot;https://gdstechnology.blog.gov.uk/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/&quot;&gt;improve frontend performance&lt;/a&gt; for all users. I&#39;d been pushing to make this change for a number of months, but unfortunately changes in government tend to take quite a while. So although it was a fairly simple change, the impact it had on other GDS projects was noticeable. This increased the implementation timeline quite considerably. Since this change was made we&#39;ve been looking to improve other areas of the website to make it even faster. In doing so I&#39;ve been doing a lot of reading and experimenting with web performance. That got me thinking &quot;I wonder how well my website performs?&quot;.&lt;/p&gt;&lt;h2 id=&quot;initial-results&quot;&gt;Initial results&lt;/h2&gt;&lt;p&gt;I used an excellent tool called &lt;a href=&quot;https://developers.google.com/web/tools/lighthouse/&quot;&gt;Google Lighthouse&lt;/a&gt; that is built into Chrome to check the overall performance of the site. The initial results were good, but not perfect. The tests were conducted on the default Lighthouse settings for mobile with &quot;Applied Fast 3G, 4x CPU Slowdown&quot; selected. You can see the &lt;a href=&quot;https://googlechrome.github.io/lighthouse/viewer/?gist=e185d8f59f07e714f7f188a0f12b45e4&quot;&gt;full results from the audit here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Lighthouse score for this website before optimisation.&quot; decoding=&quot;async&quot; height=&quot;621&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/zkGvPSIVDE-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/zkGvPSIVDE-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/zkGvPSIVDE-600.webp 600w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/zkGvPSIVDE-814.webp 814w&quot; title=&quot;Performance score 97/100. Good but not perfect.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see the score was 97 out of 100. It&#39;s a decent score, but where were the last 3 points being lost? Well there&#39;s a handy Google sheet called &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1dXH-bXX3gxqqpD1f7rp6ImSOhobsT1gn_GQ2fGZp8UU/edit?ts=59fb61d2#gid=0&quot;&gt;&#39;Lighthouse Score Weighting&#39;&lt;/a&gt; that you can examine. It tells you what you should be scoring for each performance metric to achieve a certain percentage:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;First Contentful Paint&lt;/li&gt;&lt;li&gt;First Meaningful Paint&lt;/li&gt;&lt;li&gt;Speed Index&lt;/li&gt;&lt;li&gt;First CPU Idle&lt;/li&gt;&lt;li&gt;Time to Interactive&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A comparison between the two sets of data showed there was a fair bit of improvement needed to hit 100. Time to look at the network tab and run an audit on &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;Web Page Test&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;what-were-the-issues&quot;&gt;What were the issues?&lt;/h2&gt;&lt;p&gt;From the audit run on Web Page Test it was easy to spot where some improvements could be made. The results shown were run on an iPhone 5c on a 4G connection. Not the most up-to-date handset, but still one that is very popular.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Web Page Test waterfall graph before&quot; decoding=&quot;async&quot; height=&quot;414&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/6KpXxe04x8-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/6KpXxe04x8-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/6KpXxe04x8-600.webp 600w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/6KpXxe04x8-814.webp 814w&quot; title=&quot;A waterfall graph is a great way to spot inefficiencies in network performance.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;On the graph you can see some green, orange and purple bands. This is DNS lookup, connection established and SSL negotiation. Each of these is taking 800ms. Now that doesn&#39;t sound like much, but in web performance terms that is huge! Each of these lookups was for a Google service: Analytics and Google Fonts. As I want to monitor the analytics data, there&#39;s not much I can do about that. For the fonts there are certainly optimisations that can be made.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The WPT filmstrip shows where the issue is: font performance.&quot; decoding=&quot;async&quot; height=&quot;129&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/o9vf0e-3zg-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/o9vf0e-3zg-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/o9vf0e-3zg-600.webp 600w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/o9vf0e-3zg-814.webp 814w&quot; title=&quot;The filmstrip view shows exactly what the issue is: Font performance isn&#39;t great. iPhone 5C 10.3 (no preload support)&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The filmstrip view from Web Page Test really highlights the main issue. Notice how 3 seconds into loading we have the background colours but no text. At this point the font hasn&#39;t been downloaded and what we are seeing is a Flash of Invisible Text (FOIT). This will be killing the First Meaningful Paint (FMP) score which measures when a user perceives that the primary content of a page is visible. It&#39;s easy to see why this is all happening when you combine the waterfall graph with the filmstrip. The fonts are being hosted on a 3rd party website, so there will be a separate set of connection negotiation and round trip time associated with the font download. Time to eliminate these issues. The full Web Page Test report can be &lt;a href=&quot;https://www.webpagetest.org/result/181201_W7_a2e23b029f4b4b83963a43f0eeced8fe/&quot;&gt;seen here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;what-did-i-change&quot;&gt;What did I change?&lt;/h2&gt;&lt;h3 id=&quot;eliminate-the-3rd-party-hosting-google-fonts&quot;&gt;Eliminate the 3rd-party hosting (Google Fonts)&lt;/h3&gt;&lt;p&gt;Is it possible? Sure it is. Thankfully there is a really helpful tool to allow you to do this easily, &lt;a href=&quot;https://google-webfonts-helper.herokuapp.com/fonts&quot;&gt;google-webfonts-helper&lt;/a&gt;. Select the font you want to self-host, download and away you go. Place the files on the same server as your main website so you are no longer dependant on a 3rd party for your fonts.&lt;/p&gt;&lt;h3 id=&quot;remove-unneeded-fonts-swap-for-a-system-font&quot;&gt;Remove unneeded fonts, swap for a system font&lt;/h3&gt;&lt;p&gt;Did I really need the second webfont for the main content? In using two Google Fonts, this is doubling up on the number of fonts to download and also the associated CSS. More bytes to download equals slower performance. So I swapped the main content font to &lt;code&gt;Geneva&lt;/code&gt;, as it is very similar to what I had before. Now the browser can use a locally installed font rather than having to download the second font.&lt;/p&gt;&lt;h3 id=&quot;control-the-way-fonts-are-displayed-while-loading&quot;&gt;Control the way fonts are displayed while loading&lt;/h3&gt;&lt;p&gt;How can we control the way the font loads? Well there&#39;s a CSS property for that &lt;code&gt;font-display&lt;/code&gt;. At the moment browser support &lt;a href=&quot;https://caniuse.com/#search=font-display&quot;&gt;isn&#39;t perfect&lt;/a&gt;, but it is getting there. This setting allows a developer to control the &lt;a href=&quot;https://font-display.glitch.me/&quot;&gt;block, swap and failure period&lt;/a&gt; when loading. I&#39;ve chosen to use &lt;code&gt;font-display: swap;&lt;/code&gt;. What this does is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;There&#39;s no block period, so before the custom font is loaded the fallback font is displayed&lt;/li&gt;&lt;li&gt;Once the font is downloaded, the fallback will be swapped for the custom font. There is an infinite period in which this can happen.&lt;/li&gt;&lt;li&gt;Downside to this setting, you could potentially see Flash of Unstyled Text (FOUT)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So why don&#39;t Google Fonts use this setting by default? Why can it only be applied when you self-host the fonts? Well there&#39;s a &lt;a href=&quot;https://github.com/google/fonts/issues/358&quot;&gt;really long thread discussing just that&lt;/a&gt; if you are interested.&lt;/p&gt;&lt;h3 id=&quot;minimise-the-chance-of-fout&quot;&gt;Minimise the chance of FOUT&lt;/h3&gt;&lt;p&gt;Is it possible to minimise the likelihood of the FOUT? Yes, and it is super simple to implement. What is needed is the &lt;code&gt;preload&lt;/code&gt; link element in the &lt;code&gt;&amp;lt;head&gt;&lt;/code&gt; tag. This allows a developer to tell the browser about critical assets that will need to be downloaded in the future. By telling the browser ahead of time that the font will be required, it should therefore be downloaded by the time it is actually required on the page. If the font is already in the cache, no FOUT!&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;preload&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/assets/font.woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;font/woff2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is worth checking out the &lt;a href=&quot;https://beta.caniuse.com/#search=preload&quot;&gt;browser support&lt;/a&gt; for this feature, as it is still quite new. And a word of warning about its usage: use it sparingly for assets you know the page absolutely needs. The preloaded asset will be bumped to the highest priority in the queue. It will be prioritised even higher than CSS. So if you preload a lot of files, you&#39;ve just added a large number of render blocking bytes to the &lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/critical-rendering-path/&quot;&gt;critical path&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;I&#39;ve run a &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=181205_X7_82bdf910748d7390556e3c9ca21f62cf%2C181205_4M_c596e12ea22aa279d6ee1fd26518094e&amp;thumbSize=200&amp;ival=100&amp;end=visual&quot;&gt;couple of tests on WPT&lt;/a&gt; and compared them if you&#39;d like to see the difference &lt;code&gt;preload&lt;/code&gt; can make on supporting browsers. iPhone 5 SE on a 4G connection visually completes 500ms quicker with &lt;code&gt;preload&lt;/code&gt;!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Web Page Test waterfall with preloaded font visible.&quot; decoding=&quot;async&quot; height=&quot;336&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/mIG6-TE-H7-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/mIG6-TE-H7-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/mIG6-TE-H7-600.webp 600w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/mIG6-TE-H7-814.webp 814w&quot; title=&quot;Preload in action on the iPhone SE iOS 12. Notice the red bar directly after the initial HTML download.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The full specification for preload can be &lt;a href=&quot;https://www.w3.org/TR/preload/&quot;&gt;found here&lt;/a&gt;, along with the section related to &lt;a href=&quot;https://www.w3.org/TR/preload/#early-fetch-of-critical-resources&quot;&gt;fetching critical assets&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;final-results&quot;&gt;Final results&lt;/h2&gt;&lt;p&gt;Well I&#39;m happy to report that I managed to join the &quot;100 club&quot;! A completely made up club for people who&#39;s website scores 100/100 for performance in Lighthouse. Silliness aside, the results were really impressive considering how little work it required. The full audit for the after performance can be &lt;a href=&quot;https://googlechrome.github.io/lighthouse/viewer/?gist=e4a712467b36cd2964d627e82a078be4&quot;&gt;viewed here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Lighthouse score for this website after optimisation.&quot; decoding=&quot;async&quot; height=&quot;621&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/v5UK4GvwRl-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/v5UK4GvwRl-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/v5UK4GvwRl-600.webp 600w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/v5UK4GvwRl-814.webp 814w&quot; title=&quot;Performance score 100/100. Huzzah!&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The key metrics were all reduced by the following amounts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;First Contentful Paint: 630ms&lt;/li&gt;&lt;li&gt;First Meaningful Paint: 1,780ms&lt;/li&gt;&lt;li&gt;Speed Index: 810ms&lt;/li&gt;&lt;li&gt;First CPU Idle: 460ms&lt;/li&gt;&lt;li&gt;Time to Interactive: 460ms&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That&#39;s an incredible increase in performance, especially around the First Meaningful Paint (when the primary content is visible to the user). Almost 1.8 seconds quicker!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Web Page Test waterfall graph after&quot; decoding=&quot;async&quot; height=&quot;391&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/I5Dujo563B-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/I5Dujo563B-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/I5Dujo563B-600.webp 600w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/I5Dujo563B-814.webp 814w&quot; title=&quot;Notice how the number of DNS/Connect/SSL bars has been reduced. No preload support on iPhone 5c 10.3 though :(&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The changes can really be seen in the waterfall graph. Notice how there are only 2 DNS/Connect/SSL negotiations in this graph (there were 4 before the changes were made). The font is downloaded immediately since the initial connection negotiation to the main domain has already been established.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;New improved filmstrip shows increased performance&quot; decoding=&quot;async&quot; height=&quot;159&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/WFB7KJm596-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/WFB7KJm596-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/WFB7KJm596-600.webp 600w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/WFB7KJm596-814.webp 814w&quot; title=&quot;Notice how there are fewer panels than before and only a single panel with the font not visible. iPhone 5C 10.3 (no preload support)&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Overall page load time and the number of panels without main content have been reduced. The perceived performance of the site has been greatly improved for users. The full Web Page Test report can be &lt;a href=&quot;https://www.webpagetest.org/result/181201_48_b631ffc49a5c7f2b85e66a4806cd00f3/&quot;&gt;seen here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Animation showing the difference in page load performance.&quot; decoding=&quot;async&quot; height=&quot;435&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/lOgXZwB5nH-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/12/06/joining-the-100-club/lOgXZwB5nH-300.webp 300w, https://nooshu.com/blog/2018/12/06/joining-the-100-club/lOgXZwB5nH-600.webp 600w&quot; title=&quot;A visualisation of what the changes have made to page load. iPhone 5C 10.3 (no preload support)&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;Web Page Test actually comes with a really handy feature. You can compare multiple results on the same page, allowing you to see exactly what has changed. Here&#39;s a comparison of the &lt;a href=&quot;https://www.webpagetest.org/video/compare.php?tests=181201_48_b631ffc49a5c7f2b85e66a4806cd00f3%2C181201_W7_a2e23b029f4b4b83963a43f0eeced8fe&amp;thumbSize=200&amp;ival=500&amp;end=doc&quot;&gt;before and after audits&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;By using freely available performance tools like Lighthouse and Web Page Test, it is possible to easily identify performance bottleneck&#39;s in your website&#39;s frontend code. Solving those performance issues once identified can sometimes a more difficult task. Thankfully the optimisation of webfont performance has been a high priority for browser vendors over the past few years. We as developers have been given tools that can make a real difference in web performance. This should making the web faster for all users as they become more widely adopted.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Making GOV.UK pages load faster and use less data</title>
    <link href="https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/" />
    <updated>2018-10-04T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This post was originally posted on the &lt;a href=&quot;https://gdstechnology.blog.gov.uk/&quot;&gt;GDS Technology blog&lt;/a&gt;. It can be &lt;a href=&quot;https://gdstechnology.blog.gov.uk/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/&quot;&gt;viewed here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;img alt=&quot;GDS testing the New Transport font weights.&quot; decoding=&quot;async&quot; height=&quot;445&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/PoIGuqlMxy-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/PoIGuqlMxy-300.webp 300w, https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/PoIGuqlMxy-600.webp 600w, https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/PoIGuqlMxy-770.webp 770w&quot; width=&quot;770&quot;&gt;&lt;/p&gt;&lt;p&gt;We&#39;ve used a custom version of the &lt;a href=&quot;https://gds.blog.gov.uk/2012/07/05/a-few-notes-on-typography/&quot;&gt;New Transport font&lt;/a&gt; since the launch of &lt;a href=&quot;https://www.gov.uk/government/news/launch-of-gov-uk-a-key-milestone-in-making-public-service-delivery-digital-by-default&quot;&gt;GOV.UK&lt;/a&gt;. New Transport was rendered on-screen by &lt;a href=&quot;https://en.wikipedia.org/wiki/Base64&quot;&gt;Base64 encoding&lt;/a&gt; font files into a CSS file. Base64 encoding was the preferred technique when we launched in 2012 as it helped to reduce the number of HTTP requests and minimise the &lt;a href=&quot;https://en.wikipedia.org/wiki/Flash_of_unstyled_content&quot;&gt;flash of unstyled content&lt;/a&gt; (FOUC).&lt;/p&gt;&lt;h2 id=&quot;why-we-changed-font-loading-on-gov-uk&quot;&gt;Why we changed font loading on GOV.UK&lt;/h2&gt;&lt;p&gt;Browsers have improved a lot since 2012 and Base64 encoding font files is now &lt;a href=&quot;https://csswizardry.com/2017/02/base64-encoding-and-performance/&quot;&gt;considered a method you should avoid&lt;/a&gt;. The main problems we had with the Base64 encoding method were that we:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;forced browsers to download fonts even if they were not used&lt;/li&gt;&lt;li&gt;could only use the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Guide/WOFF&quot;&gt;Web Open Font Format&lt;/a&gt; (WOFF) because of the file size restriction&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Base64 encoding also added extra bytes to the page that were sitting on the browsers&#39; critical rendering path. All these bytes needed to be loaded before the page could be rendered. The CSS file was approximately 200 kilobytes (KB), which was 82% of the total blocking assets needed to load the page.&lt;/p&gt;&lt;h2 id=&quot;how-we-changed-font-loading-on-gov-uk&quot;&gt;How we changed font loading on GOV.UK&lt;/h2&gt;&lt;p&gt;We made a simple code change but the impact on the wider GOV.UK codebase was significant. We changed the way the &lt;code&gt;@font-face&lt;/code&gt; loading method referenced fonts. By removing the inline Base64 encoded font and loading the files via standard HTTP requests we can now:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;serve multiple font types from the same &lt;code&gt;@font-face&lt;/code&gt; declaration, which allows a browser to choose the appropriate font it supports&lt;/li&gt;&lt;li&gt;remove approximately 200KB from the critical path to load pages faster&lt;/li&gt;&lt;li&gt;start to take advantage of modern browser optimisation techniques like the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display&quot;&gt;CSS font-display property&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content&quot;&gt;resource preloading&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;New Transport font sizes in the Design System.&quot; decoding=&quot;async&quot; height=&quot;297&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/34H6_Zxsfm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/34H6_Zxsfm-300.webp 300w, https://nooshu.com/blog/2018/10/04/making-gov-uk-pages-load-faster-and-use-less-data/34H6_Zxsfm-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;the-main-advantages-for-end-users&quot;&gt;The main advantages for end users&lt;/h2&gt;&lt;h3 id=&quot;less-data-usage&quot;&gt;Less data usage&lt;/h3&gt;&lt;p&gt;Removing the Base64 encoded font has reduced the total page weight by 16% (75 KB) per request (assuming no caching). This may not sound like a huge difference, but GOV.UK receives approximately 48 million visitors per month, so this adds up to mobile users saving approximately 800 GB per month, cumulatively. This is especially important to users on older mobile devices and expensive data plans.&lt;/p&gt;&lt;p&gt;By removing the Base64 encoding we are also giving mobile users the ability to opt in or out of downloading the font completely. This can help users reduce their data consumption.&lt;/p&gt;&lt;h3 id=&quot;faster-page-loading&quot;&gt;Faster page loading&lt;/h3&gt;&lt;p&gt;We have seen big improvements in page load time since we introduced the change. In our testing, the page load time for high-end devices using 4G dropped by 13% and visual population of the page (&lt;a href=&quot;https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index&quot;&gt;Speed Index&lt;/a&gt;) reduced by 50%.&lt;/p&gt;&lt;p&gt;But where the change is most noticeable is on slow mobile devices on a 2G connection. Under these conditions the page loads approximately 10 seconds faster according to our simulated &lt;a href=&quot;https://speedcurve.com/&quot;&gt;SpeedCurve&lt;/a&gt; tests. This large performance increase comes in part from the 16% reduction in overall page size, but also from the fact we have removed around 200 KB of CSS that no longer needs to be processed by the browser before the page is shown.&lt;/p&gt;&lt;h3 id=&quot;lower-battery-power-usage&quot;&gt;Lower battery power usage&lt;/h3&gt;&lt;p&gt;With over 44% of GOV.UK users accessing the website through a mobile device, pages need to render quickly and efficiently. Mobile devices are often CPU, memory and battery limited.&lt;/p&gt;&lt;p&gt;A page with a large amount of blocking data can have a huge impact on users with pre-2012 devices. A GOV.UK page will take longer to download due to the mobile connection speed. The CPU and RAM requirement will increase when processing the data and display content to the screen. These both have an impact on a users mobile battery life. The same principle applies to users on older desktop and laptop devices.&lt;/p&gt;&lt;p&gt;By optimising the GOV.UK frontend code, we are improving the experience for all users no matter what device they are using to access it.&lt;/p&gt;&lt;h2 id=&quot;continuing-to-improve-gov-uk-frontend-performance&quot;&gt;Continuing to improve GOV.UK frontend performance&lt;/h2&gt;&lt;p&gt;This is just the first stage in improving the GOV.UK frontend performance. The &lt;a href=&quot;https://www.a2-type.co.uk/new-transport&quot;&gt;font foundry&lt;/a&gt; is working on a new font that promises to reduce the font weight by another 47% for WOFF2 (50% for WOFF).&lt;/p&gt;&lt;p&gt;We&#39;ll also experiment with new browsers features like the &lt;a href=&quot;https://www.zachleat.com/web/font-checklist/&quot;&gt;&lt;code&gt;font-display&lt;/code&gt; property and asset preloading&lt;/a&gt;. By combining all these changes we&#39;ll aim to reduce the load time and also the overall page weight for GOV.UK users. This will provide a better user experience for all users no matter how they access the site.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Changing our testing requirements for Internet Explorer 8, 9 and 10</title>
    <link href="https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/" />
    <updated>2018-06-26T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This post was originally posted on the &lt;a href=&quot;https://gdstechnology.blog.gov.uk/&quot;&gt;GDS Technology blog&lt;/a&gt;. It can be &lt;a href=&quot;https://gdstechnology.blog.gov.uk/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/&quot;&gt;viewed here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;img alt=&quot;Time to upgrade your old IE browser advert from Microsoft.&quot; decoding=&quot;async&quot; height=&quot;306&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/CQOcohfXD6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/CQOcohfXD6-300.webp 300w, https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/CQOcohfXD6-586.webp 586w&quot; width=&quot;586&quot;&gt;&lt;/p&gt;&lt;p&gt;Service team developers no longer need to test on Internet Explorer 8, 9 and 10 (old versions of IE) when following our &#39;&lt;a href=&quot;https://www.gov.uk/service-manual/technology/designing-for-different-browsers-and-devices&quot;&gt;designing for different browsers and devices&lt;/a&gt;&#39; guidance.&lt;/p&gt;&lt;p&gt;As users upgrade the devices they use to access the web, older browsers become less common. Your service must be universally accessible, but there are 3 big downsides to supporting older browsers once they have fallen out of use for most people. Below we explain why we&#39;ve made this decision.&lt;/p&gt;&lt;h2 id=&quot;why-we-no-longer-require-teams-to-test-older-versions-of-internet-explorer&quot;&gt;Why we no longer require teams to test older versions of Internet Explorer&lt;/h2&gt;&lt;h3 id=&quot;security-issues&quot;&gt;Security issues&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://www.microsoft.com/en-gb/windowsforbusiness/end-of-ie-support&quot;&gt;Microsoft stopped regularly supporting older versions of Internet Explorer on 12 January 2016&lt;/a&gt;. Since then, old IE browsers have received a few critical security updates but they don&#39;t support all the latest security standards. This means these browsers are not secure for users and departments who continue to use them.&lt;/p&gt;&lt;p&gt;The National Cyber Security Centre (NCSC) currently recommends using the &lt;a href=&quot;https://www.ncsc.gov.uk/guidance/tls-external-facing-services#UsingTLStoprotectdata-Combination1oftheSuiteBprofileforTLS&quot;&gt;Transport Layer Security 1.2 protocol&lt;/a&gt; to provide privacy between communicating applications and their users, and also between services. However, TLS 1.2 is either not supported or not enabled by default in old IE browsers (depending on operating system in use). This means that certain services, such as &lt;a href=&quot;https://www.payments.service.gov.uk/&quot;&gt;GOV.UK Pay&lt;/a&gt; which require a minimum of TLS 1.2 due to PCI compliance (PCI DSS), won&#39;t work. Other services, such as &lt;a href=&quot;https://www.notifications.service.gov.uk/&quot;&gt;GOV.UK Notify&lt;/a&gt; and &lt;a href=&quot;https://www.cloud.service.gov.uk/&quot;&gt;GOV.UK PaaS&lt;/a&gt;, also plan to require TLS 1.2 in the future.&lt;/p&gt;&lt;p&gt;Older versions of Internet Explorer don&#39;t support the latest HTML5 features either. This means they won&#39;t benefit from security features like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Punycode&quot;&gt;Punycode identification&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&quot;&gt;Cross-Origin Resource Sharing (CORS)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP&quot;&gt;Content Security Policy (CSP)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security&quot;&gt;HTTP Strict Transport Security (HSTS)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These features are all available in &lt;a href=&quot;https://www.techopedia.com/definition/31094/evergreen-browser&quot;&gt;‘evergreen&#39; browsers&lt;/a&gt; like Microsoft Edge, Google Chrome and Mozilla Firefox. Browsers like these update automatically without user interaction.&lt;/p&gt;&lt;h3 id=&quot;poor-user-experience&quot;&gt;Poor user experience&lt;/h3&gt;&lt;p&gt;Browser technology has evolved and no matter how much user testing is done, people with old IE browsers will have a poor user experience when accessing government websites. Although the pages will work thanks to the &lt;a href=&quot;https://www.gov.uk/service-manual/technology/using-progressive-enhancement&quot;&gt;progressive enhancement techniques we use&lt;/a&gt;, old browsers will not provide as good an experience as with modern browsers.&lt;/p&gt;&lt;p&gt;Webpages take longer to load and may even look broken in many cases, due to a lack of support for the latest browser standards. If the very small percentage of users (or the IT teams in their departments) relying on old browsers make an upgrade, their overall experience of government services will improve.&lt;/p&gt;&lt;h3 id=&quot;code-complexity&quot;&gt;Code complexity&lt;/h3&gt;&lt;p&gt;Supporting older browsers creates code complexity for developers. The codebase is harder to maintain which can lead to technical debt, making things harder for future developers who inherit the project. The additional testing required to enable a service to work in these older browsers also adds to development timelines. This extra development effort costs time and money which isn&#39;t cost-effective when old IE has known security and performance issues.&lt;/p&gt;&lt;h2 id=&quot;what-this-change-means-for-gov-uk-users&quot;&gt;What this change means for GOV.UK users&lt;/h2&gt;&lt;p&gt;The number of users visiting GOV.UK using old IE browsers has fallen dramatically in the past 12 months, from 1.11% to 0.16% of total users in May 2018.&lt;/p&gt;&lt;p&gt;This means the number of users affected by the change to the guidance is small, compared to the overall traffic we receive. And, according to our &lt;a href=&quot;https://www.gov.uk/service-manual/technology/designing-for-different-browsers-and-devices#understanding-the-table&quot;&gt;Service Manual guidelines&lt;/a&gt; and Google Analytics data, we should focus on supporting the browsers and devices that are in use.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Graph showing the decline in old Internet Explorer usage on GOV.UK.&quot; decoding=&quot;async&quot; height=&quot;512&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/ISVMF0eiqf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/ISVMF0eiqf-300.webp 300w, https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/ISVMF0eiqf-600.webp 600w, https://nooshu.com/blog/2018/06/26/changing-our-testing-requirements-for-internet-explorer-8-9-and-10/ISVMF0eiqf-768.webp 768w&quot; width=&quot;768&quot;&gt;&lt;/p&gt;&lt;p&gt;The change to our guidance doesn&#39;t mean that GOV.UK pages will stop working on older browsers. We still recommend that your service teams use the &lt;a href=&quot;https://www.gov.uk/service-manual/technology/using-progressive-enhancement&quot;&gt;progressive enhancement development&lt;/a&gt; approach. By doing this we can help to make sure that the small number of users who use old IE browsers will still be able to access government content but some services may have limited functionality.&lt;/p&gt;&lt;p&gt;When users use old browsers to access GOV.UK they will also see a banner with a link to &lt;a href=&quot;https://www.gov.uk/help/browsers&quot;&gt;a page to help them upgrade&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you provide internal-facing services, you should look at your analytics data and check the percentage of users that use these older browsers. You can then make an informed decision about whether to support or drop old IE from your browser matrix.&lt;/p&gt;&lt;p&gt;Continuing to support these older browsers beyond their life cycle ultimately costs the taxpayer more time and money. This money would be better spent on creating and improving services for the other 99.8% of users who access GOV.UK and services that sit beneath it. It&#39;s time to say goodbye to these legacy browsers and move towards a more standards-compliant future.&lt;/p&gt;&lt;p&gt;To make sure you stay up to date with all the latest developments, you can sign up to alerts from the &lt;a href=&quot;https://gdstechnology.blog.gov.uk/subscribe/&quot;&gt;GDS Technology blog&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Cloudless Chrome - Broken CSS filters</title>
    <link href="https://nooshu.com/blog/2018/05/18/Cloudless-chrome/" />
    <updated>2018-05-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2018/05/18/Cloudless-chrome/</id>
    <content type="html">&lt;p&gt;A month or so ago I happened to open this very website in the latest version of Chrome (65 at the time) and I noticed something was a little bit off. The animating clouds in the header of the website were very broken. Considering the code for this part of the site hasn&#39;t changed since I first built it, this pointed to the fact that this change hadn&#39;t come from me.&lt;/p&gt;&lt;p&gt;As you can see in the images below, there was quite a rendering difference between Firefox and Chrome.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of the clouds in Firefox browser&quot; decoding=&quot;async&quot; height=&quot;248&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/a0TbA9LVeL-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/a0TbA9LVeL-300.webp 300w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/a0TbA9LVeL-600.webp 600w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/a0TbA9LVeL-814.webp 814w&quot; title=&quot;Animated SVG clouds with a slight blur in Firefox.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of the broken clouds in Chrome browser&quot; decoding=&quot;async&quot; height=&quot;246&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/6FzXbY3e7g-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/6FzXbY3e7g-300.webp 300w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/6FzXbY3e7g-600.webp 600w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/6FzXbY3e7g-814.webp 814w&quot; title=&quot;There are supposed to be some clouds here, but Chrome is having issues.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;what-happened&quot;&gt;What happened?&lt;/h2&gt;&lt;p&gt;Well as you may (or may not) know all modern web browsers are what is known as &quot;evergreen browsers&quot;. This is defined as:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;...browsers that are automatically upgraded to future versions, rather than being updated by distribution of new versions from the manufacturer, as was the case with older browsers.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So the Chrome browser had automatically updated and in doing so a regression had been introduced. Unfortunately this is one negative point around auto-updating browsers that require no user interaction. The update will happen, something will break and the user will have no idea what changed. Luckily this doesn&#39;t happen very often as browser vendors have a very extensive testing process before a new version hits the stable channel. It takes approximately 10 weeks from a commit entering the Canary channel to land on the stable channel. You can see what the Chrome team have in the pipeline on the &lt;a href=&quot;https://www.chromestatus.com/features&quot;&gt;ChromeStatus website&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;css-filter-broke-the-clouds&quot;&gt;CSS filter broke the clouds&lt;/h2&gt;&lt;p&gt;So what actually broke? It turned out a single line of CSS was causing the issue:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.cloud-class-example&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  //...
  &lt;span class=&quot;token property&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;blur&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;2.2px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  //...
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Applying the &lt;code&gt;blur()&lt;/code&gt; CSS filter to the cloud SVG was breaking the rendering. After a little investigation it turns out that applying any CSS filter seemed to break the rendering, it wasn&#39;t just tied to &lt;code&gt;blur()&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Looking through the &lt;a href=&quot;https://bugs.chromium.org/&quot;&gt;Chromium bugs&lt;/a&gt; for anything related to &quot;CSS filter&quot;, two bugs are listed that looked to be very similar to the issues I&#39;ve been seeing:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=830242&quot;&gt;830242 - Chrome65 CSS filter rendering exceptions&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=804164&quot;&gt;804164 - Some combinations of CSS filters cause complete rendering failures&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Reading through these bug reports it turns out that this bug is very specific to OSX version 10.11 with Intel video cards. So only a certain number of users will be seeing this issue. Checking my computer specifications, sure enough 10.11 with an Intel video card!&lt;/p&gt;&lt;h2 id=&quot;examples&quot;&gt;Examples&lt;/h2&gt;&lt;p&gt;For those wondering if you&#39;re affected by this bug I&#39;ve created a very basic test page that can be viewed on &lt;a href=&quot;https://www.sassmeister.com/gist/1a6ff0dfd6ec47bbceb07c5841060f29&quot;&gt;Sassmeister&lt;/a&gt; (&lt;a href=&quot;https://gist.github.com/Nooshu/1a6ff0dfd6ec47bbceb07c5841060f29&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot; title=&quot;Opens in a new tab&quot;&gt;Gist&lt;span class=&quot;visually-hidden&quot;&gt; (opens in new tab)&lt;/span&gt;&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;Here&#39;s a screenshot from Firefox of what the rendering should look like:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Firefox rendering the example&quot; decoding=&quot;async&quot; height=&quot;218&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/L56qhqXO16-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/L56qhqXO16-300.webp 300w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/L56qhqXO16-600.webp 600w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/L56qhqXO16-814.webp 814w&quot; title=&quot;Firefox browser renders the example as expected.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Compare it to how it renders in Chrome 66. Very broken!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome rendering the example&quot; decoding=&quot;async&quot; height=&quot;236&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/tKONl-gi_h-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/05/18/Cloudless-chrome/tKONl-gi_h-300.webp 300w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/tKONl-gi_h-600.webp 600w, https://nooshu.com/blog/2018/05/18/Cloudless-chrome/tKONl-gi_h-814.webp 814w&quot; title=&quot;Chrome 66 looks really broken, with some clouds not even being rendered.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;In the example code you will see I tried a number of permutations to see if it was a combination of the animation and the filter, or if it was only a SVG rendering issue related to the filter. It turns out that it has nothing to do with the animation or SVG&#39;s. The clouds are still broken on non-animated assets and non-SVG content (&lt;code&gt;&amp;lt;div&gt;&lt;/code&gt;).&lt;/p&gt;&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;&lt;p&gt;So what&#39;s the solution for this issue you may ask? Well aside from upgrading your OS from OSX 10.11 or removing the CSS filtering from the element, there really isn&#39;t a solution to this issue at the moment. Oh no! you may shout. But do not despair, the Google Chrome team have it covered. You will be pleased to hear that in the latest Chrome Canary (68 at the time of writing) all the examples render fine!&lt;/p&gt;&lt;p&gt;The list of browsers with this specific bug for OSX 10.11 with Intel GPU&#39;s are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Chrome 64 - not broken&lt;/li&gt;&lt;li&gt;Chrome 65 - &lt;strong&gt;broken&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Chrome 66 - &lt;strong&gt;broken&lt;/strong&gt; (current stable channel)&lt;/li&gt;&lt;li&gt;Chrome 67 - &lt;strong&gt;broken&lt;/strong&gt; (current beta channel)&lt;/li&gt;&lt;li&gt;Chrome 68 - not broken (current Canary channel)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Chrome 68 has a planned release date of July 2018. This is when it should hit the stable channel, at which point I expect this issue to rectify itself very quickly for the small number of users it affects.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Todays &quot;evergreen browsers&quot; will auto-update without the need for any user interaction and in doing so this can sometimes introduce regressions. The average user won&#39;t understand the origin of this regression since it will look like nothing has changed.&lt;/p&gt;&lt;p&gt;Is this a bad? Sure it could be! But if you compare it to the web when we didn&#39;t have &quot;evergreen browsers&quot; and we relied on users or admins to update the browser version (IE8 anyone?), we are in a much better place now. New features and bug fixes are rolled out quickly, meaning a safer and more exciting web for users and developers alike!&lt;/p&gt;&lt;p&gt;You will be pleased to hear that all major browser vendors now have auto-updating browsers (even Microsoft Edge!).&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Workbox - Generate a service worker with ease</title>
    <link href="https://nooshu.com/blog/2018/03/02/workbox-generate-with-ease/" />
    <updated>2018-03-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2018/03/02/workbox-generate-with-ease/</id>
    <content type="html">&lt;p&gt;Back in July 2016 I wrote a blog post about how to &lt;a href=&quot;https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/&quot;&gt;implement a service worker&lt;/a&gt;. At the time the technology was still fairly new, with only Firefox, Chrome, Opera and Samsung Internet having &lt;a href=&quot;https://beta.caniuse.com/#search=service%20worker&quot;&gt;enabled the API&lt;/a&gt; in their browsers. It&#39;s amazing what a difference 18 months can make in the Frontend world. WebKit &lt;a href=&quot;https://webkit.org/blog/8084/release-notes-for-safari-technology-preview-48/&quot;&gt;announced in January&lt;/a&gt; that service workers would be supported in Safari Technology Preview 48. Closely followed by &lt;a href=&quot;https://blogs.windows.com/msedgedev/2018/02/06/welcoming-progressive-web-apps-edge-windows-10/#bZSi1lwOHSWiSppM.97&quot;&gt;Microsoft announcing Edge 17 will also support them&lt;/a&gt;. The future looks bright for service workers so we&#39;d better get used to creating them! So the question you may be asking is how do I create one?&lt;/p&gt;&lt;h2 id=&quot;the-old-way-copy-paste-hope&quot;&gt;The old way (copy / paste / hope)&lt;/h2&gt;&lt;p&gt;The old way seemed very much a case of read the documentation, copy a script, modify for your own needs then hope you didn&#39;t break anything when you deploy it to your users. There&#39;s nothing wrong with this method and it is a great way to learn a new technology (I do it all the time!). But this isn&#39;t a good strategy to use when it comes to service workers.&lt;/p&gt;&lt;p&gt;With a service worker, if you aren&#39;t careful you could inadvertently break your website completely for your users. If this were to happen they would have no idea why or even how to fix it. Imagine a service worker being installed on a users machine that is broken in some way. Say for example all requests are swallowed by the service worker once it is activated. When the user revisits your site they won&#39;t see a homepage. In fact they would most likely only get a standard browser error page, but only for your website. This is bad! But what is worse is that since the user can no longer get to your website, they can also no longer receive service worker updates to fix the issue! Now you may be thinking &#39;they can just clear the cache&#39;. Afraid not, even if they clear the browser cache the service worker will still persist. You&#39;ve inadvertently created a &lt;a href=&quot;https://www.youtube.com/embed/CPP9ew4Co0M?rel=0&quot;&gt;zombie service worker&lt;/a&gt; on the users machine that they can only kill if they know how to use the DevTools or update the device.&lt;/p&gt;&lt;p&gt;Alexander Pope spoke about this topic in his 25 minute presentation at JSConf EU 2017:&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allow=&quot;autoplay; encrypted-media&quot; allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/CPP9ew4Co0M?rel=0&quot; title=&quot;YouTube video&quot; width=&quot;560&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;Scary stuff huh! So what&#39;s the solution? Well you could be very careful and test your code multiple times on multiple machines before releasing to production... but you are already doing that anyway aren&#39;t you? So that may not help you in this instance. It simply isn&#39;t possible to test all possible permutations across all devices. Another more viable solution to consider is simply abstract the problem away. This is where &lt;a href=&quot;https://developers.google.com/web/tools/workbox/&quot;&gt;Workbox&lt;/a&gt; comes to the rescue.&lt;/p&gt;&lt;h2 id=&quot;the-new-way-workbox&quot;&gt;The new way (Workbox)&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://developers.google.com/web/tools/workbox/&quot;&gt;Workbox&lt;/a&gt; is a collection of libraries that you can use to generate your service workers in a safe and well tested manner. If you are looking to add offline functionality or improve loading performance for repeat users, there really isn&#39;t a simpler way to do it. It will even make deployment much easier because you can even integrate Workbox into your development workflow using npm (workbox-cli), Gulp or Webpack.&lt;/p&gt;&lt;p&gt;I recently removed my own implementation of a service worker for this blog and replaced it with one generated by Workbox. Now every time I run gulp to generate the static website (I use &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt;), Workbox examines the resulting HTML and assets looking for byte-level differences from the previous versions. If differences are found it will generate a new service worker file (sw.js) with the relevant files revision hash updated. The update cycle will then be triggered on the clients machine and the service worker will cache the modified assets. You are now no longer required to manually manage your cached assets and service worker script!&lt;/p&gt;&lt;h2 id=&quot;example-setup&quot;&gt;Example setup&lt;/h2&gt;&lt;p&gt;To setup Workbox with Gulp it is fairly straightforward. The examples below are what I have used for this site.&lt;/p&gt;&lt;h3 id=&quot;config&quot;&gt;Config&lt;/h3&gt;&lt;p&gt;This is the config used by Workbox to define what is cached and where the final service worker file (sw.js) is generated. Notice how there is a Stale While Revalidate request strategy used with the Google Font. This means the resource is requested from both the cache and the network in parallel, then the service worker immediately responds with the cached version. If there is an updated version of the resource it is swapped out in the background, with no impact on a users browsing experience. This strategy is great for non-critical page assets like favicons and avatars. It also displays another powerful feature of Workbox. You can easily define different request strategies for different assets in the config and it will handle the implementation for you.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// workbox-cli-config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property literal-property&quot;&gt;globDirectory&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;_site&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property literal-property&quot;&gt;globPatterns&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&#39;images/app-shell/*&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property literal-property&quot;&gt;swDest&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;_site/sw.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property literal-property&quot;&gt;clientsClaim&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property literal-property&quot;&gt;skipWaiting&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property literal-property&quot;&gt;runtimeCaching&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property literal-property&quot;&gt;urlPattern&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RegExp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;https://fonts.googleapis.com/css?family=Dosis|Open+Sans&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property literal-property&quot;&gt;handler&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;staleWhileRevalidate&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;gulp&quot;&gt;Gulp&lt;/h3&gt;&lt;p&gt;For reference I have my gulp tasks split into separate files for easy maintenance. There really isn&#39;t much going on in this file. We require Workbox, point it to the config defined above and watch for success / errors.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// generate-service-worker.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; gulp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;gulp&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; workbox &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;workbox-build&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateServiceWorker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; workbox&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;generateSW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;../workbox-cli-config&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Service worker generation completed.&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Service worker generation failed: &#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

gulp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;generate-service-worker&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; generateServiceWorker&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&#39;ve had to create an additional gulp task to copy the service worker files from the &lt;code&gt;_site&lt;/code&gt; directory (where the static site is generated by gulp), back into the project root. This task is required because GitHub pages doesn&#39;t generate a &lt;code&gt;_site&lt;/code&gt; directory when deploying the site. Once the file is moved and committed into the project root it will then be deployed by GitHub pages like any other file.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// copy-sw-code-to-root.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; gulp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;gulp&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;copySwCodeToRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; gulp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;_site/sw.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;_site/workbox*&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gulp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;./&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

gulp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;copy-sw-code-to-root&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; copySwCodeToRoot&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;output&quot;&gt;Output&lt;/h3&gt;&lt;p&gt;What comes out the end is clean and simple auto-generated sw.js file that you can &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Enter_service_workers&quot;&gt;register in the usual manner&lt;/a&gt;. Note the importScripts() at the top of the generated file. This is loading a production version of Workbox.js. This is the file that does all the heavy lifting for us so we don&#39;t need to get involved in all the details of the service worker setup. These implementation details have been abstracted away into well tested external script that we don&#39;t need to worry about. Very useful!&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// sw.js&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;importScripts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;workbox-sw.prod.v2.1.2.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * DO NOT EDIT THE FILE MANIFEST ENTRY
 *
 * The method precache() does the following:
 * 1. Cache URLs in the manifest to a local cache.
 * 2. When a network request is made for any of these URLs the response
 *    will ALWAYS comes from the cache, NEVER the network.
 * 3. When the service worker changes ONLY assets with a revision change are
 *    updated, old cache entries are left as is.
 *
 * By changing the file manifest manually, your users may end up not receiving
 * new versions of files because the revision hasn&#39;t changed.
 *
 * Please use workbox-build or some other tool / approach to generate the file
 * manifest which accounts for changes to local files and update the revision
 * accordingly.
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fileManifest &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;images/app-shell/bubble.svg&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;revision&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;cd8f3e69287067840832444472f3aece&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; workboxSW &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;WorkboxSW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property string-property&quot;&gt;&quot;skipWaiting&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property string-property&quot;&gt;&quot;clientsClaim&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
workboxSW&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;precache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fileManifest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
workboxSW&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;router&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;registerRoute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token language-regex regex-source&quot;&gt;https:&#92;/&#92;/fonts.googleapis.com&#92;/css?family=Dosis|Open+Sans&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; workboxSW&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;strategies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;GET&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;I&#39;ve only scratched the surface of what Workbox can do. Take a look at the &lt;a href=&quot;https://developers.google.com/web/tools/workbox/reference-docs/latest/&quot;&gt;documentation&lt;/a&gt; and &lt;a href=&quot;https://codelabs.developers.google.com/codelabs/workbox-lab/index.html?index=..%2F..%2Findex#0&quot;&gt;tutorials&lt;/a&gt; available for more information about the full feature set of Workbox. Version 3.0.0 beta 0 was &lt;a href=&quot;https://github.com/GoogleChrome/workbox/releases/tag/v3.0.0-beta.0&quot;&gt;released very recently&lt;/a&gt;, with a whole new set of features. It is certainly a project to keep watch or &lt;a href=&quot;https://github.com/GoogleChrome/workbox&quot;&gt;star on GitHub&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Progressive Web Apps and service workers are here to stay. You only need to take a look in your Chrome DevTools under &quot;Applications &gt; Service Workers&quot; to see how many websites you browse have already installed a service worker on your machine. If treated with care and implemented correctly they can significantly boost your site performance and offer your users some really cool features. But if the implementation goes wrong... well you may never see those users again! Using Workbox to generate the service worker for your Progressive Web App should be high on your priority list.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>We&#39;ve added Samsung Internet to our browser testing recommendations</title>
    <link href="https://nooshu.com/blog/2018/02/26/weve-added-samsung-internet-to-browser-testing-recommendations/" />
    <updated>2018-02-26T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2018/02/26/weve-added-samsung-internet-to-browser-testing-recommendations/</id>
    <content type="html">&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This post was originally posted on the &lt;a href=&quot;https://gdstechnology.blog.gov.uk/&quot;&gt;GDS Technology blog&lt;/a&gt;. It can be &lt;a href=&quot;https://gdstechnology.blog.gov.uk/2018/02/26/weve-added-samsung-internet-to-our-browser-testing-recommendations/&quot;&gt;viewed here&lt;/a&gt;.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;img alt=&quot;GOV.UK displayed on Samsung Internet.&quot; decoding=&quot;async&quot; height=&quot;610&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2018/02/26/weve-added-samsung-internet-to-browser-testing-recommendations/FHMDHJULob-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2018/02/26/weve-added-samsung-internet-to-browser-testing-recommendations/FHMDHJULob-300.webp 300w, https://nooshu.com/blog/2018/02/26/weve-added-samsung-internet-to-browser-testing-recommendations/FHMDHJULob-600.webp 600w, https://nooshu.com/blog/2018/02/26/weve-added-samsung-internet-to-browser-testing-recommendations/FHMDHJULob-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;We update our browser &lt;a href=&quot;https://www.gov.uk/service-manual/technology/designing-for-different-browsers-and-devices#browsers-to-test-in&quot;&gt;testing recommendations in the Service Manual&lt;/a&gt; every 6 months. Samsung Internet is the latest addition to our list to test your service in.&lt;/p&gt;&lt;h2 id=&quot;why-we-added-samsung-internet&quot;&gt;Why we added Samsung Internet&lt;/h2&gt;&lt;p&gt;Samsung Internet is the default browser on Samsung devices and has &lt;a href=&quot;http://gs.statcounter.com/browser-market-share/all/united-kingdom&quot;&gt;2.71% of UK browser market share&lt;/a&gt; and over 400 million active monthly users worldwide.&lt;/p&gt;&lt;p&gt;This browser now accounts for a significant share of traffic to GOV.UK. From December to January, 466,012 users visited GOV.UK using Samsung Internet, accounting for 3.2% of traffic. On GOV.UK&#39;s 10 most-used service start pages, Samsung Internet accounts for between 1% and 5% of traffic.&lt;/p&gt;&lt;h2 id=&quot;samsung-internet-and-chrome-for-android-are-different-and-more-changes-are-expected&quot;&gt;Samsung Internet and Chrome for Android are different and more changes are expected&lt;/h2&gt;&lt;p&gt;Samsung Internet is one of several browsers for the Android operating system built on Chromium, &lt;a href=&quot;https://www.chromium.org/&quot;&gt;the open source code project&lt;/a&gt; that&#39;s also the basis for the Google Chrome browser.&lt;/p&gt;&lt;p&gt;Webpages and features on Samsung Internet and Google Chrome for Android function in a similar way but this might change as the browsers diverge. Google Analytics started to distinguish between Samsung Internet and Chrome for Android in August 2017 - previously, it had counted them as the same browser.&lt;/p&gt;&lt;p&gt;Some &lt;a href=&quot;https://caniuse.com/#compare=and_chr+64,samsung+6.2&quot;&gt;functional differences between the 2 browsers&lt;/a&gt; are already apparent and web developers will need to consider slight differences in CSS, JavaScript and interface features when building for this browser mix.&lt;/p&gt;&lt;p&gt;In the future, there may be a fundamental difference in how pages render or function on Samsung Internet and Chrome for Android. That&#39;s why we think testing this new, popular browser is important.&lt;/p&gt;&lt;h2 id=&quot;having-more-browsers-around-is-better-for-everyone&quot;&gt;Having more browsers around is better for everyone&lt;/h2&gt;&lt;p&gt;Diversity in the browser market is great for users. Competition between browsers helps to stimulate development of the web, as browsers are iterated to become faster, more secure and easier to use. We&#39;ll keep monitoring use data to make sure GOV.UK keeps pace with the evolving browser space.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Removing Accelerated Mobile Pages (AMP)</title>
    <link href="https://nooshu.com/blog/2018/02/06/removing-amp/" />
    <updated>2018-02-06T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2018/02/06/removing-amp/</id>
    <content type="html">&lt;p&gt;Around 18 months ago I wrote a blog post called &lt;a href=&quot;https://nooshu.com/blog/2016/08/07/accelerated-mobile-pages/&quot;&gt;Accelerated Mobile Pages (AMP)&lt;/a&gt; (imaginative I know!) about implementing AMP into this very website. At the time it was purely out of curiosity. I happened to see a new navigation item pop up in the left hand panel of Webmaster Tools and thought I would give it a try. So why am I now removing it? I&#39;ve listed a few of my reasons below.&lt;/p&gt;&lt;h2 id=&quot;original-purpose&quot;&gt;Original Purpose&lt;/h2&gt;&lt;p&gt;The purpose of Google AMP is to improve the user experience of the web. It does this by enforcing a strict standard on a set of custom elements. These custom elements can then be highly optimised for speed. Speed is the key selling point when adopting AMP. Pages across the web are becoming bloated and slow to load and this is becoming a real problem on mobile devices which have a limited resources and data available to them. Web sites in 2018 have more code, larger images, more ads, more of everything! I&#39;m just glad I&#39;m no longer on a 56K modem, can you imagine the frustration?!&lt;/p&gt;&lt;p&gt;Now I must admit AMP pages load really quickly and one reason for this is a browser will &lt;a href=&quot;https://medium.com/@cramforce/why-amp-html-does-not-take-full-advantage-of-the-preload-scanner-7e7f788aa94e&quot;&gt;prerender the page&lt;/a&gt; before a user even clicks the link. This is really handy if you are on an intermittent connection (like on the tube for example). Need a page to read before you head into the next tunnel? Look for the AMP logo in the search results and it will already be there for you to read, no more data required (for the &quot;above-the-fold&quot; visible content). I hear you say &quot;But that is awesome! What&#39;s not to like?&quot;. Well for me it all comes down the finer details.&lt;/p&gt;&lt;h2 id=&quot;amp-html-implementation&quot;&gt;AMP HTML implementation&lt;/h2&gt;&lt;p&gt;The implementation wasn&#39;t difficult, it was just a real pain to make sure everything was valid. I use a static site generator called &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; which allows me to easily host this blog on GitHub pages. Unfortunately, because you are rendering from Markdown and not handcrafting HTML, you don&#39;t have 100% control over the HTML that comes out the other end. So if some HTML that is generated isn&#39;t considered valid AMP HTML, you either have to find a hack or you are out of luck. No Google AMP Cache for you! I mention one of these issues in the &lt;a href=&quot;https://nooshu.com/blog/2016/08/07/accelerated-mobile-pages/#fixing-jekyll-rouge&quot;&gt;original post&lt;/a&gt; around the use of Jekyll Rouge.&lt;/p&gt;&lt;h2 id=&quot;amp-js&quot;&gt;AMP JS&lt;/h2&gt;&lt;p&gt;AMP is very (very) strict when it comes to JavaScript. No JavaScript to be served other than AMP JS. So no &lt;a href=&quot;https://jquery.com/&quot;&gt;jQuery&lt;/a&gt;, no &lt;a href=&quot;https://getbootstrap.com/docs/4.0/getting-started/javascript/&quot;&gt;Bootstrap&lt;/a&gt; or &lt;a href=&quot;https://foundation.zurb.com/sites/docs/javascript.html&quot;&gt;Foundation&lt;/a&gt;, no nothing. The only JavaScript that is allowed to be served is AMP JS which is the library that powers AMP. Again note that it can only be served from Google, a self hosted version isn&#39;t allowed.&lt;/p&gt;&lt;p&gt;So what is AMP JS? Well it is used to to transform the custom elements that Google has defined into elements that the browser understands. So why no other JavaScript allowed? Well the purpose stated is that to maximise page load speed, the browsers critical rendering path needs to be minimised. Adding other JavaScript will break this requirement if implemented incorrectly, so it is safer to remove all other JavaScript completely. Seems a little extreme! Is it true that AMP JS is all about speed? Not according to Maciej Cegłowski who rewrote the &lt;a href=&quot;http://idlewords.com/amp_static.html&quot;&gt;original AMP demo page&lt;/a&gt; without using AMP JS and it turned out to be much smaller and much quicker.&lt;/p&gt;&lt;p&gt;So it isn&#39;t really about speed. I fear it is more to do with tracking and locking you into the ecosystem.&lt;/p&gt;&lt;h2 id=&quot;amp-cache-and-validity&quot;&gt;AMP Cache and validity&lt;/h2&gt;&lt;p&gt;Once all your pages are valid you are good to go though... not quite. There have been a couple of occasions where I would see that a number of pages had suddenly become invalid due to the AMP specification changing. Now this isn&#39;t a big deal in itself as the changes were fairly minor, but it does highlight the fact that you are essentially using a third-party product that is out of your control to display a large percentage of your website to your users, and what they say goes. In this case said third-party is Google, so what they really say goes!&lt;/p&gt;&lt;p&gt;For your content to be added to the AMP Cache it must validate. No exceptions. For a small site with a low number of pages like mine this isn&#39;t a big deal. For websites with tens of thousands of pages, that&#39;s potentially a lot of work! So if the specification changes and a 90% of your pages are suddenly invalid, best case scenario is it&#39;s a quick 2 minute change to a template, or you could be trawling through hundreds of pages to make them revalidate.&lt;/p&gt;&lt;h2 id=&quot;ecosystem&quot;&gt;Ecosystem&lt;/h2&gt;&lt;p&gt;Assuming you have taken the plunge and decided to adopt AMP, you are now essentially locked into the Google ecosystem. AMP is open source, but it is primarily driven by Google.&lt;/p&gt;&lt;p&gt;So you&#39;ve decided to take the plunge and use AMP. Here&#39;s what to expect:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;loading AMP JS: the JavaScript that powers AMP. This can only be served from the Google servers, no self-hosting allowed.&lt;/li&gt;&lt;li&gt;to use AMP Cache: a lot of the speed gained from AMP is the fact that the pages will be served from a CDN; these CDN&#39;s are run by Google.&lt;/li&gt;&lt;li&gt;a Google CDN URL when sharing content: Want to share a page you found that is using AMP with a friend? You will be most likely be sharing the Google CDN URL rather than content from the original author (NOTE: It looks like they &lt;a href=&quot;https://amphtml.wordpress.com/2018/01/09/improving-urls-for-amp-pages/&quot;&gt;fixed this very recently!&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So what do you get when you use all these Google technical toys? Well a big plus is you get preferential treatment from Google in terms of mobile search. Your content will be boosted up the search rankings above other content (on Google at least). Say you give it a try and realise it isn&#39;t for you. How easy is it to remove? Well it might not be as &lt;a href=&quot;https://shkspr.mobi/blog/2016/11/removing-your-site-from-amp/&quot;&gt;simple as you think&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I think you get the idea. Google is fully in control of the bus, you&#39;re just along for the ride.&lt;/p&gt;&lt;h2 id=&quot;creativity&quot;&gt;Creativity&lt;/h2&gt;&lt;p&gt;I think the main thing I don&#39;t like about AMP is the restrictions on &lt;a href=&quot;https://www.ampproject.org/learn/about-how/&quot;&gt;JavaScript&lt;/a&gt; and &lt;a href=&quot;https://www.ampproject.org/docs/guides/responsive/style_pages&quot;&gt;CSS&lt;/a&gt; usage. Now I understand why those restrictions and specifications are so strictly enforced as they are performance best practices, but it takes some of the adventure out of being a Web Developer. I like being able to add random JavaScript functionality that doesn&#39;t come pre-packaged in a toolkit. It is part of the fun. But then again maybe I&#39;m just the wrong audience for AMP.&lt;/p&gt;&lt;p&gt;I fully admit I magnified my creativity restrictions problem because I went all out in my AMP usage. You have two options in AMP usage:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Lay your AMP website next to your standard website HTML so they both exist in tandem.&lt;/li&gt;&lt;li&gt;Go all out and use AMP HTML for all breakpoints and only have a single version.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;I went for option 2. Had I decided to create two separate versions of the website this would have given me the creative freedom to add little bits of extra functionality here and there where needed. Which leads me onto the next oddity that I find unsettling...&lt;/p&gt;&lt;h2 id=&quot;duplicate-content&quot;&gt;Duplicate content&lt;/h2&gt;&lt;p&gt;For years one of the key rules when it came to SEO was never duplicate content. It was SEO 101, if you duplicate content Google will penalise you. Google will assume the content has been copied from another website and rank you down into oblivion. Now suddenly we find ourselves duplicated all of our websites content and essentially maintaining two separate websites. To me that isn&#39;t a good way to go, but maybe that&#39;s just me and I&#39;m starting to turn into an old man?&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;In conclusion I see why Google AMP exists and I understand what Google is trying to achieve (I think). I just don&#39;t like the fact that it is locking you into the Google ecosystem and creating another &quot;walled garden&quot; on the web. Google is already the most powerful company on the internet and Google AMP is only going to increase their dominance.&lt;/p&gt;&lt;p&gt;What would be nice is if all Web Developers take the best practices that AMP promotes in minimising the browsers critical rendering path, and apply them to their current sites. That way we get a fast, decentralised open web that isn&#39;t dominated by a single company. I wouldn&#39;t hold your breath waiting for that to happen though!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Opening Atlassian JIRA tickets with iTerm2</title>
    <link href="https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/" />
    <updated>2017-01-10T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/</id>
    <content type="html">&lt;p&gt;Happy New Year everyone! My first post of 2017 is going to be a very short, but it may just help you speed up your terminal workflow.&lt;/p&gt;&lt;p&gt;Two tools I use on a day to day basis are &lt;a href=&quot;https://www.iterm2.com/&quot;&gt;iTerm2&lt;/a&gt; and &lt;a href=&quot;https://www.atlassian.com/software/jira&quot;&gt;JIRA from Atlassian&lt;/a&gt;. iTerm2 is a terminal replacement app for OSX, and if you haven&#39;t used it before you really should give it a try (I&#39;ve mentioned it before in a previous &lt;a href=&quot;https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/&quot;&gt;blog post&lt;/a&gt;). JIRA is the go to web application for bug tracking; it also works well with the rest of the &lt;a href=&quot;https://www.atlassian.com/software&quot;&gt;Atlassian suite&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;For a while I&#39;ve wanted to be able to open tickets right from the terminal window. This would come in extremely helpful when looking at a &lt;code&gt;git log&lt;/code&gt; output like the one below for example:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Sample git log including Atlassian ticket references.&quot; decoding=&quot;async&quot; height=&quot;108&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/hBCFZGtCAI-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/hBCFZGtCAI-300.webp 300w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/hBCFZGtCAI-600.webp 600w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/hBCFZGtCAI-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Luckily iTerm2 comes with a whole host of features, one of which is called &#39;Smart Selection&#39;. You may not have noticed this before, but if you click multiple times on certain types strings they will be selected as a whole (email addresses, filesystem paths, URI&#39;s). This is Smart Selection in action. iTerm2 looks at the string directly under the cursor, runs a set of matching Regex&#39;s against it, and then allows you to run the relevant action against it.&lt;/p&gt;&lt;p&gt;We can use this feature to open the JIRA ticket page relating to the issue listed in our git logs like so:.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Animated screencast of Smart Selection in action.&quot; decoding=&quot;async&quot; height=&quot;708&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/BlUKjv-He6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/BlUKjv-He6-300.webp 300w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/BlUKjv-He6-600.webp 600w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/BlUKjv-He6-788.webp 788w&quot; width=&quot;788&quot;&gt;&lt;/p&gt;&lt;p&gt;Adding this functionality is very simple. Under iTerm2 preferences go to &#39;Profiles&#39;, then the &#39;Advanced&#39; tab. Here you will see the &#39;Smart Selection&#39; edit button. See the images below for the sample settings I&#39;ve used:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Example of the smart selection rule in iTerm2.&quot; decoding=&quot;async&quot; height=&quot;510&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/ZT_wEgp_6g-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/ZT_wEgp_6g-300.webp 300w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/ZT_wEgp_6g-600.webp 600w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/ZT_wEgp_6g-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Add &lt;code&gt;^[project-ticket-prefix]-(&#92;d+)&lt;/code&gt; to the Regex rules, then edit the corresponding action and add Open URL.. with your JIRA URL. &lt;em&gt;Note&lt;/em&gt;: This assumes the string starts with the ticket prefix, which is the case in my git logs. You may need to adjust this for your own setup.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Assigning a smart selection action in iTerm2.&quot; decoding=&quot;async&quot; height=&quot;508&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/52-QLdnle--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/52-QLdnle--300.webp 300w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/52-QLdnle--600.webp 600w, https://nooshu.com/blog/2017/01/10/opening-atlassian-jira-tickets-with-iterm2/52-QLdnle--814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;After that you should be able to right click on one of your tickets whenever they appear in the terminal and see the &#39;Open JIRA Ticket&#39; option. Simple!&lt;/p&gt;&lt;p&gt;If you&#39;re a ZSH user, you may also be interested in the &lt;a href=&quot;https://github.com/robbyrussell/oh-my-zsh/tree/master/plugins/jira&quot;&gt;JIRA plugin&lt;/a&gt; that allows you to interact with your JIRA instance via the CLI. I&#39;ve yet to use it myself, but it does look like it could be quite useful.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>A boilerplate module setup using Browserify, Babel, Mocha, and Chai</title>
    <link href="https://nooshu.com/blog/2016/12/16/browserify-boilerplate-module/" />
    <updated>2016-12-16T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2016/12/16/browserify-boilerplate-module/</id>
    <content type="html">&lt;p&gt;Over the past few months I&#39;ve been taking a look at a few of the ES2015 features that have landed in JavaScript language. There some excellent additions that will help JS developers write more succinct code. My personal favorites at the moment are &lt;a href=&quot;https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals&quot;&gt;Template literals&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Default_parameters&quot;&gt;Default parameters&lt;/a&gt;. Being able to specify a default parameter without a load of boilerplate code at the top of a function is a real win, and no more concatenating strings when you need to include a variable.&lt;/p&gt;&lt;p&gt;So while experimenting with the new features, I figured I&#39;d also bring a few other technologies together to build a very simple module boilerplate, that I could then use on future projects.&lt;/p&gt;&lt;h2 id=&quot;purpose-of-the-boilerplate&quot;&gt;Purpose of the boilerplate&lt;/h2&gt;&lt;p&gt;The initial purpose was to experiment with the ES2015 &lt;a href=&quot;https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/import&quot;&gt;import statement&lt;/a&gt;, but this then branched out to include Babel, and Browserify. Once those were implemented, I figured I would also roll in Mocha, Chai, and jQuery.&lt;/p&gt;&lt;p&gt;There was a big question in my head on how these could all work together to create a simple workflow, which could then be built upon in the future. My checklist of what I wanted to achieve with the experiment was:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Ability to use ES2015+ features&lt;/li&gt;&lt;li&gt;Split the code into separate modules, for easier maintenance and testability&lt;/li&gt;&lt;li&gt;Be able to bundle the JavaScript in an efficient manor&lt;/li&gt;&lt;li&gt;Work directly with the DOM using the querySelector methods, or even jQuery since I sometimes require old IE support&lt;/li&gt;&lt;li&gt;Be testable using a popular testing framework&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;technologies&quot;&gt;Technologies&lt;/h2&gt;&lt;p&gt;A list of the technologies used and their purpose is listed below:&lt;/p&gt;&lt;h3 id=&quot;es2015-the-language-formally-known-as-es6-formally-known-as-harmony&quot;&gt;ES2015 (the language formally known as ES6, formally known as Harmony)&lt;/h3&gt;&lt;p&gt;ES2015 is the latest iteration of the JavaScript language. The year is now being used in the name, so it is easy to distinguish when the changes in the language were added (in theory!). So as you would expect there&#39;s also an ES2016, and ES2017 in the pipeline. These latest iterations add a whole host of useful language features and improvements to make it easier to write JavaScript. The full ES2015 language specification can be found &lt;a href=&quot;http://www.ecma-international.org/ecma-262/6.0/&quot;&gt;here&lt;/a&gt;&lt;/p&gt;&lt;h3 id=&quot;babel&quot;&gt;Babel&lt;/h3&gt;&lt;p&gt;The problem with introducing new language features on the web platform is you still need to support older browsers. As the saying goes, you can&#39;t break the web. Because of this, nobody wants to write some lovely ES2015 code, and then have to write another version just for older browsers. Older browsers won&#39;t know how to interpret these new features, and the page will break.&lt;/p&gt;&lt;p&gt;This is where &lt;a href=&quot;https://babeljs.io/&quot;&gt;Babel&lt;/a&gt; comes to the rescue. Babel is a ES2015 compiler, that will transform the latest features back into ES5 compatible code so older browsers will still work. It even has the ability to polyfill methods that older browsers don&#39;t support; so there&#39;s no reason not to start learning and using ES2015 today.&lt;/p&gt;&lt;h3 id=&quot;mocha-and-chai&quot;&gt;Mocha &amp; Chai&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://mochajs.org/&quot;&gt;Mocha&lt;/a&gt; is a very popular JavaScript testing framework that runs on Node.js. It allows a developer to write and run a set of tests across their JavaScript code, and accurately report the results. Any tests that fail can then be debugged and fixed.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://chaijs.com/&quot;&gt;Chai&lt;/a&gt; is a BDD / TDD assertion library that can be used with any testing framework, but is often used in conjunction with Mocha. It comes with a whole set of API&#39;s that allow you to write your tests in whatever way you prefer (Should, Expect, Assert).&lt;/p&gt;&lt;p&gt;Using both libraries together can be a real life-saver when it comes to refactoring code. It is very quick and easy to see when new code changes have broken functionality (assuming you have written your tests correctly!).&lt;/p&gt;&lt;h3 id=&quot;jquery&quot;&gt;jQuery&lt;/h3&gt;&lt;p&gt;I don&#39;t think I really need to explain this one do I? Everyone knows jQuery, I&#39;m sure it has saved most of us from cross-browser hell in the past! I have included it here because I often have projects that still require IE8 support. In my opinion it is much easier to include jQuery, than pick out a host of micro-libs to do the same job, with all their custom API&#39;s and documentation.&lt;/p&gt;&lt;h3 id=&quot;browserify&quot;&gt;Browserify&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;http://browserify.org/&quot;&gt;Browserify&lt;/a&gt; is the &quot;glue&quot; that brings all of these technologies together. Browserify allows you to write code like you would in Node.js (using the require() method). This then gets bundled up into a single JavaScript file that the browser can execute. Browserify deals with all the code dependencies for you, and in the case of this demo also compiles ES2015 back to ES5 using Babel mentioned above.&lt;/p&gt;&lt;h2 id=&quot;simple-module-example&quot;&gt;Simple module example&lt;/h2&gt;&lt;p&gt;The code below shows a very simple example on how modules are imported into the main index.js file, then Browserify does the rest for you. The init() method kicks off the DOM interaction in module 1, and a sample unique array is being imported in from module 2.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// pull in everything from the sample module using a path&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; moduleName &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;./modules/sample-module&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// pull in single variable from a module using browserify alias (browserify.js)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; uniqueArray &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;sample-module-2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// start the module, pass any options we need&lt;/span&gt;
moduleName&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property literal-property&quot;&gt;exampleSetting&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;String Value&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// use a specific method in the module&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; addResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; moduleName&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; minusResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; moduleName&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;minus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// results from module 1&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; addResult&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; minusResult &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// results from module 2&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; uniqueArray &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The functionality of each module is hidden away in separate files, allowing you to easily modularise you code. Using this setup, it is easy to create private variables and methods inside the modules. A developer then only need expose what is required to the wider application using the &lt;code&gt;export&lt;/code&gt; keyword.&lt;/p&gt;&lt;p&gt;I&#39;ve created a very simple sample page you can view using the &lt;code&gt;grunt&lt;/code&gt; command. Grunt compiles everything and then launches a small BrowserSync http server that can be used to view the page. For more information on how to run the code see the &lt;a href=&quot;https://github.com/Nooshu/browserify-module-boilerplate&quot;&gt;Github readme&lt;/a&gt;. &lt;strong&gt;Note&lt;/strong&gt;: If you prefer to use Gulp instead of Grunt, this is fully possible. I simply used Grunt in the demo as I already had most of the code written from previous projects. This allowed me to focus on the JavaScript, rather than workflow tooling.&lt;/p&gt;&lt;h2 id=&quot;issues&quot;&gt;Issues&lt;/h2&gt;&lt;p&gt;There was a fair bit of trial and error with the initial setup to get it working as I wanted. My main issue occurred when it came to implementing Mocha, Chai, and jQuery. Testing jQuery in a Node environment became a real pain because of two errors I ran into: &lt;code&gt;ReferenceError: $ is not defined&lt;/code&gt; and &lt;code&gt;Error: jQuery requires a window with a document&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The first, &lt;code&gt;$ no defined&lt;/code&gt;, was easily solved. In the browser environment I had jQuery loaded as a global object via its own script. You then tell Browserify this is the case using &lt;code&gt;&quot;jquery&quot;: &quot;global:jQuery&quot;&lt;/code&gt; in the package.json file. This of course didn&#39;t work in Node since it isn&#39;t a browser environment, so the script wasn&#39;t loaded. Adding &lt;code&gt;import $ from &#39;jquery&#39;;&lt;/code&gt; inside the module fixed that.&lt;/p&gt;&lt;p&gt;The second, &lt;code&gt;jQuery requires a window with a document&lt;/code&gt;, took a good few hours to fix. I tried everything I could find on Stack Overflow including &lt;a href=&quot;https://github.com/tmpvar/jsdom&quot;&gt;jsdom&lt;/a&gt; and &lt;a href=&quot;https://github.com/rstacruz/mocha-jsdom&quot;&gt;mocha-jsdom&lt;/a&gt;; But no matter what setup I tried, the error still occurred. In the end it was installing &lt;a href=&quot;https://github.com/rstacruz/jsdom-global&quot;&gt;jsdom-global&lt;/a&gt; that fixed it. Install it, create yourself a &lt;code&gt;mocha.opts&lt;/code&gt; file for your the custom settings, and you are ready to go.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Example test description&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        $ &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;jquery&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// your tests go here&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then you can either run Mocha from the command line using &lt;code&gt;npm run test&lt;/code&gt; with the following settings in your package.json:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token property string-property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property string-property&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./node_modules/mocha/bin/mocha ./src/js/modules/test --reporter list --opts ./src/js/modules/test/mocha.opts&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or what I prefer to do is run the tests directly in my IDE. &lt;a href=&quot;https://www.jetbrains.com/webstorm/&quot;&gt;JetBrains WebStorm&lt;/a&gt; is my editor of choice, just remember you may need to point your IDE towards the &lt;code&gt;mocha.opts&lt;/code&gt; file in the test settings.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using Mocha for JavaScript testing inside JetBrains WebStorm.&quot; decoding=&quot;async&quot; height=&quot;460&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2016/12/16/browserify-boilerplate-module/wUgmNfph3H-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2016/12/16/browserify-boilerplate-module/wUgmNfph3H-300.webp 300w, https://nooshu.com/blog/2016/12/16/browserify-boilerplate-module/wUgmNfph3H-600.webp 600w, https://nooshu.com/blog/2016/12/16/browserify-boilerplate-module/wUgmNfph3H-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;This is my first iteration of a module system I plan to use on new projects going forwards, and it will continue to evolve to fit particular project requirements as they are discovered. You can find all the code, along with the very simple example on &lt;a href=&quot;https://github.com/Nooshu/browserify-module-boilerplate&quot;&gt;GitHub&lt;/a&gt;. Feel free to use it if you like, or even suggest improvements. I&#39;m always open to constructive criticism so raise an issue or get in contact with me via the contact page.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Accelerated Mobile Pages (AMP)</title>
    <link href="https://nooshu.com/blog/2016/08/07/accelerated-mobile-pages/" />
    <updated>2016-08-07T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2016/08/07/accelerated-mobile-pages/</id>
    <content type="html">&lt;p&gt;One of the advantages of having a blog, especially one built using a static site generator like &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt;, is it&#39;s extremely easy to craft the HTML and have it compile as you&#39;d expect it too. No CMS rendering out a load of HTML / CSS / JS that most likely isn&#39;t required and just bloats the page. Having such a site also allows me to experiment with technology that would require quite a major refactor on a larger client build. Recently I have been looking at &lt;a href=&quot;https://www.ampproject.org/&quot;&gt;Accelerated Mobile Pages&lt;/a&gt; (AMP). I have implemented the changes required to support it across the blog... did you notice?&lt;/p&gt;&lt;h2 id=&quot;what-is-amp&quot;&gt;What is AMP?&lt;/h2&gt;&lt;p&gt;Accelerated Mobile Pages is an open source project that aims to improve a user experience while browsing web content on a mobile device. It does this by taking standard HTML, and limiting its usage to a specific set of functionality that is clearly defined in the AMP spec documentation. The reason for this limitation is to improve performance on mobile devices, by only allowing current best practices across a wide range of component patterns. So for example, there are strict rules around the use of script and style tags. Too many of each could potentially slow down a page on a mobile device by blocking page rendering. AMP comes with its own validator that will report these errors so they can be corrected.&lt;/p&gt;&lt;p&gt;AMP comes in three parts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;AMP HTML&lt;/li&gt;&lt;li&gt;AMP JS&lt;/li&gt;&lt;li&gt;Google AMP Cache&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;AMP HTML is much like regular HTML, only extended slightly with a set of custom AMP properties. Some regular HTML tags have been replaced with AMP specific tags, allowing for common development patterns to be easily implemented in a performant manor. See the &lt;a href=&quot;https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md&quot;&gt;AMP HTML Spec&lt;/a&gt; for more details.&lt;/p&gt;&lt;p&gt;AMP JS is the glue that binds everything together. It is a library that adds all the custom elements mentioned above, and implements all of the AMP best practices. A key feature of the library includes allowing only asynchronous scripts, so nothing can block the DOM. Another large change (for me anyway) was all resources must be sized statically, meaning all images must have a width and height attribute. Including this width and height attribute allows the browser to render a page much quicker and with less need for redrawing as the DOM is constructed.&lt;/p&gt;&lt;p&gt;I have yet to see the Google AMP Cache in action, but now that the site is live with the tweaked AMP HTML and JS, I will monitor &lt;a href=&quot;https://www.google.com/webmasters/tools/&quot;&gt;Google Webmaster Tools&lt;/a&gt; for AMP errors so Google can start caching the pages. From what I can tell this is an automated process that happens when the pages are crawled, and not something a developer has direct control over (other than to fix errors).&lt;/p&gt;&lt;h2 id=&quot;so-what-did-i-change&quot;&gt;So what did I change?&lt;/h2&gt;&lt;p&gt;Most of the changes I needed to make were fairly minor. These included:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Adding CSS boilerplate code for AMP&lt;/li&gt;&lt;li&gt;Inlining current CSS in amp-custom style tag&lt;/li&gt;&lt;li&gt;Including required AMP JS library. This also included the form, service worker, and analytics extension scripts&lt;/li&gt;&lt;li&gt;Fixing the markup that was output by the &lt;a href=&quot;http://rouge.jneen.net/&quot;&gt;Rouge code highlighter&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There were a couple of changes that required a lot more work:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Converting standard img tags into &lt;a href=&quot;https://www.ampproject.org/docs/reference/amp-img.html&quot;&gt;amp-img&lt;/a&gt; tags&lt;/li&gt;&lt;li&gt;Making sure all images had a width and height attributes&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The use of amp-img gives a few performance benefits including lazy-loading and srcset support (polyfilled if not native to the browser). AMP-img requires a width and height attribute to be set so it can calculate the correct aspect-ratio of the image. In my case this was very time consuming task, as I had to manually add the width and height attributes for each image used across the blog. If I were to be using a standard CMS, I&#39;d be able to add these attributes very quickly then let the server do all the work!&lt;/p&gt;&lt;p&gt;An example of a basic AMP HTML page is given below. As you can see it is all very standard apart from a couple of custom components from AMP. For more information on the AMP HTML setup &lt;a href=&quot;https://www.ampproject.org/docs/get_started/create/basic_markup.html&quot;&gt;see here&lt;/a&gt;.&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;doctype&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;amp&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;en&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;utf-8&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Hello, AMPs&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;canonical&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;http://example.ampproject.org/article-metadata.html&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;viewport&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;width=device-width,minimum-scale=1,initial-scale=1&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;style&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;amp-boilerplate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;-webkit-animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;-amp-start 8s &lt;span class=&quot;token function&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;end&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 0s 1 normal both&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;-moz-animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;-amp-start 8s &lt;span class=&quot;token function&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;end&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 0s 1 normal both&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;-ms-animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;-amp-start 8s &lt;span class=&quot;token function&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;end&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 0s 1 normal both&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;-amp-start 8s &lt;span class=&quot;token function&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;end&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 0s 1 normal both&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@-webkit-keyframes&lt;/span&gt; -amp-start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;visible&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@-moz-keyframes&lt;/span&gt; -amp-start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;visible&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@-ms-keyframes&lt;/span&gt; -amp-start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;visible&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@-o-keyframes&lt;/span&gt; -amp-start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;visible&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@keyframes&lt;/span&gt; -amp-start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;visible&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;style&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;amp-boilerplate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;-webkit-animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;-moz-animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;-ms-animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;none&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;style&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;amp-custom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* Custom CSS goes here */&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- AMP analytics script added here if required --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://cdn.ampproject.org/v0.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Any additional AMP scripts added here --&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Welcome to the mobile web&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;useful-links&quot;&gt;Useful links&lt;/h2&gt;&lt;p&gt;Here are a few links I found useful, and are a good starting point if you wish to implement AMP on your own site.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/ampproject/amphtml&quot;&gt;Project GitHub&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://validator.ampproject.org/&quot;&gt;AMP HTML validator&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/amp-validator/nmoffdblmcmgeicmolmhobpoocbbmknc&quot;&gt;Chrome validator extension&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/tagged/amp-html&quot;&gt;Stackoverflow questions&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;useful-image-size-shell-script&quot;&gt;Useful image size shell script&lt;/h2&gt;&lt;p&gt;As I mentioned earlier in the post I use Jekyll as my static site generator. Because of this I had to add all the height and width attributes manually. Not a simple task when there are over 400 images that need fixing. To speed up the task I put together a very simple shell script. It loops through any images in a given directory, and outputs the width and height of each in the correct format for my Jekyll setup.&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token important shebang&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;shopt&lt;/span&gt; &lt;span class=&quot;token variable parameter&quot;&gt;-s&lt;/span&gt; nullglob
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable for-or-select&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; *.jpg *.png *.gif
&lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
	identify &lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;{print $1&quot; &quot;$3}&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;{print $1; split($2,a,&quot;x&quot;); print &quot;width: &quot;a[1]&quot;&#92;nheight: &quot;a[2]&quot;&#92;n&quot;}&#39;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An example of the output of the script to the console is given below. This can easily be copy / pasted into the relevant markdown file.&lt;/p&gt;&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;01-initial.gif
width: 767
height: 764

02-rigid.gif
width: 766
height: 764

03-large-panel.gif
width: 766
height: 764

featured.gif
width: 600
height: 250&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;fixing-jekyll-rouge&quot;&gt;Fixing Jekyll Rouge&lt;/h2&gt;&lt;p&gt;AMP is very picky about the structure of your HTML. If your pages don&#39;t validate then they won&#39;t be cached by Google, which is what you want for optimum performance on mobile. The main problem I ran into here was Jekyll Rouge was creating HTML that wasn&#39;t AMP valid. A couple of inline style attributes on a table element were enough to invalidate the page.&lt;/p&gt;&lt;p&gt;Looking at the Rouge GitHub page I could see there was already an &lt;a href=&quot;https://github.com/jneen/rouge/issues/149&quot;&gt;open issue&lt;/a&gt;; and it isn&#39;t going to be fixed anytime soon. Luckily there is a way using the Liquid template engine to modify the HTML output before final compilation:&lt;/p&gt;&lt;pre class=&quot;language-liquid&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-liquid&quot;&gt;{ % if _code contains &#39;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;pre&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;lineno&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&#39; % }
    { % assign _code = _code | replace: &#39;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;table&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-css css value&quot;&gt;&lt;span class=&quot;token property&quot;&gt;border-spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&#39;, &#39;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;table&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&#39; % }
    { % assign _code = _code | replace: &#39;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;td&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;gutter gl&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-css css value&quot;&gt;&lt;span class=&quot;token property&quot;&gt;text-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&#39;, &#39;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;td&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;gutter gl&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&#39; % }
{ % endif % }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above code looks for the &lt;code&gt;pre class=&quot;lineno&quot;&lt;/code&gt; element, and does a search and replace on the HTML where required. To use, wrap your Liquid highlight include like so:&lt;/p&gt;&lt;pre class=&quot;language-liquid&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-liquid&quot;&gt;{ % capture _code % }{ % highlight plaintext linenos=table % }
{ % endhighlight % }{ % endcapture % }{ % include fixlinenos.html % }{ { _code } }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And there you have it, valid AMP HTML coming from Rouge. Just remember to add the required CSS back into your standard stylesheet.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: I have added spaces around the liquid tags to stop Jekyll getting confused when compiling. Remove these spaces when you add them to your build.&lt;/p&gt;&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h2&gt;&lt;p&gt;AMP is fairly simple to implement, although it does come with a strict set of functional restrictions which you will need to work with for your pages to validate. There&#39;s an &lt;a href=&quot;https://www.ampproject.org/docs/reference/extended.html&quot;&gt;extensive range of components&lt;/a&gt; that can be used today, with more being developed and tested all the time. Time will tell on whether the extra development effort is worth it in terms of performance and usability. I&#39;ve defiantly noticed a speed increase of the site on a mobile when loading over a 3G connection (although I haven&#39;t timed it to be 100% sure it isn&#39;t a placebo). I will be monitoring my analytics data closely over the coming weeks, to see if there&#39;s any data that can support a case for overall improvement. But is it the future of the mobile Web? Ask me again in 5 years and I&#39;ll tell you! It is certainly an interesting approach that you should consider investigating if you want to increase your mobile user base.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Implementing a Service Worker</title>
    <link href="https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/" />
    <updated>2016-07-22T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/</id>
    <content type="html">&lt;p&gt;I recently had the pleasure of attending &lt;a href=&quot;https://skillsmatter.com/conferences/7278-fullstack-2016-the-conference-on-javascript-node-and-internet-of-things&quot;&gt;FullStack 2016&lt;/a&gt;, an excellent three day conference hosted in London (just around the corner from where I work, which is handy). The event had five presentation streams being held at the concurrently, which guaranteed a few interesting talks for everyone. If you get a chance to attend in 2017, I highly recommend it.&lt;/p&gt;&lt;p&gt;The main theme I noticed running over the three days was a new development technique emerging called Progressive Web Apps (PWA). A Progressive Web App uses the latest browser technologies to bring a native look and feel to your web application. Imagine being able create a web application that performs like a native application and looks like a native application but is built on web technologies. Sounds too good to be true! No need to create multiple native applications, one for each OS; build one web application and then allow the user to &quot;install&quot; it so it looks and performs like it is native.&lt;/p&gt;&lt;p&gt;The features of a PWA include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Add to home screen functionality&lt;/li&gt;&lt;li&gt;Push notifications&lt;/li&gt;&lt;li&gt;Responsive so flexes to any screen size&lt;/li&gt;&lt;li&gt;Background data synchronisation&lt;/li&gt;&lt;li&gt;Instant loading with the use of Service Workers&lt;/li&gt;&lt;li&gt;Secure loading through HTTPS (this is required for a service worker to function)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The final two bullet points touch on the key technology powering PWA&#39;s: Service Workers.&lt;/p&gt;&lt;h2 id=&quot;what-is-a-service-worker&quot;&gt;What is a service worker?&lt;/h2&gt;&lt;p&gt;A service worker is essentially a proxy that sits between the browser and the network. It allows us as developers to intercept network requests, manipulate and respond to them in a way that is advantageous to our application.&lt;/p&gt;&lt;p&gt;The big difference between a native application and a web application is that a native application is primarily based offline. If there&#39;s no network, the experience may be very limited depending on the app developer choices, and you may not get the most up-to-date data, but you still get some sort of experience. However if you have no network connection with a web application...well you have no web application. You will most likely just get a blank error screen saying no network available. A very poor experience for the user.&lt;/p&gt;&lt;p&gt;This is where a service worker can help us, allowing us to create an offline experience by caching key assets that can still be accessed by the application. Once configured a service worker will step through to following stages:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Register&lt;/strong&gt; - Register a service worker with the browser, it doesn&#39;t do anything yet but it is now available for configuration&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Install&lt;/strong&gt; - On install the service worker can pre-cache any assets required for the offline experience&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Activate&lt;/strong&gt; - With the service worker now active it can begin to monitor network activity and manage the cache state&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Fetch&lt;/strong&gt; - The fetch event is fired every time a request is made. Here we can manipulate responses and hook into the pre-cache from the install event&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;offline-first&quot;&gt;Offline-first&lt;/h2&gt;&lt;p&gt;This is where we as developers need to start thinking &quot;offline-first&quot;. Ask yourself, If your web application suddenly disconnected from the network, but you still had access to online assets, what functionality would be useful to the user? Some thoughts I&#39;ve had around this are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Latest x number of blog posts or press releases&lt;/li&gt;&lt;li&gt;Contact information and map to your office location&lt;/li&gt;&lt;li&gt;A &lt;a href=&quot;https://www.theguardian.com/info/developer-blog/2015/nov/04/building-an-offline-page-for-theguardiancom&quot;&gt;simple game&lt;/a&gt; to keep the user occupied&lt;/li&gt;&lt;li&gt;A promotional, or just entertaining video (depending on size)&lt;/li&gt;&lt;li&gt;If the site is down, pull in a status feed from Twitter for example&lt;/li&gt;&lt;li&gt;A simple offline page mentioning they have connectivity issues&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The list of creative uses for an offline page is endless, it really depends on your user demographic and the time you are willing to spend crafting them.&lt;/p&gt;&lt;h2 id=&quot;think-performance&quot;&gt;Think performance&lt;/h2&gt;&lt;p&gt;Service workers don&#39;t only improve a users offline experience, they can also improve their online experience too by way of performance. The fetch event gives a developer the power to monitor requests and craft custom responses back to the browser. What this allows us to do is look to see if a particular file is in the cache, and if it is serve it back from disk. If it isn&#39;t in the cache it will request it from the network as per usual. Serving an asset from disk will always be quicker than a network request, so if you choose your assets wisely you can minimise the amount of new data being fetched on each page load. Just think of it as a browser cache that we have complete control over!&lt;/p&gt;&lt;p&gt;I&#39;ve implemented a very simple service worker on this blog. If you open your browser developer console you will see a console info message stating that the service worker has been registered (assuming your browser supports them). To see it in action try disconnecting from your network and refresh the page. You should see a very basic offline page stating you have network connectivity issues. If you want to drill down into the inner workings of the service worker you will need &lt;a href=&quot;https://www.google.co.uk/chrome/browser/canary.html&quot;&gt;Chrome Canary&lt;/a&gt; (version 54 at the time of writing). This version will give you a brand new application tab where you can view the active service worker and its cache.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome Canary has a new application tab for use with service workers&quot; decoding=&quot;async&quot; height=&quot;426&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/QsYr2TR_5z-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/QsYr2TR_5z-300.webp 300w, https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/QsYr2TR_5z-600.webp 600w, https://nooshu.com/blog/2016/07/22/implementing-a-service-worker/QsYr2TR_5z-814.webp 814w&quot; title=&quot;Service worker is visible and active in Chrome Canary version 54.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;&lt;p&gt;If you want to take a look at the full service worker JavaScript I&#39;ve used it is available &lt;a href=&quot;https://nooshu.github.io/sw.js&quot;&gt;here&lt;/a&gt;. The script borrowed heavily from the following resources:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://googlechrome.github.io/samples/service-worker/custom-offline-page/&quot;&gt;Service Worker Sample: Custom Offline Page Sample&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://blog.lamplightdev.com/2015/01/06/A-Simple-ServiceWorker-App/&quot;&gt;A simple ServiceWorker app&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;ve split the script down into the separate stages below with a brief set of comments for explanation.&lt;/p&gt;&lt;h3 id=&quot;register&quot;&gt;Register&lt;/h3&gt;&lt;p&gt;Add the following code to your HTML pages.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// check see if your browser supports service workers&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;serviceWorker&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceWorker
        &lt;span class=&quot;token comment&quot;&gt;// register the service worker script&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/sw.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// using promises tell us if successful or there was an error&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;reg&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Service Worker registration successful: &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reg&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Service Worker setup failed: &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;install&quot;&gt;Install&lt;/h3&gt;&lt;p&gt;The code sections below sit inside the service worker JavaScript file (sw.js).&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;install&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// extend the events lifetime until the promise resolves&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// SW won&#39;t be considered installed until all caching is complete&lt;/span&gt;
    event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;waitUntil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// resolve the cache object that matches the name&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// it is created if it doesn&#39;t exist&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// return a Promise once resolved&lt;/span&gt;
        caches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheNameStatic&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;// add all the following resources to the cache&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
                    &lt;span class=&quot;token constant&quot;&gt;OFFLINE_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token constant&quot;&gt;FOUR_OH_FOUR_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/css/styles.css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/css/styles.min.css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/bubble.svg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/cloud-r.svg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/fox-bg.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/grid.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/iceburg.svg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/lines.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/narwhals-bg.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token string&quot;&gt;&#39;/images/whale.svg&#39;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;activate&quot;&gt;Activate&lt;/h3&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;activate&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// extend the events lifetime until the promise resolves&lt;/span&gt;
    event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;waitUntil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// return a Promise that resolves to an array of cache names&lt;/span&gt;
        caches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cacheNames&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;// passes an array of values from all the promises in the iterable object&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;token comment&quot;&gt;// map over the cacheNames array&lt;/span&gt;
                    cacheNames&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cacheName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;token comment&quot;&gt;// if any existing caches don&#39;t match the current used cache, delete them&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentCacheNames&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; caches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;fetch&quot;&gt;Fetch&lt;/h3&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// NOTE: the fetch event is triggered for every request on the page. So for every individual CSS, JS and image file.&lt;/span&gt;
self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;fetch&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// only respond if navigating for a HTML page&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// see https://googlechrome.github.io/samples/service-worker/custom-offline-page/ for more details&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;navigate&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;method &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;GET&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;accept&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;text/html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;respondWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// make sure the request we are making isn&#39;t in the cache&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createCacheBustedRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;token comment&quot;&gt;// if the response has a 404 code, serve the 404 page&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; caches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FOUR_OH_FOUR_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;token comment&quot;&gt;// check see if the response is in the cache, if not fetch it from the network&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; caches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;// If catch is triggered fetch has thrown an exception meaning the server is most likely unreachable&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; caches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;OFFLINE_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// respond to all the other fetch events&lt;/span&gt;
        event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;respondWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            caches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// if the request is in the cache, send back the cached response. If not fetch from the network&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;learning-resources&quot;&gt;Learning Resources&lt;/h2&gt;&lt;p&gt;There are some great resources around the web if you want to learn more about service workers, I have listed a few below (remember to check out the two links above too):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;http://www.html5rocks.com/en/tutorials/service-worker/introduction/&quot;&gt;Introduction to Service Worker&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API&quot;&gt;Service Worker API&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md&quot;&gt;Service workers explained&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=qDJAz3IIq18&quot;&gt;Video - Instant-loading Offline-first (Progressive Web App Summit 2016)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://skillsmatter.com/skillscasts/8155-an-introduction-to-service-workers&quot;&gt;Video - An introduction to Service Workers (FullStack 2016)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://skillsmatter.com/skillscasts/8154-thinking-offline&quot;&gt;Video - Thinking offline (FullStack 2016)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.udacity.com/course/offline-web-applications--ud899&quot;&gt;Course - Offline Web Applications&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I hope this post gave you a little insight into the potential of the service worker, and the Offline-first era we are about to enter into! If you know of any others resources, get in touch and I will add them to the list.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>HTML Email Countdown</title>
    <link href="https://nooshu.com/blog/2016/06/24/html-email-countdown/" />
    <updated>2016-06-24T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2016/06/24/html-email-countdown/</id>
    <content type="html">&lt;p&gt;Now I must admit I don&#39;t get excited about HTML emails very often. After years (and years) of having to painstakingly build them, and weeping over the horrible quirks of email rendering engines (Lotus Notes anyone?), it is easy to understand why. But an email landed in my email account the other day that I was quite impressed with.&lt;/p&gt;&lt;p&gt;A large company here in the UK sent me an email promoting one of its soon to be released products. All perfectly normal, but then over to the right hand side I saw an animated clock that was counting down the number of days until launch. This was intriguing to say the least as HTML emails are very primitive beasts. Usually laid out using tables, CSS support has its quirks, and JavaScript... not a chance! So how exactly was the countdown clock working?&lt;/p&gt;&lt;p&gt;After a minute or so digging around in DevTools I could quickly see what they were doing. Using their server (via a proxy) running PHP they were passing a set of URL parameters, which in turn was generating an animated gif on the fly, and serving it back to the email inside an image src attribute. Very interesting technique, as it actually produces an accurate countdown clock every time you reload the email. Here&#39;s an example of the URL they were using:&lt;/p&gt;&lt;p&gt;&lt;code&gt;https://www.foo-bar.co.uk/clock/index.php?time=2016-06-13+23:00&amp;width=400&amp;bg=f00000&amp;shadow=f00000&amp;font=000&lt;/code&gt;&lt;/p&gt;&lt;p&gt;The parameters being sent are pretty self-explanatory. The fact that the company can change the date and the colours means they can use the same technique over and over again for different products with different email designs. It got me thinking; I wonder if I can create something similar using node.js? After a little bit of research I found a few tools I could use to mock something up:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;http://momentjs.com&quot;&gt;Moment.js&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/Automattic/node-canvas&quot;&gt;Node-canvas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/eugeneware/gifencoder&quot;&gt;GifEncoder&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Moment.js is the go-to library if you want to do any form of date / time manipulation in JavaScript. Super simple API, very well tested, and comes with an excellent set of documentation. Node-canvas is a canvas implementation in node, using &lt;a href=&quot;https://www.cairographics.org/&quot;&gt;Cairo&lt;/a&gt;. It allows us to draw whatever we like to the canvas and use it as a frame in our Gif animation. The final piece of the puzzle is GifEncoder. It takes our canvas frames, ties them all together as an animation and encodes them into a Gif image we can view. Setup was all quite simple. I only ran into &lt;a href=&quot;https://github.com/Automattic/node-canvas/issues/548&quot;&gt;one issue&lt;/a&gt; where my version Cairo was out-of-date leading to all the characters being squashed together. After updating to the latest version using &lt;code&gt;homebrew&lt;/code&gt;, and a quick &lt;code&gt;npm install&lt;/code&gt; everything worked as expected.&lt;/p&gt;&lt;p&gt;Below you can see the final result of what I wrote. A simple animated countdown gif generated on the fly using node.js.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Animated countdown gif using node.js.&quot; decoding=&quot;async&quot; height=&quot;148&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2016/06/24/html-email-countdown/xkMGkxbJlL-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2016/06/24/html-email-countdown/xkMGkxbJlL-300.webp 300w, https://nooshu.com/blog/2016/06/24/html-email-countdown/xkMGkxbJlL-600.webp 600w, https://nooshu.com/blog/2016/06/24/html-email-countdown/xkMGkxbJlL-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It is worth noting that as we are generating and manipulating raw pixel data, the more pixels you have, the slower it is to create the gif files. Larger gifs with more frames can take over 2 seconds to generate, which isn&#39;t great in terms of performance. But it does the job, and I enjoyed building it so win-win.&lt;/p&gt;&lt;p&gt;You can view the code on &lt;a href=&quot;https://github.com/Nooshu/node-countdown-gif&quot;&gt;GitHub&lt;/a&gt;, and see a live demo on &lt;a href=&quot;https://date-gif.herokuapp.com/&quot;&gt;Heroku&lt;/a&gt;. The GitHub readme file has an explanation as to what options are available, as well as the methods of how to retrieve the generated gif.&lt;/p&gt;&lt;p&gt;&lt;small&gt;Note: I am on the free Heroku plan which shuts down after 30 minutes of inactivity, so it may take a second or so to spin back up once requested.&lt;/small&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Reasons to be Bashful</title>
    <link href="https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/" />
    <updated>2016-04-13T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/</id>
    <content type="html">&lt;p&gt;There&#39;s been some big news in the past week or so, &lt;a href=&quot;https://blogs.windows.com/buildingapps/2016/03/30/run-bash-on-ubuntu-on-windows/&quot;&gt;Bash is coming to Windows 10&lt;/a&gt;. Now I have to admit, this has got me pretty excited to say the least. For those who don&#39;t know, Bash is an acronym for &lt;a href=&quot;https://en.wikipedia.org/wiki/Bash_(Unix_shell)&quot;&gt;&quot;Bourne-again shell&quot;&lt;/a&gt;, an interactive-shell that is used on UNIX bases operating systems.&lt;/p&gt;&lt;p&gt;Over the past 3 years I&#39;ve moved away from developing on a Windows machine purely to developing on Mac. One of the main reasons for this is due to the terminal. Tooling for Interface Development has become heavily invested in Bash; with tools like &lt;a href=&quot;http://gruntjs.com/&quot;&gt;Grunt&lt;/a&gt;, &lt;a href=&quot;http://gulpjs.com/&quot;&gt;Gulp&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/&quot;&gt;npm&lt;/a&gt;, and &lt;a href=&quot;https://www.vagrantup.com/&quot;&gt;Vagrant&lt;/a&gt; becoming a major part of and Interface Developers workflow; including mine. So much so I have no idea how I lived without them.&lt;/p&gt;&lt;p&gt;The primary reason I am excited about the move towards Bash by Microsoft is due to the way window management works on OSX… I basically find it clunky to use. Maybe it&#39;s just me, but navigating and moving files around is just a real pain. Navigating between windows is also very hit and miss. For example if I have multiple windows open for Chrome, if I &lt;code&gt;CMD + Tab&lt;/code&gt; onto Chrome, how do I get to the other windows, not just the last one used? I know pressing the alt key at the same time brings a minimised window to the front, but it just isn&#39;t intuitive&lt;sup&gt;†&lt;/sup&gt;. So the fact that in the future I will be able to use Windows window management and Bash together is perfect!&lt;/p&gt;&lt;p&gt;So what can Bash be used for? Anything and everything would be an accurate answer. If you want do dig down into the inner workings of your computer, Bash is the way to go (&lt;strong&gt;NOTE&lt;/strong&gt;: be very careful with the commands you run. It is quite easy to kill your machine by copying commands from a random forum if you don&#39;t know what they do!).&lt;/p&gt;&lt;p&gt;Here&#39;s a very small selection of what the Bash prompt can do for you:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Move / copy files and directories around your machine&lt;/li&gt;&lt;li&gt;Remove files and directories (very powerful, be careful!)&lt;/li&gt;&lt;li&gt;Open files listed files in your favourite editor&lt;/li&gt;&lt;li&gt;Find files using the &lt;code&gt;find&lt;/code&gt; command, look inside files using the &lt;code&gt;grep&lt;/code&gt; command&lt;/li&gt;&lt;li&gt;Edit file permissions&lt;/li&gt;&lt;li&gt;Piping one commands output to another commands input&lt;/li&gt;&lt;li&gt;Administer your computer as well as remote machines&lt;/li&gt;&lt;li&gt;Combine all of the above with its own &lt;a href=&quot;https://en.wikipedia.org/wiki/Shell_script&quot;&gt;scripting language&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Honestly, the list above is just an incredibly small selection of what you can do with Bash. I&#39;ve been using it for a few years and Im only just scratching the surface of what is possible. It certainly is a powerful tool to have in your toolkit.&lt;/p&gt;&lt;h2 id=&quot;my-setup&quot;&gt;My Setup&lt;/h2&gt;&lt;p&gt;So I thought I&#39;d give you a small rundown of what I use Bash for, and a set of tweaks &amp; tools I use to make Interface Development a little easier. I&#39;m always interested in hearing how to improve my workflow, so if there&#39;s anything I&#39;m missing &lt;a href=&quot;https://nooshu.com/contact&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;My terminal setup using iTerm2&quot; decoding=&quot;async&quot; height=&quot;490&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/uAl4DuiOLY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/uAl4DuiOLY-300.webp 300w, https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/uAl4DuiOLY-600.webp 600w, https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/uAl4DuiOLY-814.webp 814w&quot; title=&quot;iTerm2 in action with the powerlevel9k theme and Tig.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;dotfiles&quot;&gt;Dotfiles&lt;/h3&gt;&lt;p&gt;There&#39;s no need to stick with the standard Bash setup, you can customise the way bash works by modifying a set of config files (.bashrc, .bash_profile etc). These are known as dotfiles and are usually found in you user profiles home folder. You can modify the look and feel, as well as add custom aliases of commands you frequently use to speed up your workflow. A very useful technique I have used is to store all my dotfiles in a single directory, then symlink them to the correct place in my home directory. Add this single directory to git and hey presto; if your computer dies you can easily restore your Bash profile setup. A very in-depth tutorial on how to do this can be found &lt;a href=&quot;http://blog.smalleycreative.com/tutorials/using-git-and-github-to-manage-your-dotfiles/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you are interested in seeing what other developers do with their dotfiles I highly recommend you check out &lt;a href=&quot;https://dotfiles.github.io/&quot;&gt;https://dotfiles.github.io/&lt;/a&gt;. Here you will find a huge list of dotfile setups from all over GitHub. It is a real treasure trove of useful commands and potential workflow timesavers all in one place!&lt;/p&gt;&lt;h3 id=&quot;zsh&quot;&gt;ZSH&lt;/h3&gt;&lt;p&gt;Once you discover &lt;a href=&quot;http://www.zsh.org/&quot;&gt;zsh&lt;/a&gt; you will never go back to a standard Bash setup. Zsh can be thought of as an extension to the Bourne shell. It comes with a whole heap of improvements and features from other interactive shells. The zsh website is a little bare, so I&#39;d recommend you take a look at the &lt;a href=&quot;https://github.com/robbyrussell/oh-my-zsh&quot;&gt;oh-my-zsh&lt;/a&gt; GitHub page. It is a community-driven framework for your zsh configuration. It includes plugins, themes and other handy additions to improve your shell.&lt;/p&gt;&lt;p&gt;I personally use the &lt;a href=&quot;https://github.com/bhilburn/powerlevel9k&quot;&gt;powerlevel9k&lt;/a&gt; for my theme (seen above) with the git, vagrant, and npm plugins enabled. It is recommended you only enable the plugins you actually need as having too many enabled could slow down your overall shell experience.&lt;/p&gt;&lt;p&gt;As you can see the prompt looks very different to the standard Bash prompt, incorporating things like the time the command was run, where the command sits in the history, and many additions to make working with git easier. These are all customisable of course by editing your dotfiles.&lt;/p&gt;&lt;h3 id=&quot;aliases&quot;&gt;Aliases&lt;/h3&gt;&lt;p&gt;Now I must admit I don&#39;t have many custom aliases (you will see the reason why later in the blog post). Here are just a couple of examples of how you can use aliases to speed up your workflow:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ipa&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ifconfig | grep -Eo &#39;inet (addr:)?([0-9]*&#92;.){3}[0-9]*&#39; | grep -Eo &#39;([0-9]*&#92;.){3}[0-9]*&#39; | grep -v &#39;127.0.0.1&#39;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This command stands for &quot;IP Address&quot;, and it will give me the current IP address of my machine (or multiple if on multiple networks).&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;vus&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;vagrant up &amp;&amp; vagrant ssh&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I use Vagrant on most of my projects, so I often find myself having to bring up boxes and halt them. This alias just allows you to bring a machine up then SSH into it quickly.&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;opens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;open -a Sublime&#92; Text&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&#39;m primarily a WebStorm user, but whenever I want to quickly edit a file from the shell in a GUI I use the opens command: &lt;code&gt;opens [filename]&lt;/code&gt;. Of course you could also use nano or vim if you wanted to just stick to the terminal.&lt;/p&gt;&lt;h3 id=&quot;fasd&quot;&gt;Fasd&lt;/h3&gt;&lt;p&gt;o here&#39;s one of the reasons why I don&#39;t have many custom aliases. When I first started using the terminal I found I had quite a few aliases related jumping in and out of specific directories around my machine. This is where &lt;a href=&quot;https://github.com/clvv/fasd&quot;&gt;Fasd comes in&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;It is a productivity booster that keeps track of files and directories you have used recently. It even tracks how often you use them and ranks them accordingly. Very handy if you often edit the same set of files. Here are some example commands you could use:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;f scss&lt;/code&gt; list recent files matching the string &quot;scss&quot;&lt;/li&gt;&lt;li&gt;&lt;code&gt;d website&lt;/code&gt; list recent directories matching the string &quot;website&quot;&lt;/li&gt;&lt;li&gt;&lt;code&gt;a script&lt;/code&gt; list files and directories matching the string &quot;script&quot;&lt;/li&gt;&lt;li&gt;&lt;code&gt;z images&lt;/code&gt; cd into the most recently used &quot;images&quot; directory&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you are using it with zsh there is a little bit of additional setup to get it all up and running, but once it is you will wonder how you lived without it.&lt;/p&gt;&lt;h3 id=&quot;scm-breeze&quot;&gt;SCM Breeze&lt;/h3&gt;&lt;p&gt;Here&#39;s the second reason I don&#39;t have many custom aliases: &lt;a href=&quot;https://github.com/ndbroadbent/scm_breeze&quot;&gt;SCM Breeze&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;When it comes to Git, the terminal is my first port of call. I&#39;ve tried GUI&#39;s in the past but have always reverted back to the terminal. Once you get your head around the basic commands it is fairly simple to use, and I also find it helps you understand the inner workings of Git a little more.&lt;/p&gt;&lt;p&gt;Now as much as I love typing, any way to speed up the process is big plus with me. This is where SCM Breeze comes in. It is a set of shell scripts that speed up your interaction with Git massively by adding a whole heap of useful shortcuts. From adding files to the staging area all the way down to branching and rebasing. For example:&lt;/p&gt;&lt;pre class=&quot;language-txt&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;$ gb - show a list of local branches ordered with corresponding number
$ gco 2 - check out branch number 2 from the list
$ gs - look at local staging status, files ordered with corresponding number
$ ga 1-5 - add files 1 to 5 to the staging area
$ &amp;lt;ctrl+x&gt; c - commit the staged files, asks for a commit message
$ gps - push your branch to the remote&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you can see the above workflow is much quicker to type than with the standard git commands. For a full list of aliases available you can use the &lt;code&gt;list_aliases git&lt;/code&gt; command.&lt;/p&gt;&lt;h3 id=&quot;tig&quot;&gt;Tig&lt;/h3&gt;&lt;p&gt;One of the main advantages of using a GUI with Git is the ability to quickly look over all the commits across various branches to see what has been changed and by who. You can prettify the standard &lt;code&gt;git log&lt;/code&gt; command which helps, but even that is still lacking. For a more comprehensive tool you should look at &lt;a href=&quot;http://jonas.nitro.dk/tig/&quot;&gt;Tig&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;TIG in action&quot; decoding=&quot;async&quot; height=&quot;479&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/t-Jd1TrEGV-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/t-Jd1TrEGV-300.webp 300w, https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/t-Jd1TrEGV-600.webp 600w, https://nooshu.com/blog/2016/04/13/reasons-to-be-bashful/t-Jd1TrEGV-814.webp 814w&quot; title=&quot;Tig in action, with detailed information about the commit on the right-hand side.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Tig is a text-mode interface for Git. Its primary usage is as a git repository browser, but it can be used for so much more. I&#39;ve really only scratched the surface of what Tig can do as I mainly use it for a quick glance at commit diffs. I highly recommend you take a look at the &lt;a href=&quot;http://jonas.nitro.dk/tig/manual.html&quot;&gt;user manual&lt;/a&gt; to see what else it can do for you.&lt;/p&gt;&lt;h3 id=&quot;iterm2&quot;&gt;iTerm2&lt;/h3&gt;&lt;p&gt;So the standard terminal that comes with OSX is okay. It does the job, but there&#39;s so much more you can do with it to improve your workflow. The tabs feature is handy, but it is still missing something. This is where &lt;a href=&quot;https://www.iterm2.com/&quot;&gt;iTerm2&lt;/a&gt; blows the standard terminal out of the water. You can see an example of iTerm2 above.&lt;/p&gt;&lt;p&gt;iTerm2 comes with a huge set of features and improvements like autocomplete, search, paste history, and the hotkey window. But by far my favourite feature is the ability to have a set of split panels either horizontally or vertically. Once setup you can have multiple sessions visible at the same time. Great if you need to work on a project as well as SSHing into a remote machine at the same time. Multi-tasking win!&lt;/p&gt;&lt;h2 id=&quot;learning-bash&quot;&gt;Learning Bash&lt;/h2&gt;&lt;p&gt;So if you&#39;ve never used the Bash prompt before, not to worry! There are plenty of resources on the internet to help you learn. Here are just a few that I have used in the past to get you started:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.learnenough.com/command-line-tutorial&quot;&gt;Learn Enough Command Line to Be Dangerous&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://www.bash.academy/&quot;&gt;The Bash Academy&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://ryanstutorials.net/bash-scripting-tutorial/&quot;&gt;Bash Scripting Tutorial&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://linuxcommand.org/tlcl.php&quot;&gt;The Linux Command Line Book - Not Free&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://code.tutsplus.com/courses/how-to-be-a-terminal-pro&quot;&gt;How to Be a Terminal Pro Screencast - Not Free&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://code.tutsplus.com/courses/advanced-command-line-techniques&quot;&gt;Advanced Command Line Techniques - Not Free&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://terminal.training/&quot;&gt;Command Line for Non-Techies - Not Free&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Hopefully you found some of my bashful ramblings useful! &lt;a href=&quot;https://nooshu.com/contact&quot;&gt;Let me know&lt;/a&gt; if there&#39;s some essential terminal related tool or technique I&#39;m missing, I&#39;d love to hear about it.&lt;/p&gt;&lt;p&gt;&lt;small&gt;&lt;sup&gt;†&lt;/sup&gt;Maybe I&#39;m doing it all wrong? Please contact me if there are some secrets or helper tools I&#39;m missing out on, I&#39;d love to give them a try!&lt;/small&gt;&lt;/p&gt;&lt;h2 id=&quot;update&quot;&gt;Update&lt;/h2&gt;&lt;p&gt;I recently stumbled across this &lt;a href=&quot;http://jilles.me/badassify-your-terminal-and-shell/&quot;&gt;blog post&lt;/a&gt; by Jilles Soeters from 2014. Both our lists are almost identical (which is quite reassuring), but there was a small addition that caught my eye. &lt;a href=&quot;https://github.com/zsh-users/zsh-syntax-highlighting&quot;&gt;zsh-syntax-highlighting&lt;/a&gt; is an plug-in for Zsh that I had no idea existed. It highlights commands while you type them, green if they are recognised, red if not. Very useful indeed! I&#39;ve now installed it and it works perfectly, so thanks to Jilles for the tip, and I highly recommend you take a read through his blog post too.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Keyframe animations and media queries</title>
    <link href="https://nooshu.com/blog/2016/03/12/media-query-keyframes/" />
    <updated>2016-03-12T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2016/03/12/media-query-keyframes/</id>
    <content type="html">&lt;p&gt;Recently I re-developed my website (I hope you noticed!?). I&#39;m going to be putting together a couple of short blog posts on what I learned in the process. One interesting issue I stumbled across was related to how IE 9, 10, &amp; 11 handle &lt;code&gt;@keyframe&lt;/code&gt; rule&#39;s inside media queries.&lt;/p&gt;&lt;p&gt;As I was cross-browser testing the site across various browsers I noticed that non of the animations were working in IE. As I&#39;d been using the excellent &lt;a href=&quot;https://github.com/postcss/autoprefixer&quot;&gt;postCSS autoprefixer&lt;/a&gt; in my build process I was quite surprised by this. A quick check of &lt;a href=&quot;http://caniuse.com/#feat=transforms2d&quot;&gt;caniuse.com&lt;/a&gt; confirmed that IE 9-11 supported 2D transforms (although IE9 requires the -ms prefix). So what was happening?&lt;/p&gt;&lt;p&gt;After a little bit of head scratching I finally discovered what the issue was. My &lt;code&gt;@keyframe&lt;/code&gt; animations were sitting inside of a media query definition! I&#39;d included the media query as I only wanted animations to render on tablet / desktop devices. Every other browser rendered the animation as you would expect, it was only IE that had the issue.&lt;/p&gt;&lt;p&gt;Since I was using Sass I thought I&#39;d try wrapping the keyframe rule in an &lt;code&gt;@at-root{}&lt;/code&gt; directive (introduced in Sass 3.3); but it was no help. The directive will only return you to the root of the media query block, not the root of the CSS (as you would expect). Anyway it was a simple fix. Just move the &lt;code&gt;@keyframe&lt;/code&gt; rule outside of the media query block, then all browsers are happy. You can still apply the animation to the devices you want by wrapping the animation property in a media query should you wish.&lt;/p&gt;&lt;p&gt;Take a look at the example below and try it out in various browsers. You will notice the top cloud doesn&#39;t animate in IE 10 - 11, but the bottom one does. You will be able to see why from the CSS tab.&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe src=&quot;https://jsfiddle.net/Nooshu/obn51t8c/6/embedded/css,html,result/&quot; style=&quot;width:100%;height:300px&quot; title=&quot;Embedded content&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;This could be an easy issue to slip into if you are including any animations using mixins, as you don&#39;t always know where they are going to be included, so worth keeping in mind.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Using Wordpress with Foundation Interchange</title>
    <link href="https://nooshu.com/blog/2014/01/14/wordpress-foundation-interchange/" />
    <updated>2014-01-14T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2014/01/14/wordpress-foundation-interchange/</id>
    <content type="html">&lt;p&gt;I recently created a blog / portfolio website for a friend based around &lt;a href=&quot;https://wordpress.org/&quot;&gt;Wordpress&lt;/a&gt; and the fantastic &lt;a href=&quot;http://foundation.zurb.com/&quot;&gt;Foundation&lt;/a&gt; front-end responsive framework. Foundation comes with with a excellent set of components and utilities to help you build a responsive site that works across all devices. One of my favorites is called &lt;a href=&quot;http://foundation.zurb.com/sites/docs/v/5.5.3/components/interchange.html&quot;&gt;Interchange&lt;/a&gt;, and is how Foundation handles responsive images.&lt;/p&gt;&lt;p&gt;The way you handle your images on a responsive build is key to its success. You don&#39;t want huge images loading on a mobile device as it is a waste of bandwidth. On the opposite side of the spectrum you don&#39;t want really small images loading on desktop as they will look very pixelated and ruin the design. Interchange allows you to get the best of both worlds. It even comes with a no JavaScript fallback.&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;data-interchange&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;[/path/to/default.jpg, (default)], [/path/to/medium-image.jpg, (medium)], [/path/to/large-image.jpg, (large)]&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/path/to/default.jpg&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The way it works is fairly self-explanatory. On load the JavaScript looks at the current device width (and pixel density), and swaps out the image source for the correct one listed in the &lt;code&gt;data-interchange&lt;/code&gt; data attribute.&lt;/p&gt;&lt;p&gt;When it come to using Interchange with Wordpress I wanted to make it as simple as possible for the user to upload images. The key was to make Wordpress do the work of creating the different sizes and outputting the required HTML onto the page. To do this I hooked into the native gallery functionality Wordpress comes bundled with. It allows a user to assign a set of media to a specific post, which is then displayed in a gallery on the page.&lt;/p&gt;&lt;p&gt;Below you will see a breakdown of what I added to the theme (mainly to the functions.php file), and a brief explanation of what is going on. &lt;em&gt;Note&lt;/em&gt;: The &lt;code&gt;&amp;lt;?php ?&gt;&lt;/code&gt; tags have been added to fix the syntax highlighting in Jekyll (startinline option didn&#39;t work), you don&#39;t need to include them.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;// functions.php
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;add_theme_support&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// enable thumbnail support for the portfolio post type&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;add_theme_support&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;post-thumbnails&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;portfolio&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// set our required image sizes to be created (name, width, height)&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// note the height is 99999 as we don&#39;t care what it comes out as, only the width is important (aspect ratio will be preserved)&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;add_image_size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;responsive-large&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;99999&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;add_image_size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;responsive-medium&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;99999&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;add_image_size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;responsive-small&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9999&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// not required, but I removed the default gallery to save confusion&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;remove_shortcode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;gallery&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;gallery_shortcode&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// stop default gallery styles from being rendered on use&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;use_default_gallery_style&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// create a new shortcode for our custom gallery&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_shortcode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;custom-gallery&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;custom_gallery&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we have the basics setup for the theme, we want to create a function that renders the correct HTML for use with Foundation Interchange.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;// functions.php
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// generate a single responsive image tag based off a specific image ID&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;generate_responsive_image_tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$image_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// grab the image alt tag&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$alt&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_post_meta&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$image_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;_wp_attachment_image_alt&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// grab the src for each of the image sizes&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;responsive-small&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$small&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wp_get_attachment_image_src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$image_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;responsive-medium&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$med&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wp_get_attachment_image_src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$image_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;responsive-large&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$lrg&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wp_get_attachment_image_src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$image_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// create the final image tag with queries (and noscript fallback)&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&amp;lt;img class=&quot;responsive-image&quot; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;data-interchange=&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;[&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$small&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;, (default)],&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;[&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$med&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;, (medium)],&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;[&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$lrg&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;, (large)],&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&quot; alt=&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$alt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&amp;lt;noscript&gt;&amp;lt;img class=&quot;responsive-image&quot; src=&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$small&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&quot; alt=&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$alt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&quot;&gt;&amp;lt;/noscript&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// return the final HTML&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Time to create the HTML that will be rendered when using the custom gallery shortcode &lt;code&gt;[custom-gallery]&lt;/code&gt;.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;// functions.php
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;custom_gallery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$attr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$post&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;extract&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;shortcode_atts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
		&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;order&#39;&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;ASC&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;orderby&#39;&lt;/span&gt;    &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;menu_order ID&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;id&#39;&lt;/span&gt;         &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$post&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$post&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ID&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;size&#39;&lt;/span&gt;       &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;responsive-small&#39;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$attr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;custom-gallery&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// get the image ID&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;intval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// get the post thumbnail ID&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$post_thumbnail_id&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_post_thumbnail_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// get the attachments in the post&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$attachments&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_children&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;post_parent&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;post_status&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;inherit&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;post_type&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;attachment&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;post_mime_type&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;image&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;order&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;orderby&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$orderby&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// you will need to customise this surrounding HTML depending on the layout required&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$output&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&amp;lt;div class=&#39;row&#39;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// using the Foundation grid for a simple stacked&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$output&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&amp;lt;div class=&#39;small-12 columns small-centered&#39;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// loop through each image ID generating the required HTML&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$attachments&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$attachment&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// skip if we are looking at the post thumbnail image&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$post_thumbnail_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        	&lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Don&#39;t show the thumbnail&lt;/span&gt;
		&lt;span class=&quot;token variable&quot;&gt;$imagehtml&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generate_responsive_image_tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token variable&quot;&gt;$output&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$imagehtml&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// close the tags and return the gallery HTML&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$output&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&amp;lt;/div&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$output&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&amp;lt;/div&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The last thing we need to do is include the gallery in the page. Now we can either do this manually using the &lt;code&gt;[custom-gallery]&lt;/code&gt; shortcode; or in my case I wanted every portfolio post to automatically add the gallery of images associated with it.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;// single-portfolio.php
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;do_shortcode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;[custom-gallery]&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that&#39;s it. On a portfolio page you can now upload a set of images (using the &#39;Add Media&#39; button) and Wordpress will create the different sizes and output the correct HTML for Interchange automatically. Just make sure you are uploading the highest resolution image possible as you don&#39;t want Wordpress to be up-scaling small images.&lt;/p&gt;&lt;p&gt;One last addition I added was the ability to add responsive images in a standard blog post. Again you insert an image into a standard blog post and the responsive Interchange compatible HTML is returned to the editor. Responsive images all over the place!&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;// functions.php
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;interchange_image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$caption&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generate_responsive_image_tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;image_send_to_editor&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;interchange_image&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I hope you found this little bit of Wordpress / Foundation integration helpful. If there are any improvements that can be made feel free to contact me via the contact form.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Site update using Zurb Foundation 4</title>
    <link href="https://nooshu.com/blog/2013/08/28/site-update-using-zurb-foundation-4/" />
    <updated>2013-08-28T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2013/08/28/site-update-using-zurb-foundation-4/</id>
    <content type="html">&lt;p&gt;&lt;img alt=&quot;Zurb Foundation 4&quot; decoding=&quot;async&quot; height=&quot;246&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2013/08/28/site-update-using-zurb-foundation-4/Yq4dpqZnhT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2013/08/28/site-update-using-zurb-foundation-4/Yq4dpqZnhT-300.webp 300w, https://nooshu.com/blog/2013/08/28/site-update-using-zurb-foundation-4/Yq4dpqZnhT-518.webp 518w&quot; title=&quot;Look at the cute little guy, complete with hipster glasses!&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;So last Wednesday I returned from Australia after two years of work and travelling; it&#39;s gone so quick! I guess time flies when you are having fun and building websites! In that time one reoccurring word I&#39;ve come across is &quot;responsive&quot;. When I left the UK responsive design and responsive development was an emerging methodology; but that&#39;s all changed and now everybody wants a responsive website!&lt;/p&gt;&lt;p&gt;The responsive dust has now settled, and, as always happens in development two prominent responsive frameworks have emerged. &lt;a href=&quot;http://getbootstrap.com/&quot;&gt;Bootstrap&lt;/a&gt;, originally developed by two developers at Twitter and &lt;a href=&quot;http://foundation.zurb.com/&quot;&gt;Foundation by Zurb&lt;/a&gt;. Now I&#39;ve had previous experience with Bootstrap (which uses LESS) but only passing experience with Foundation (which uses Sass). I&#39;ve been meaning to make the website responsive for quite a while, so I may as well kill two birds with one stone; learn something new and responsivise (is that a word?) the website, so Foundation looks to be the obvious choice.&lt;/p&gt;&lt;p&gt;Foundation has just reached version 4 and I must say you can really tell. It&#39;s a very mature framework with an excellent set of &lt;a href=&quot;http://foundation.zurb.com/docs/&quot;&gt;documentation&lt;/a&gt;. As a bonus if you need to get up to speed quickly and have a spare bit of cash lying around they even offer &lt;a href=&quot;http://foundation.zurb.com/training.php&quot;&gt;training sessions&lt;/a&gt; once a month where you get to ask the original developers questions. What a fantastic way to learn a new framework!&lt;/p&gt;&lt;p&gt;There are a whole set of pre-built JavaScript add-ons available for developers to use, all responsive by nature and easily customisable if needed. Each is independent of each other so you only need to include the ones you need. From a CSS point of view the framework is built upon &lt;a href=&quot;http://sass-lang.com/&quot;&gt;Sass (Syntactically Awesome Stylesheets)&lt;/a&gt; and it offers a whole heap of cool functionality like variables, mixins and nested rules. Using a command line tool these .scss files compile down to regular CSS. Awesome!&lt;/p&gt;&lt;p&gt;I&#39;ve updated the site today with the basic functionality I need but I have a few issues I still need to resolve:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Responsive images using the &lt;a href=&quot;http://foundation.zurb.com/docs/components/interchange.html&quot;&gt;interchange.js&lt;/a&gt; add-on. The add-on works really well but it&#39;s figuring out how best to generate the different images automatically using WordPress that is the issue.&lt;/li&gt;&lt;li&gt;Adding a &quot;medium&quot; grid layout to target tablet size devices. Version 4.2 has an experimental version that works, but has a few quirks.&lt;/li&gt;&lt;li&gt;Working with Sass is fantastic but I&#39;m sure there are a few optimisation that I can make to my workflow when using it Sublime Text 2. &lt;a href=&quot;https://github.com/alexnj/SublimeOnSaveBuild&quot;&gt;SublimeOnSaveBuild&lt;/a&gt; looks like a handy plug-in but unfortunately it doesn&#39;t seem to be working for me at the moment.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There we have it, converting a standard website to a responsive layout is quick and easy with Zurb Foundation. I&#39;m looking forward to learning lots more about this excellent framework in the future. I may even have to sign up to their online training too!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Marker Cluster Calculator for Google Maps v3</title>
    <link href="https://nooshu.com/blog/2012/10/03/marker-cluster-calculator-for-google-maps-v3/" />
    <updated>2012-10-03T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2012/10/03/marker-cluster-calculator-for-google-maps-v3/</id>
    <content type="html">&lt;p&gt;If you&#39;ve ever worked with the &lt;a href=&quot;https://developers.google.com/maps/documentation/javascript/&quot;&gt;Google Maps API&lt;/a&gt; for a large number of markers I&#39;d say you&#39;ve run into this issue; how do you manage hundreds of markers on the map without killing your browser? Now admittedly over the past few years browsers have &lt;a href=&quot;https://www.google.com/intl/en/chrome/browser/&quot;&gt;improved massively&lt;/a&gt; (thanks to the &quot;browser wars 2.0&quot;), but handling a large number of markers is a sure-fire way to make even modern browsers fall over.&lt;/p&gt;&lt;p&gt;So what&#39;s the answer? Thankfully there are helper libraries that allow you to cluster markers together, so the browser only has to draw one icon, rather than fifty. An &lt;a href=&quot;https://developers.google.com/maps/articles/toomanymarkers&quot;&gt;excellent article&lt;/a&gt; on the subject was written in 2010 by Luke Mahe and Chris Broadfoot of Google. In it they go into great detail on what the problem is, and how to solve it with a sprinkle of JavaScript magic.&lt;/p&gt;&lt;p&gt;For a recent project I required marker clustering, as there were 500+ markers to be rendered on the map. I decided to use the &lt;a href=&quot;https://developers.google.com/maps/articles/toomanymarkers#markerclusterer&quot;&gt;MarkerClusterer library&lt;/a&gt; to do the job. It applies grid-based clustering, each marker is added to its closest cluster group within the set bounds. Everything worked perfectly until the client requested a change to how many cluster groups there were. Luckily there is a setCalculator() method that allows you to modify how clusters are defined and influence the styling, but looking around the net I saw very little explanation of what exactly this calculator function was doing, so I&#39;ve created a &lt;a href=&quot;https://nooshu.com/lab/experiments/marker-cluster/index.html&quot;&gt;small demo&lt;/a&gt; and heavily commented the setCalculator function for future users.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
* Set our own custom marker cluster calculator
* It&#39;s important to remember that this function runs for EACH
* cluster individually.
* @param  {Array} markers Set of markers for this cluster.
* @param {Number} numStyles Number of styles we have to play with (set in mcOptions).
*/&lt;/span&gt;
markerCluster&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setCalculator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;markers&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; numStyles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//create an index for icon styles&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; index &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Count the total number of markers in this cluster&lt;/span&gt;
        count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; markers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Set total to loop through (starts at total number)&lt;/span&gt;
        total &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; count&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/**
     * While we still have markers, divide by a set number and
     * increase the index. Cluster moves up to a new style.
     *
     * The bigger the index, the more markers the cluster contains,
     * so the bigger the cluster.
     */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;total &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Create a new total by dividing by a set number&lt;/span&gt;
        total &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;total &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Increase the index and move up to the next style&lt;/span&gt;
        index&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/**
     * Make sure we always return a valid index. E.g. If we only have
     * 5 styles, but the index is 8, this will make sure we return
     * 5. Returning an index of 8 wouldn&#39;t have a marker style.
     */&lt;/span&gt;
    index &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; numStyles&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Tell MarkerCluster this clusters details (and how to style it)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; count &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot; (&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; index &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; index
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hopefully it&#39;s all self-explanatory as to what the method is doing. Each cluster has its own index, this defines what icon is used. You can modify these icons and add your own, and also change how many cluster levels there will be.&lt;/p&gt;&lt;p&gt;You can view the &lt;a href=&quot;https://nooshu.com/lab/experiments/marker-cluster/index.html&quot;&gt;simple demo here&lt;/a&gt;, here&#39;s a direct link to the map &lt;a href=&quot;https://nooshu.com/lab/experiments/marker-cluster/js/main.js&quot;&gt;JavaScript file&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Auto-spacing input field text</title>
    <link href="https://nooshu.com/blog/2012/03/25/auto-spacing-input-field-text/" />
    <updated>2012-03-25T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2012/03/25/auto-spacing-input-field-text/</id>
    <content type="html">&lt;p&gt;On a recent project, a set of wireframes landed on my desk which contained some &quot;interesting&quot; ideas, particularly when it came to form fields. One that really stood out was having a credit card number auto-space &lt;strong&gt;as the user types&lt;/strong&gt; it into a single input box. This of course would depend completely on what type of card they are entering, as different cards have different layouts. American Express for example have a [4, 6, 5] layout, where as Visa use a [4, 4, 4, 4] number layout.&lt;/p&gt;&lt;p&gt;Thankfully this functionality was eventually removed from the UI specifications after concerns from myself and others were raised. Just to make this perfectly clear; I think this is a &lt;strong&gt;bad idea&lt;/strong&gt;, and here is why:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Users are very wary when it comes entering their credit card details. Spaces magically appearing in their credit card number could freak a few of them out.&lt;/li&gt;&lt;li&gt;Most e-commerce website I&#39;ve ever used ask for the credit card number as one long string of numbers with no spaces. Auto-spacing goes against this convention, so a user may actually go back and try to remove the spaces (I know I would).&lt;/li&gt;&lt;li&gt;Validation scripts for credit card numbers usually ask for the input to have no spaces, or will remove any spaces on submit.&lt;/li&gt;&lt;li&gt;If you do want to space out the credit card number, why not use different input boxes for each set of numbers? It looks cleaner and is simpler to implement. Just have your JavaScript automatically jump between boxes as the user types.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Even though this functionality was removed, I decided to &lt;a href=&quot;https://nooshu.com/lab/experiments/input-auto-space/index.html&quot;&gt;build a prototype&lt;/a&gt; of it in action because... well because I can and I was bored. :) Please, (please!), whatever you do don&#39;t use this code in production. I created it as an experiment and should be treated that way. It&#39;s very simple to get around the auto-spacing and then all hell breaks loose! (not really, but it is easy to break).&lt;/p&gt;&lt;p&gt;The spacing is implemented by counting the length of the string inside the input field, but you could also monitor the current position of the cursor. Anyone wishing to do this see this excellent set of answers on &lt;a href=&quot;http://stackoverflow.com/questions/263743/how-to-get-cursor-position-in-textarea&quot;&gt;Stackoverflow&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;You can see the prototype &lt;a href=&quot;https://nooshu.com/lab/experiments/input-auto-space/index.html&quot;&gt;here&lt;/a&gt;. Remember, don&#39;t use it. It&#39;s not big and it&#39;s not clever :)&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>HTML5 date input type on mobile</title>
    <link href="https://nooshu.com/blog/2012/03/06/html5-date-input-type-on-mobile/" />
    <updated>2012-03-06T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2012/03/06/html5-date-input-type-on-mobile/</id>
    <content type="html">&lt;p&gt;Whilst developing a new mobile only website, the UX team specified in the wireframes that a datepicker was needed for an input field; but nowhere else on the site was a datepicker used, so I really wanted to avoid using a &lt;a href=&quot;http://jqueryui.com/demos/datepicker/&quot;&gt;JavaScript heavy solution&lt;/a&gt;. I couldn&#39;t justify including so much extra JavaScript, CSS and images for so little usage. So what other options do we have?&lt;/p&gt;&lt;p&gt;Well there is a jQuery plug-in called &lt;a href=&quot;http://code.google.com/p/mobiscroll/&quot;&gt;Mobiscroll&lt;/a&gt; which is around 16KB is size and works on iOS and Android devices. Unfortunately there&#39;s no mention of Windows-based mobiles which is a real shame, as it&#39;s almost a perfect solution. The other option we have is to take advantage of some cutting edge HTML5 form input types; the date input type. Support is &lt;a href=&quot;http://www.quirksmode.org/html5/inputs_mobile.html&quot;&gt;nowhere near 100%&lt;/a&gt; yet, but if you approach its usage from a progressive enhancement standpoint, it works well. On mobile browsers that support it, the user gets a fancy datepicker that&#39;s very intuitive; browsers that don&#39;t simply drop back to a standard text input. It&#39;s worth noting that &lt;a href=&quot;http://www.modernizr.com/docs#inputtypes&quot;&gt;Modernizr can&#39;t detect that date inputs create a datepicker&lt;/a&gt;, since Modernizr can&#39;t do it I assume it isn&#39;t possible. Damn!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An example of the date input type on iPhone, iPad and iPods.&quot; decoding=&quot;async&quot; height=&quot;487&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2012/03/06/html5-date-input-type-on-mobile/6CtIC6Nxtu-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2012/03/06/html5-date-input-type-on-mobile/6CtIC6Nxtu-300.webp 300w, https://nooshu.com/blog/2012/03/06/html5-date-input-type-on-mobile/6CtIC6Nxtu-600.webp 600w&quot; title=&quot;The date input type as displayed on iOS mobile Webkit devices.&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;Once I&#39;d decided on how the date picker was going to work (HTML5) it was time to implement it and test it across my available mobile browsers. Implementation was dead simple:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Fancy datepicker on supported devices, standard text input on unsupported --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;fancyDate&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;fancyDate&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where things got a little tricky (and annoying) was the client-side validation of the input. Since the wireframes contained lots of forms with a varying range of input types and requirements, I decided to go with quite a heavyweight &lt;a href=&quot;http://bassistance.de/jquery-plugins/jquery-plugin-validation/&quot;&gt;validation script&lt;/a&gt; (around 21KB minified). This turned out to be a very good decision in the end, as there are a few quirks to the date input element.&lt;/p&gt;&lt;p&gt;First thing I had to consider was how were users going to input the date when they don&#39;t have the fancy datepicker? The most intuitive way (at least for me) is they simply enter in the format dd/mm/yyyy. Great, that&#39;s sorted! And guess what, when a user uses the fancy datepicker it also returns a value of dd/mm/yyyy... no, no it doesn&#39;t! It looks like that&#39;s the value that will be returned as that&#39;s how it is displayed, but no, it&#39;s never that easy. Webkit actually returns the date in the format yyyy-mm-dd, not quite what I was expecting! Unfortunately this breaks our validation as it is looking for a format of dd/mm/yyyy!&lt;/p&gt;&lt;p&gt;So what&#39;s the fix? Well you could take the easy route and make your non-fancy datepicker users enter the date in the yyyy-mm-dd format; but that just feels wrong. No user is going to expect to enter a date like that, it&#39;s completely unintuitive! No I&#39;m afraid it&#39;s going to take a little hack to fix this issue. The hack involves using a second input field of type &quot;text&quot;; when a user interacts with the fancy datepicker or enters in the date manually, the date is &quot;cleaned up&quot; and passed along to the second input field. You then do your form validation against this second field. You can see an example of this working &lt;a href=&quot;https://nooshu.com/lab/experiments/html5-mobile-datepicker/form.html&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * iOS input type=&quot;date&quot; returns a value of yyyy-mm-dd even when
 * it displays dd/mm/yyyy. This breaks validation. Fix for this using a second input.
 * Keyup event for testing in a browser, not needed for mobile.
 */&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;input[type=&#39;date&#39;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;blur keyup&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Does the input have &quot;-&quot;, if so it is from the webkit datepicker, fix it&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; cleanDateArray &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cleanDateArray&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; cleanDateArray&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; cleanDateArray&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Set the hidden value to validate on, trigger the blur and keyup event for validation as you type&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#hiddenDateField&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trigger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;blur&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trigger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;keyup&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A couple of points to note about using this method. Even though we aren&#39;t validating the datepicker input, we still have to set &quot;date: false&quot; in the validation rules. This is because the validation plug-in automatically tries to validate date inputs. Validation fails because it doesn&#39;t like the dd/mm/yyyy format. I&#39;ve also used the dateITA method to validate the date on the &quot;hidden&quot; input (you can find this in the additional-methods.min.js file). This additional method is more robust than the plug-ins standard date validation. A minor pitfall with this hack is that setting the second input to type &quot;hidden&quot; will stop its validation; this is due to the validation plug-in ignoring hidden inputs. So to hide the input you can set &quot;display: none&quot; in the CSS (urghh I know, I feel dirty too!).&lt;/p&gt;&lt;p&gt;So there you have it, that&#39;s how I solved validating a date input on mobile with fancy datepicker and standard text fallback. It&#39;s not ideal by any means, but it works. Another solution would be writing a validator Regex that accepts both dd/mm/yyyy and yyyy-mm-dd date formats, but this means returning both types of dates to the server on submit. Due to server-side constraints with the project, this isn&#39;t an option (boo!). Is there a much simpler method that I&#39;m completely missing? If so please leave a comment! :)&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Sublime Text 2, a truly sublime text editor</title>
    <link href="https://nooshu.com/blog/2012/03/01/sublime-text-2-a-truly-sublime-text-editor/" />
    <updated>2012-03-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2012/03/01/sublime-text-2-a-truly-sublime-text-editor/</id>
    <content type="html">&lt;p&gt;I&#39;m always on a quest to find that one perfect editor, I&#39;ve been hunting for it for &lt;a href=&quot;https://nooshu.com/blog/2010/10/01/goodbye-aptana-hello-komodo-edit/&quot;&gt;many years&lt;/a&gt;. Having been an UltraEdit, Notepad++, Eclipse, Aptana and Komoddo user in the past, they all had their pro&#39;s and cons. Eclipse and Aptana (built on Eclipse) were very feature rich (and very slow!). UltraEdit and Notepad++ were quick but didn&#39;t quite fulfil what I was looking for in an editor (maybe that has changed in newer versions). Komoddo I was very impressed with, but it had a few bugs that just became annoying after a while (like tabbing code blocks). None of them were quite right.&lt;/p&gt;&lt;p&gt;Upon starting my &lt;a href=&quot;https://nooshu.com/blog/2012/02/27/new-country-new-job/&quot;&gt;new job&lt;/a&gt; a couple of months ago, a colleague, &lt;a href=&quot;http://www.antonmills.com/&quot;&gt;Anton Mills&lt;/a&gt;, introduced me to &lt;a href=&quot;http://www.sublimetext.com/2&quot;&gt;Sublime Text 2&lt;/a&gt; and I haven&#39;t opened another editor since!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Sublime Text 2 editor in action with 2 columns.&quot; decoding=&quot;async&quot; height=&quot;436&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2012/03/01/sublime-text-2-a-truly-sublime-text-editor/MoM1i3Vy3g-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2012/03/01/sublime-text-2-a-truly-sublime-text-editor/MoM1i3Vy3g-300.webp 300w, https://nooshu.com/blog/2012/03/01/sublime-text-2-a-truly-sublime-text-editor/MoM1i3Vy3g-600.webp 600w, https://nooshu.com/blog/2012/03/01/sublime-text-2-a-truly-sublime-text-editor/MoM1i3Vy3g-814.webp 814w&quot; title=&quot;Sublime Text 2 editor with its very handy 2 column layout.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There are a few key features I look for in an editor. They are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Speed&lt;/strong&gt;: It needs to be responsive and quick to load. There is nothing more frustrating than an editor that freezes all the time and takes 5 minutes to load.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Regular updates&lt;/strong&gt;: If there is a bug it&#39;s nice to know it will be fixed soon(ish).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Plugins&lt;/strong&gt;: It may not be possible for an editor to have everything you need in the core, so a good plug-in interface is essential.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Community&lt;/strong&gt;: The author may not be on hand to answer all questions so having a helpful community is a plus.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Thankfully, Sublime meets all of these criteria. Development builds are released every week or so and there is a vibrant community on hand to answer any questions. As I&#39;m in a list making mood, here&#39;s a few plug-ins I&#39;d encourage any Front-end Developer install:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://wbond.net/sublime_packages/package_control&quot;&gt;Package Control&lt;/a&gt;&lt;/strong&gt;: Before you do anything, install this plug-in! It makes installing all other plug-ins a breeze.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/Kronuz/SublimeCodeIntel&quot;&gt;SublimeCodeIntel&lt;/a&gt;&lt;/strong&gt;: An excellent code auto-complete plug-in that&#39;s been ported from Komoddo.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://bitbucket.org/sublimator/sublime-2-zencoding&quot;&gt;Zen Coding&lt;/a&gt;&lt;/strong&gt;: If you haven&#39;t heard of Zen Coding take a look at my &lt;a href=&quot;https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/&quot;&gt;previous blog post&lt;/a&gt;. You will wonder how you ever survived without it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://wbond.net/sublime_packages/sftp&quot;&gt;SFTP&lt;/a&gt;&lt;/strong&gt;: Making changes and uploading them directly from the editor can be a godsend at times, but use with caution! Paid plug-in but you get a free trial.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/spadgos/sublime-jsdocs&quot;&gt;DocBlockr&lt;/a&gt;&lt;/strong&gt;: Makes documenting your code quick and easy (supports JavaScript and PHP).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A few of the key features to look out for when you try it are the full screen and distraction free mode, multi columned editor (see image above), multiple cursors (this one I love!), code mini-map and the command palette. Those are just a few I&#39;ve noted but I&#39;m discovering more every day. Checkout this informative &lt;a href=&quot;http://net.tutsplus.com/tutorials/tools-and-tips/sublime-text-2-tips-and-tricks/&quot;&gt;post by Nettuts+&lt;/a&gt; for more awesome features.&lt;/p&gt;&lt;p&gt;Now it&#39;s not perfect, there are a cons. Automatic tag closing isn&#39;t working for me at the moment and the project panel needs a little work (being able to drag and drop files would be nice), but the pros far outweigh the cons, so I&#39;m willing to overlook them. If you have a suggestion or find a bug the place to report them is &lt;a href=&quot;http://sublimetext.userecho.com/&quot;&gt;here&lt;/a&gt;. As you can see there are lots of feature requests already and the author actually implements (some) of them!&lt;/p&gt;&lt;p&gt;Big thank you to the author Jon Skinner, a fellow Sydney resident for developing such an excellent editor! Download it &lt;a href=&quot;http://www.sublimetext.com/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>New country, new job!</title>
    <link href="https://nooshu.com/blog/2012/02/27/new-country-new-job/" />
    <updated>2012-02-27T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2012/02/27/new-country-new-job/</id>
    <content type="html">&lt;p&gt;Boy, it&#39;s been a while since my last blog post! Over the past two months I&#39;ve discovered how hard it can be to get a broadband connection in Australia! I&#39;d been &quot;refused&quot; by three different companies before I was finally connected on Saturday. Lesson to be learned: check which company owns your local exchange; If you don&#39;t you will be refused (but they won&#39;t really tell you why!).&lt;/p&gt;&lt;p&gt;I&#39;ve also started a new job at an excellent Australian digital agency called &lt;a href=&quot;http://www.visualjazz.com.au/&quot;&gt;Visual Jazz Isobar&lt;/a&gt; (I know, it&#39;s a bit long-winded isn&#39;t it). VJ have an excellent set of clients and the agency is producing some outstanding work. We&#39;ve also just become part of the &lt;a href=&quot;http://www.isobar.com/&quot;&gt;Isobar group&lt;/a&gt; which have offices all over the globe. With over 2500+ employees, the group is producing some world-class work. It&#39;s a very exciting time to be at Visual Jazz!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;My Visual Jazz avatar.&quot; decoding=&quot;async&quot; height=&quot;518&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2012/02/27/new-country-new-job/zd_f9sr_o_-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2012/02/27/new-country-new-job/zd_f9sr_o_-300.webp 300w, https://nooshu.com/blog/2012/02/27/new-country-new-job/zd_f9sr_o_-518.webp 518w&quot; title=&quot;My Visual Jazz avatar: that&#39;s me holding Nigel Mansell&#39;s crash helmet!&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Above you can see my custom VJ avatar. Everyone at VJ gets one and, as I&#39;m a huge F1 fan I decided to incorporate it into my design. The crash helmet I&#39;m holding is that of my childhood hero &lt;a href=&quot;http://en.wikipedia.org/wiki/Nigel_Mansell&quot;&gt;Nigel Mansell&lt;/a&gt;; winner of the 1992 F1 Season. I remember being on Club Corner, Silverstone in 1991 as a 10 year old, watching &lt;a href=&quot;http://buriedplanet.com/userfiles/Nigel-mansell-gives-ayrton-senna-a-ride(1).jpg&quot;&gt;Mansell giving Senna a lift&lt;/a&gt; back to the pits as his McLaren had run out of fuel. Ahh the good old days!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Writing efficient CSS selectors</title>
    <link href="https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/" />
    <updated>2011-12-08T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/</id>
    <content type="html">&lt;p&gt;With modern browsers getting quicker with every new version number it&#39;s easy to fall into the trap of writing inefficient code. A page will run super quick on the latest version on Chrome or Firefox, but you also have to consider older browsers and mobile devices. That shiny new web application that uses some super fancy CSS selectors may be unusable on certain devices due to its limited hardware. That&#39;s not to say you shouldn&#39;t be using super fancy selectors; you just have to be careful to consider your target audience, and use them in the most efficient way possible.&lt;/p&gt;&lt;p&gt;The first thing to note about CSS selectors is they don&#39;t work in the way you&#39;d expect. In the west we read a page from left to right. Reading a CSS selector, you&#39;d expect that&#39;s what the browser does as well. Wrong! The browser actually reads a selector from right to left (in Mozilla&#39;s case anyway, and most likely in others too). So take the following CSS as an example:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body #wrapper .article ul.meta li a&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 700&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Arial&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The browser first looks for all the anchor tags (called the &#39;key&#39; as it&#39;s the rightmost selector), then looks at the list items, it evaluates those and throws away the results that don&#39;t match. Next the browser moves onto elements with a class of &#39;meta&#39;, throwing out results that don&#39;t match and so on... you get the idea! There&#39;s so much redundancy in the above selector, it could easily be cut down to:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.meta a&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 700&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Arial&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This example is much more efficient. There are less rules for the browser to evaluate, it&#39;s much easier to read and if you apply minimal selectors across your whole stylesheet you will notice a big difference in file size. The key to writing efficient selectors is to be as specific as possible. Whatever you do don&#39;t write this:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body *&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The universal selector(*) is bad (even body isn&#39;t needed)! You are targeting every single element in the DOM and setting it&#39;s padding and margin to zero. For a large page that could easily be thousands of elements!&lt;/p&gt;&lt;p&gt;I actually inspected the stylesheet for this website and went over it with a fine tooth comb. I found many additional selectors that just weren&#39;t needed. The size of my stylesheet went from 18.3kB down to 16.5kB, a saving of 1.8kB. It doesn&#39;t sound a lot in terms of file size, but that&#39;s a whole lot of selectors the browser no longer has to evaluate to render the page.&lt;/p&gt;&lt;p&gt;Luckily there are tools available that can help you make your CSS more efficient, as well as many other areas of your website too. The first tool I&#39;d recommend is called &lt;a href=&quot;http://code.google.com/speed/page-speed/docs/overview.html&quot;&gt;Page Speed&lt;/a&gt;, created by Google. The Page Speed extension is available on both Chrome and Firefox. Once installed you have the option to run it on any page; it will give you an overall score for that page and recommendations on how to &lt;a href=&quot;http://code.google.com/speed/page-speed/docs/rules_intro.html&quot;&gt;improve it&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Google Page Speed results for Nooshu.com.&quot; decoding=&quot;async&quot; height=&quot;439&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/kfHE1ZUPM2-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/kfHE1ZUPM2-300.webp 300w, https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/kfHE1ZUPM2-600.webp 600w, https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/kfHE1ZUPM2-814.webp 814w&quot; title=&quot;Page Speed results for this site. Note the &#39;Use efficient CSS selectors&#39; drop down.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The second tool I&#39;d recommend is Opera&#39;s &lt;a href=&quot;http://www.opera.com/dragonfly/&quot;&gt;Dragonfly&lt;/a&gt;. Dragonfly (similar to Firebug, in name at least) is Opera&#39;s developer toolkit, much like Web Inspector for Chrome. An awesome feature that Dragonfly has, that other toolkits don&#39;t is &#39;style recalculation&#39;. &lt;a href=&quot;http://my.opera.com/dragonfly/blog/style-profiler-preview&quot;&gt;Style recalculation&lt;/a&gt; gives you a breakdown of all the selectors that were run on the page, how long they took to evaluate and how many elements they hit along the way (hits).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Opera&#39;s Dragonfly debugger in action on nooshu.com.&quot; decoding=&quot;async&quot; height=&quot;439&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/Kh2ZEORF5--300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/Kh2ZEORF5--300.webp 300w, https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/Kh2ZEORF5--600.webp 600w, https://nooshu.com/blog/2011/12/08/writing-efficient-css-selectors/Kh2ZEORF5--814.webp 814w&quot; title=&quot;Dragonfly gives you an extremely useful breakdown of what selectors were used and how long they took to evaluate.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;If you look closely at the results in the image above, you will see that the selectors with the most number of hits are the ones involving the universal selector(*), as you would expect. You may also notice that most of the timings say 0.0ms which isn&#39;t very helpful. This is due to the fact that the size of the DOM being tested is very small, and timing to 1 decimal place isn&#39;t accurate enough to show the actual time it took to evaluate. If you were to run this test on a huge page, say the &lt;a href=&quot;http://dev.w3.org/html5/spec/Overview.html&quot;&gt;HTML5 Specification&lt;/a&gt; for example, you would really be able see the difference in CSS selector efficiency.&lt;/p&gt;&lt;p&gt;This feature of Dragonfly is very new and is still in testing. There are a few issues still to be ironed out in future releases but it&#39;s definitely a tool to keep in your bookmarks.&lt;/p&gt;&lt;p&gt;A word of warning when it comes to writing efficient selectors. I found myself trying to make the selectors so efficient it was becoming quite hard to pin-point where exactly on the site they were being used. If you have a very specific area of a site you are trying to target, it is easier to read if you have the selectors starting with an ID reflecting that area. You then know for certain that the changes you make won&#39;t affect other parts of the site. Also remember that removing &#39;unused&#39; selectors will affect the specificity of the rule. You could end up breaking something, as what you thought was an unused selector was actually used in overriding another rule.&lt;/p&gt;&lt;p&gt;Having too many descendent selectors is something that Page Speed frowns upon, but as with all things in life it&#39;s a case of finding that happy medium. In this case it&#39;s between efficient selectors and CSS that is easy to read and maintain (for you and other developers).&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Design for Developers</title>
    <link href="https://nooshu.com/blog/2011/11/30/design-for-developers/" />
    <updated>2011-11-30T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/11/30/design-for-developers/</id>
    <content type="html">&lt;p&gt;As a developer I must admit I find design hard! Open up a blank PSD in Photoshop and I come out in a cold sweat. More often than not a developer will quickly construct an application, get it working and dump everything on a page. The application works, but it&#39;s horrible to look at and is very unintuitive to use. Thankfully, a designer by the name of &lt;a href=&quot;http://wolfslittlestore.be/&quot;&gt;Johan Ronsse&lt;/a&gt; has put together a presentation aimed at developers who are looking to improve their design skills.&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;485&quot; marginheight=&quot;0&quot; marginwidth=&quot;0&quot; scrolling=&quot;no&quot; src=&quot;https://www.slideshare.net/slideshow/embed_code/key/nZw2IbywCXmf8X&quot; title=&quot;SlideShare presentation&quot; width=&quot;595&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;The presentation has some excellent content on what you should (and shouldn&#39;t) do with an interface design. Basics on fonts, colours, shadows and icons are all explained in a clear and concise way. For those wishing to continue with their new-found design skills, slide number 179 has a long list of links and books to read. My personal favourite is the simple, yet effective [Grid Calculator](http://www.29digital.net/grid/) tool. I&#39;ve read many articles on the grid system, but have never used it. This handy website does all the mundane maths for you, allowing you to get back to making your application look pretty!&lt;p&gt;Big thank you to Johan for the presentation, every developer should read it at least once (or bookmark it to read later)!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Embracing Git for version control</title>
    <link href="https://nooshu.com/blog/2011/11/29/embracing-git-for-version-control/" />
    <updated>2011-11-29T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/11/29/embracing-git-for-version-control/</id>
    <content type="html">&lt;p&gt;For many years I&#39;ve been using Subversion (SVN) as my version control of choice. It&#39;s been a part of my deployment process, all of my work is in Subversion, even the small demos I&#39;ve created are in a repository. It&#39;s a good feeling to know that if your laptop dies (or is stolen), all your work is backed up on a remote server.&lt;/p&gt;&lt;p&gt;Recently I&#39;ve heard many developers raving about &lt;a href=&quot;http://git-scm.com/&quot;&gt;Git&lt;/a&gt;, and I&#39;ve looked at libraries and code snippets that are using it, so I thought I&#39;d check it out and see what all the fuss is about. After a couple of hours reading up and watching a few tutorials I&#39;m genuinely excited about using it in future projects. It really is that good! So, what&#39;s so good about it then you may ask? Well here are a few points that stood out for me:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Easy to install and start using (Windows has a simple installer)&lt;/li&gt;&lt;li&gt;Distributed version control, so no need for a central server&lt;/li&gt;&lt;li&gt;Runs on your local machine, no need to connected to the internet to commit(!)&lt;/li&gt;&lt;li&gt;Very clean, it only creates one git directory for the whole repository (no hidden .svn&#39;s everywhere)&lt;/li&gt;&lt;li&gt;Incredibly easy to branch and merge your code (this is a big plus!)&lt;/li&gt;&lt;li&gt;It&#39;s simple to use Git with SVN, no need to abandon your SVN repositories. Git can pull &amp; push directly into an existing SVN repository!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;d heard developers saying how easy it was to branch and merge your code using Git, I assumed they were exaggerating. But no, it really is simple:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;#create a new branch in my repository&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; branch my_new_branch

&lt;span class=&quot;token comment&quot;&gt;#move to the new branch for commits etc&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout my_new_branch

&lt;span class=&quot;token comment&quot;&gt;#finished with the branch, so lets merge it back into master&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; merge my_new_branch&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;One of the most amazing parts of Git that blew me away: when you jump between branches it automatically updates your file structure accordingly! So lets say you have a new set of files in a new branch, and you need to jump back to master (trunk) to make some changes. Simply run &#39;git checkout master&#39;, the new branch files will be &#39;removed&#39; and stored away until you are back on the new branch where they were added. Amazing!&lt;/p&gt;&lt;p&gt;The feature that really sold Git to me was the stash command. So many times I&#39;ve been working on a project and got half way through some changes, only to have to fix a bug in the original version. So you copy the modified files somewhere, undo all your changes, fix the bug, copy the files over and start where you left off. Not fun! Git and the stash command come to the rescue:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;#store your current changes in a &#39;clipboard&#39; so they can be seen again later&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; stash

&lt;span class=&quot;token comment&quot;&gt;#you are now working on the unmodified version of the branch&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#after you&#39;ve fixed the issue, start from where you left off by applying the stash&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; stash apply&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Another huge advantage Git has is how simple it is to share code between developers. It only takes a couple of commands to clone another repository. Once you have a local copy (clone), you can change whatever you like. Make the project better (or break it horribly, it&#39;s up to you). For an example of how powerful social coding is take a look at &lt;a href=&quot;https://github.com/&quot;&gt;Github&lt;/a&gt;. The 3D JavaScript library &lt;a href=&quot;https://github.com/mrdoob/three.js/&quot;&gt;three.js&lt;/a&gt; for example has over 4000 people watching and 400+ people have forked (cloned) the repository. If your version adds a cool new feature or fixes a bug, it can easily be merged back into the original project!&lt;/p&gt;&lt;p&gt;If you are interested in learning how to use Git there are some superb resources available. For people who like screencasts I highly recommend watching the one created by &lt;a href=&quot;http://peepcode.com/products/git&quot;&gt;Peepcode&lt;/a&gt;. It&#39;s only $9 (US), 1 hour-long and will get you up and running with Git in no time at all! Here are some other resources I found useful:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;http://git.or.cz/course/svn.html&quot;&gt;Git-SVN - A Crash Course&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://nvie.com/posts/a-successful-git-branching-model/&quot;&gt;A successful Git branching model&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://net.tutsplus.com/tutorials/other/easy-version-control-with-git/&quot;&gt;NetTuts - Easy Version Control with Git&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://help.github.com/&quot;&gt;Github help documents&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Right, I&#39;m off to start committing to a repository while travelling on a train to work when I don&#39;t have an internet connection (warm fuzzy feeling enabled)!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Remote Loading HTML5 Elements with jQuery</title>
    <link href="https://nooshu.com/blog/2011/11/18/remote-loading-html5-elements-with-jquery/" />
    <updated>2011-11-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/11/18/remote-loading-html5-elements-with-jquery/</id>
    <content type="html">&lt;p&gt;I ran into a rather annoying problem a few months ago while developing my &lt;a href=&quot;http://solostrightnow.com/&quot;&gt;travel blog&lt;/a&gt;; the problem of course was involving Internet Explorer. I&#39;m used to working with WordPress as I use it all the time, but I wanted to get away from the standard pagination you find on a blog. So I decided to use jQuery 1.6.4 to pull in articles from other pages (blogname.com/page/2, blogname.com/page/3 etc). Now this is all fairly simple using the handy jQuery .load() method; point it to a URL, pull in the page, pick the bits you need and reinsert into the page. Simple! Unfortunately once I got a prototype working in &#39;good&#39; browsers, IE8 and below was having non of it!&lt;/p&gt;&lt;p&gt;After a little head scratching to work out what was failing, I worked out it was because I was using HTML5 elements such as &#39;article&#39;, &#39;header&#39; and &#39;footer&#39;. There was no problem displaying them on the page, as I&#39;d used Remy Sharps excellent &lt;a href=&quot;http://remysharp.com/2009/01/07/html5-enabling-script/&quot;&gt;HTML5 Shiv script&lt;/a&gt;. It only failed when trying to pull in these &#39;new&#39; elements via Ajax.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Before you read on you&#39;ll be happy to hear that this issue no longer occurs in &lt;a href=&quot;http://blog.jquery.com/2011/11/03/jquery-1-7-released/&quot;&gt;jQuery 1.7.0&lt;/a&gt;. Horray!&lt;/p&gt;&lt;p&gt;If you can&#39;t upgrade, for whatever reason, I hashed together a &lt;a href=&quot;https://nooshu.com/lab/experiments/html5-load-method/index.html&quot;&gt;little work-around&lt;/a&gt; for browsers IE8 and below, so read on. I admit the work around isn&#39;t pretty, but it works. I tried for a few hours to get IE8- to recognise the &#39;new&#39; elements after an Ajax request, but to no avail. Eventually I had to wrap the HTML5 tags in a div using conditionals:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if lt IE 9]&gt;&amp;lt;div id=&quot;ieHackContainer&quot;&gt;&amp;lt;![endif]--&gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;article&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;html5Content&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;header&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;H2 wrapped in a header element&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;header&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Other HTML here... --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;footer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;This is the footer element&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;footer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;article&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if lt IE 9]&gt;&amp;lt;/div&gt;&amp;lt;![endif]--&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The div is added for browsers IE8 and below, you can then use it as a hook to pull in the elements inside the div. I was hoping that wrapping the HTML5 elements in a standard div would be the end of it, I&#39;m afraid not. The .load() method still didn&#39;t work, IE just ignored the elements it didn&#39;t recognise. I used jQuerys much more customisable .ajax() method to fix the issue in IE6, 7 and 8:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//IE8- work around to load HTML5 elements into a page&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#loadHTML5LinkOld&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        $&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ajax&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;page_to_load.html&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token literal-property property&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token function function-variable&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;HTML&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;token comment&quot;&gt;//Clear before load&lt;/span&gt;
                        &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#html5LoadHolder&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//IE: Look for the hacky wrapper before insertion&lt;/span&gt;
                        &lt;span class=&quot;token constant&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;div#ieHackContainer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//If HTML var is empty assume using newer browsers&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;HTML&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                                &lt;span class=&quot;token constant&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#html5Content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Append to page&lt;/span&gt;
                        &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#html5LoadHolder&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;HTML&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This method works in all browsers I&#39;ve tested in as I&#39;ve added a fall back (or should that be forward?) for newer browsers. The solution isn&#39;t ideal (I really hate the conditional comments), but older versions of IE aren&#39;t going away any time soon, and it works. Maybe there&#39;s a more obvious solution that I missed? Leave a comment if there is, I&#39;d love to know!&lt;/p&gt;&lt;p&gt;You can see a working example &lt;a href=&quot;https://nooshu.com/lab/experiments/html5-load-method/index.html&quot;&gt;here&lt;/a&gt;. The page I&#39;m loading from is &lt;a href=&quot;https://nooshu.com/lab/experiments/html5-load-method/page_to_load.html&quot;&gt;here&lt;/a&gt; and the JavaScript in full &lt;a href=&quot;https://nooshu.com/lab/experiments/html5-load-method/js/html5test.js&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Back to my desk (down under)</title>
    <link href="https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/" />
    <updated>2011-11-13T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/</id>
    <content type="html">&lt;p&gt;Well that was a quick three months! &lt;a href=&quot;http://solostrightnow.com/&quot;&gt;Travelling around south-east Asia&lt;/a&gt; was incredible! So many memories and stories to take away from the experience. Unfortunately all good things have to come to an end, so back to work it is. Actually I&#39;ve been looking forward to getting back to work for a couple of weeks, itching to start using the latest technologies the web has to offer.&lt;/p&gt;&lt;p&gt;A lot can change on the web in three months; so in order to get my head around what&#39;s new, I&#39;ve compiled a list of exciting developments and changes in the Web Developer community (in no particular order).&lt;/p&gt;&lt;h3 id=&quot;browsers&quot;&gt;Browsers&lt;/h3&gt;&lt;p&gt;Thankfully the browser war has started again (this time for the better) and it certainly hasn&#39;t slowed down in the last three months!&lt;/p&gt;&lt;p&gt;Some amazing news first; Has Internet Explorers browser market share finally &lt;a href=&quot;http://mashable.com/2010/10/05/ie-50-percent-market-share/&quot;&gt;dropped below 50%&lt;/a&gt;? Well according to Statcounter it has (oh please let it be true!). As with all statistics you have to be careful how you interpret the data. A more important statistic would be how that 50% is broken down. How many users are still using IE6? If corporations are still sticking with IE6 due to internal tools and upgrade cost, that value won&#39;t be changing any time soon (boo!).&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://en.wikipedia.org/wiki/WebGL&quot;&gt;WebGL&lt;/a&gt; is the big thing on the web at the moment. Incredible graphics rendered directly in the browser (no Flash required!). Of course Microsoft being Microsoft, they aren&#39;t going to support it. To be fair they do give a good excuse; the specification isn&#39;t 100% set, so they aren&#39;t going to implement it (but when has that ever stopped them before?). Luckily for developers a small team in Russia have decided to add it themselves by creating a plug-in for IE (for non-commercial use) called &lt;a href=&quot;http://iewebgl.com/&quot;&gt;IEWebGL&lt;/a&gt;. It&#39;s not ideal but it&#39;s better than nothing!&lt;/p&gt;&lt;p&gt;There&#39;s been big changes at Firefox over the past three months. The Firefox team have now adopted a six week release schedule just like Chrome. So while I&#39;ve been away we&#39;ve had version 6,7 and &lt;a href=&quot;http://www.getfirefox.net/&quot;&gt;8&lt;/a&gt;. There&#39;s even talk of Aurora 10 (a.k.a. Firefox 10) on the horizon. Mozilla also seem to have adopted the &#39;channel&#39; route for deployment, where by you can join the stable, beta or nightly channel depending on how brave you are. You can receive a new version of Firefox every few days (or sometimes a broken version) if you so wish. I&#39;m hoping they also adopt the &#39;delta&#39; update strategy that Chrome uses; no need to download the whole installer every time, only the parts that have changed. On a side note, Firefox now gets 100% on the &lt;a href=&quot;http://www.acid3.org/&quot;&gt;Acid3 test&lt;/a&gt;, so well done to Mozilla for achieving that (if that type of browser comparison floats your boat).&lt;/p&gt;&lt;p&gt;I love this little addition; in the latest nightly versions of Firefox, Mozilla has added support for the &lt;a href=&quot;https://wiki.mozilla.org/JoystickAPI&quot;&gt;draft JoystickAPI&lt;/a&gt;. What an incredibly simple idea, it never even occurred to me! The API allows a browser to communicate directly with a joystick / gamepad, meaning you can control that snazzy HTML5 game you&#39;ve written just like you would on a games console. There&#39;s a breakdown of what is supported on the API page including handy code examples. All you need is a joystick that&#39;s supported by your PC or Mac. It looks like a standard Xbox360 controller will work, and they&#39;re fairly cheap to buy. I May just have to add that to my Christmas list this year and give it a whirl!&lt;/p&gt;&lt;p&gt;With the IE browser market share (apparently) going below 50%, it looks like Chrome could be set to take &lt;a href=&quot;http://news.slashdot.org/story/11/09/29/1945201/chrome-set-to-take-no-2-spot-from-firefox&quot;&gt;number 2 spot from Firefox&lt;/a&gt; very soon. I must admit, I&#39;m not too bothered about who&#39;s in second place; as long as all browser vendors keep improving their products I&#39;m a very happy developer. More competition equals better browsers for all. When one of the &#39;good&#39; browsers gets to number one, then I will celebrate.&lt;/p&gt;&lt;p&gt;At the start of October Google unveiled their new &lt;a href=&quot;http://googlesystem.blogspot.com/2011/10/dart-googles-programming-language-for.html&quot;&gt;Dart programming language&lt;/a&gt; which is supposed to replace JavaScript as the working language on open web platforms. Dart will compile to ECMAScript 3 on the fly for non-Dart compatible browsers. There&#39;s even a &lt;a href=&quot;http://www.dartlang.org/docs/getting-started/index.html&quot;&gt;simple IDE&lt;/a&gt; so you can start playing around with a bit of Dart right now. Will it catch on? Who knows, only time will tell.&lt;/p&gt;&lt;p&gt;Last but not least the browser that everyone forgets about (but it&#39;s actually an excellent browser), Opera. At the start of October they &lt;a href=&quot;http://my.opera.com/desktopteam/blog/2011/10/13/introducing-opera-12-alpha&quot;&gt;released Opera 12 alpha&lt;/a&gt;, which finally supports WebGL! Great news for Opera and WebGL. As it looks like WebGL is here to stay, maybe it&#39;s time the IE team reconsidered its position? Let&#39;s hope they do.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome is about to replace Firefox at the number 2 spot.&quot; decoding=&quot;async&quot; height=&quot;476&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/dfjwGtQ6d_-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/dfjwGtQ6d_-300.webp 300w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/dfjwGtQ6d_-600.webp 600w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/dfjwGtQ6d_-814.webp 814w&quot; title=&quot;Will Chrome overtake Firefox for browser market share in November?&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;javascript&quot;&gt;JavaScript&lt;/h3&gt;&lt;p&gt;The big event (in Europe at least) that happened while I was away was &lt;a href=&quot;http://jsconf.eu/2011/&quot;&gt;JSConf.eu&lt;/a&gt;, a 2 day conference dedicated to the JavaScript programming language. This year it was held in Berlin, Germany, somewhere I&#39;ve never been (but have heard many great things about). I wish I could have been there, a couple of the talks I&#39;d loved to have seen include “&lt;a href=&quot;http://jsconf.eu/2011/magic_wand_for_surface_generat.html&quot;&gt;Magic Wand for surface generation – Voxels with JS&lt;/a&gt;” and “&lt;a href=&quot;http://jsconf.eu/2011/connecting_the_real_world_to_n.html&quot;&gt;Connecting the real world to node&lt;/a&gt;“. Luckily for those of us who couldn&#39;t be there videos of most of the talks are available &lt;a href=&quot;http://blip.tv/jsconfeu&quot;&gt;online&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Talking of &lt;a href=&quot;http://nodejs.org&quot;&gt;Node&lt;/a&gt;, the development team has been bug fixing and adding lots of lovely new features to the code base, and it is now up to &lt;a href=&quot;https://github.com/joyent/node/wiki/ChangeLog&quot;&gt;version 0.6.0&lt;/a&gt;. A point that really caught my attention was the work regarding Windows support. Being a Windows user myself it&#39;s always nice to know the Windows platform is being considered in future development. Looking at the performance statistics in the &lt;a href=&quot;http://blog.nodejs.org/2011/11/05/node-v0-6-0/&quot;&gt;0.6.0 blog post&lt;/a&gt;, the team have made huge improvements by supporting native APIs (rather than through Cygwin).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: As I was writing this blog post &lt;a href=&quot;http://blog.nodejs.org/2011/11/11/node-v0-6-1/&quot;&gt;Node 0.6.1 was released&lt;/a&gt; and it now comes with a Windows installer (MSI).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;JSConf.eu took place in Berlin in 2011&quot; decoding=&quot;async&quot; height=&quot;230&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/kITCGkuByF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/kITCGkuByF-300.webp 300w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/kITCGkuByF-600.webp 600w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/kITCGkuByF-814.webp 814w&quot; title=&quot;Lots of lovely JavaScript geeking out at this years JSConf.eu.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;jquery&quot;&gt;jQuery&lt;/h3&gt;&lt;p&gt;For users of jQuery (myself included) it&#39;s been a very busy three months. There have been three new versions released (not including betas and RC&#39;s), &lt;a href=&quot;http://blog.jquery.com/2011/09/01/jquery-1-6-3-released/&quot;&gt;1.6.3&lt;/a&gt;, &lt;a href=&quot;http://blog.jquery.com/2011/09/12/jquery-1-6-4-released/&quot;&gt;1.6.4&lt;/a&gt; and &lt;a href=&quot;http://blog.jquery.com/2011/11/03/jquery-1-7-released/&quot;&gt;1.7&lt;/a&gt;. Here are some of the key features that caught my eye in each version:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;1.6.3&lt;/strong&gt;: requestAnimationFrame API has been removed for animations due to strange goings on when animated tabs are hidden from view. The team plans to re-implement it in a later version.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;1.6.4&lt;/strong&gt;: Minor bug fix release.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;1.7&lt;/strong&gt;: Big changes in the way events are bound (and unbound) to elements. There&#39;s a whole new .on(), .off() event API which aims to unify all the ways of attaching events in jQuery. Also, as mentioned on the jQuery blog they are shorter to type!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For version 1.8 the jQuery team is planning on &lt;a href=&quot;http://blog.jquery.com/2011/11/08/building-a-slimmer-jquery/&quot;&gt;slimming down the library&lt;/a&gt; to reduce it&#39;s overall gzipped file size. They are asking for feedback from the community as to what should be removed and offloaded into a separate plug-in, or maybe even removed all together. There are now &lt;a href=&quot;http://api.jquery.com/category/version/1.7/&quot;&gt;so many methods available&lt;/a&gt; to a developer, a lot of which are probably never used; I can see why they are looking to slim it down. Maintaining rarely used code is never fun.&lt;/p&gt;&lt;p&gt;I personally would like to see the animations offloaded to a separate plug-in, as for most projects I never use them. I&#39;d happily add the animation plug-in back in if and when needed. Maybe this could be the start of a more modular version of jQuery; by that I mean something along the lines of the &lt;a href=&quot;http://mootools.net/core/&quot;&gt;MooTools core builder&lt;/a&gt;. Creating a custom jQuery build, with only the parts you need really would be a great option. I&#39;m sure lots of other jQuery developers would be against it, but each to their own.&lt;/p&gt;&lt;p&gt;Last in the jQuery news is the announcement that there will be a &lt;a href=&quot;http://blog.jquery.com/2011/11/01/jquery-conference-2012-united-kingdom-announced/&quot;&gt;jQuery conference in Oxford&lt;/a&gt;, UK in 2012. It&#39;s the first jQuery conference in the UK, and of course I&#39;m now in Australia. Typical! Oh well, maybe next time.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;jQuery confrence in the UK in 2012.&quot; decoding=&quot;async&quot; height=&quot;327&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/Fzh5teg59p-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/Fzh5teg59p-300.webp 300w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/Fzh5teg59p-600.webp 600w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/Fzh5teg59p-814.webp 814w&quot; title=&quot;The first jQuery conference in the UK while I&#39;m in Australia. Damn!&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;three-js&quot;&gt;Three.js&lt;/h3&gt;&lt;p&gt;The three.js community has been busy beavering away with lots of &lt;a href=&quot;http://www.reddit.com/r/three.js/&quot;&gt;cool new demos&lt;/a&gt;. It&#39;s incredible how this superb JavaScript library has taken off since mr.doob unleashed it on the web.&lt;/p&gt;&lt;p&gt;I&#39;ve noticed from the &lt;a href=&quot;https://github.com/mrdoob/three.js/&quot;&gt;Git repository for three.js&lt;/a&gt; that the number of updates has really slowed down. Maybe there are things in the pipe line that have yet to be rolled into the Git repository; or maybe it&#39;s a sign of the three.js API stabilising.&lt;/p&gt;&lt;p&gt;One of the major problems I found while working on a few personal projects was the lack of documentation. It really was a case of diving into the library and examples and having a play (fun, but not ideal). Stability will allow the documentation to catch up with the current release, and in turn allow the wider community to develop lots more interesting 3D demos.&lt;/p&gt;&lt;p&gt;Speaking of interesting demos, here&#39;s one that&#39;s been released by HelloEnjoy called &lt;a href=&quot;http://lights.elliegoulding.com/&quot;&gt;Lights – An interactive music experience&lt;/a&gt;. I can&#39;t say I&#39;ve heard of Ellie Goulding (I know, I&#39;m old), but it&#39;s a catchy tune and kudos to her record company for pushing the music video envelope. Is interactivity the future of music videos? I hope so!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Lights by Ellie Goulding, lovely interactive music video&quot; decoding=&quot;async&quot; height=&quot;388&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/FFtm8GZ1co-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/FFtm8GZ1co-300.webp 300w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/FFtm8GZ1co-600.webp 600w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/FFtm8GZ1co-814.webp 814w&quot; title=&quot;Music video using the power of three.js.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;demos&quot;&gt;Demos&lt;/h3&gt;&lt;p&gt;One thing I really missed while I was away was how all this new technology is being applied on the web. It&#39;s always interesting to see how other developers apply technology to problems they encounter. Creating demos is a fun way to learn something new.&lt;/p&gt;&lt;p&gt;First on the list is a bit of &lt;a href=&quot;http://wesbos.com/html5-video-face-detection-canvas-javascript/&quot;&gt;face detection using HTML5 and JavaScript&lt;/a&gt;. This technique is used in various webcam applications, where you can add things like glasses and masks to your face in real-time. Hours of fun for kids (and developers too). The demo makes use of the &lt;a href=&quot;https://github.com/liuliu/ccv&quot;&gt;CCV JavaScript Face Detection library&lt;/a&gt; and HTML5 canvas element. Awesome stuff by &lt;a href=&quot;http://twitter.com/wesbos&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@wesbos&lt;/a&gt;, and as a bonus there&#39;s a whole blog post with commented code on how he did it.&lt;/p&gt;&lt;p&gt;What do you get when you combine &lt;a href=&quot;http://googleblog.blogspot.com/2011/10/step-inside-map-with-google-mapsgl.html&quot;&gt;Google maps and WebGL&lt;/a&gt;? That&#39;s right, you get MapsGL! Google has enabled the option to view its maps complete with 3D buildings, in the browser, no plug-ins required! Superb news for WebGL as there is no bigger name you want behind you to push the technology into the mainstream. It&#39;s still a little rough around the edges but it looks very promising! You can enable and view a demo &lt;a href=&quot;http://maps.google.com/mapsgl&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Mozilla developer Michael Bebenita has created and released a JavaScript-based H.264 decoder that will run natively in the browser… wow! Nicknamed &lt;a href=&quot;https://github.com/mbebenita/Broadway&quot;&gt;Broadway&lt;/a&gt;, it is based on the open-source decoder that Google uses in the Android OS. Yet another example of how powerful the JavaScript programming language is. To test it you will need a nightly version of Firefox, or you can always &lt;a href=&quot;http://yfrog.com/nmng0z&quot;&gt;view a video&lt;/a&gt; from Brendan Eich&#39;s talk at ACM&#39;s annual OOPSLA conference, where he shows it in action. Amazing!&lt;/p&gt;&lt;p&gt;Do you want to help a computer beat a Grandmaster at chess using JavaScript? Well now you can thanks to the &lt;a href=&quot;http://chessathome.org/&quot;&gt;Chess@home project&lt;/a&gt;. Developers at &lt;a href=&quot;http://joshfire.com/&quot;&gt;Joshfire&lt;/a&gt; created a project prototype at this years 48 hour Node Knockout contest. It utilises lots of cutting edge technologies. Starting with the Web Workers API in the browser, to Socket.io for communication and a Node.js – MongoDB combination for storing the results. It works by using the spare CPU cycles of browsing users to calculate the best next move. By adding a small Chess@home widget to your website you can help the project defeat a Grandmaster. By making use of the Web Worker API, visitors to your website won&#39;t even notice the difference (users can disable the widget via a check box if they so wish).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Combining Google Maps and WebGL&quot; decoding=&quot;async&quot; height=&quot;244&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/4MpMxA-di1-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/4MpMxA-di1-300.webp 300w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/4MpMxA-di1-600.webp 600w, https://nooshu.com/blog/2011/11/13/back-to-my-desk-down-under/4MpMxA-di1-814.webp 814w&quot; title=&quot;Google maps goes 3D with MapsGL - Virtual Westminster Abbey&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see the Web Development community has been busy over the past few months! Hopefully I picked out most of the important changes. If you think there&#39;s anything I&#39;ve missed that&#39;s a worthy addition, please leave a comment, I&#39;d love to hear about it.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Hanging up my Web Dev boots (for three months)</title>
    <link href="https://nooshu.com/blog/2011/08/06/hanging-up-my-web-dev-boots-for-three-months/" />
    <updated>2011-08-06T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/08/06/hanging-up-my-web-dev-boots-for-three-months/</id>
    <content type="html">&lt;p&gt;As of tomorrow evening I will no longer be a resident of this great land called England as I&#39;m setting off to travel the globe for approximately eighteen months (in total). I&#39;ll be spending three months in south-east Asia; as well as having a grandstand seat at the Singapore Grand Prix (yay!).&lt;/p&gt;&lt;p&gt;I land in Sydney, Australia early November and will be looking for Freelance work (for 12 months) when I land. So if there are and Australian agencies looking for an Interface Developer &lt;a href=&quot;https://nooshu.com/contact/&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I&#39;ll be travelling with my wonderful &lt;a href=&quot;http://spacebarspeeds.com/&quot;&gt;girlfriend&lt;/a&gt; (who will also be Freelancing in Oz). If you&#39;re interested you can follow our progress on our &lt;a href=&quot;http://solostrightnow.com/&quot;&gt;travel blog&lt;/a&gt;. We are also on &lt;a href=&quot;http://twitter.com/StillSoLost&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;twitter&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Matt and Claire&#39;s travel blog&quot; decoding=&quot;async&quot; height=&quot;455&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/08/06/hanging-up-my-web-dev-boots-for-three-months/CG3TGJki5y-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/08/06/hanging-up-my-web-dev-boots-for-three-months/CG3TGJki5y-300.webp 300w, https://nooshu.com/blog/2011/08/06/hanging-up-my-web-dev-boots-for-three-months/CG3TGJki5y-518.webp 518w&quot; title=&quot;Our travel blog updated as often as possible.&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;BBQ on the beach here we come!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>There ain&#39;t no party like a DemoJS party</title>
    <link href="https://nooshu.com/blog/2011/07/06/there-aint-no-party-like-a-demojs-party/" />
    <updated>2011-07-06T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/07/06/there-aint-no-party-like-a-demojs-party/</id>
    <content type="html">&lt;p&gt;Another day, another website that makes my jaw drop to my desk. This time it&#39;s the results from the latest &lt;a href=&quot;http://demojs.org/&quot; title=&quot;JavaScript fun!&quot;&gt;Mozilla Labs DemoJS&lt;/a&gt; party. Hosted in Paris, France over the weekend, it had many coders and artists attending hoping to win big prizes by creating interesting demo&#39;s using the latest web technologies. I may not understand French but I&#39;d have still attended just to see all the demos on show; some extremely inspiring coding going on.&lt;/p&gt;&lt;p&gt;The competition was split into two sections; 1k and freestyle. I find both interesting but as I&#39;ve mentioned before in &lt;a href=&quot;https://nooshu.com/lab/2010-08-04-1kjs-smaller-is-better/&quot;&gt;previous blog posts&lt;/a&gt; I&#39;m always amazed at what can be done with 1k of code.&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/B24bLKWIy-o&quot; title=&quot;YouTube video&quot; width=&quot;420&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;The video above is of a demo created by &lt;a href=&quot;http://www.silexars.com/&quot;&gt;Silexars&lt;/a&gt; that came in second place. A live demo can be viewed &lt;a href=&quot;http://demojs.org/2011/releases/1K/online/Silexars_-_Creation&quot;&gt;here&lt;/a&gt;. All created with less than 1k of code! WebGL really is starting to come of age. As with any of these demo&#39;s it&#39;s so easy to copy / paste, modify and learn from them. View source is a wonderful tool.&lt;/p&gt;&lt;p&gt;Another demo that caught my eye was by &lt;a href=&quot;http://www.p01.org&quot;&gt;p01&lt;/a&gt; (who I&#39;ve blogged about &lt;a href=&quot;https://nooshu.com/blog/2010/03/30/p01-org-amazing-experiments-using-javascript/&quot;&gt;before&lt;/a&gt;. This time he created &lt;a href=&quot;http://www.p01.org/releases/gl1k_cotton_candy/&quot;&gt;Quaternion Julia Fractal&lt;/a&gt; in only 550 bytes of JavaScript and GLSL! It came in third place.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Quaternion Julia Fractal by p01&quot; decoding=&quot;async&quot; height=&quot;462&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/07/06/there-aint-no-party-like-a-demojs-party/y4YEMwg4Iw-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/07/06/there-aint-no-party-like-a-demojs-party/y4YEMwg4Iw-300.webp 300w, https://nooshu.com/blog/2011/07/06/there-aint-no-party-like-a-demojs-party/y4YEMwg4Iw-600.webp 600w, https://nooshu.com/blog/2011/07/06/there-aint-no-party-like-a-demojs-party/y4YEMwg4Iw-814.webp 814w&quot; title=&quot;All this possible in less that 1k of code.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;From the freestyle section the &lt;a href=&quot;http://demojs.org/2011/releases/Freestyle/online/basecode_-_Particles.js&quot;&gt;Particles.js&lt;/a&gt; by basecode has completely killed my productivity today!&lt;/p&gt;&lt;p&gt;Nice work guys and gals! If you have a spare few minutes (hours) check out the rest of the &lt;a href=&quot;http://demojs.org/&quot; title=&quot;JavaScript fun!&quot;&gt;DemoJS&lt;/a&gt; results, you won&#39;t be disappointed.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress 3.2, auto update fail</title>
    <link href="https://nooshu.com/blog/2011/07/05/wordpress-3-2-auto-update-fail/" />
    <updated>2011-07-05T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/07/05/wordpress-3-2-auto-update-fail/</id>
    <content type="html">&lt;p&gt;Today, while on my lunch hour, I decided to auto update my blog to WordPress 3.2. Usually it goes without a hitch; unfortunately today it didn&#39;t. I&#39;m not quite sure what happened but it failed within a couple of seconds. Damn it! It may be a &lt;a href=&quot;http://www.dreamhost.com/&quot; title=&quot;Dreamhost hosting&quot;&gt;Dreamhost&lt;/a&gt; issue as I&#39;ve seen reports of other users having similar issues, or it could be just bad luck.&lt;/p&gt;&lt;p&gt;If you have a similar issue the first thing you may notice, when trying to get to your site is that maintenance mode is &quot;enabled&quot;. To disable this connect to hosting server via FTP (or SSH) and delete the file &#39;.maintenance&#39;. The file may be hidden so you may have to enable viewing hidden files in your FTP client. Once deleted you should once again be able to see your site (fingers crossed!).&lt;/p&gt;&lt;p&gt;The next issue I had was not being able to log into my site admin panel. Navigating to the page gave me a horrible fatal PHP error; a sure sign that something was very broken. Not to worry there&#39;s a (fairly) simple fix.&lt;/p&gt;&lt;p&gt;While logged onto your server via FTP or SSH, delete the &#39;wp-admin&#39; and the &#39;wp-includes&#39; folder. Make sure you &lt;strong&gt;don&#39;t&lt;/strong&gt; delete the &#39;wp-content&#39; folder! Once that&#39;s done &lt;a href=&quot;http://wordpress.org/download/&quot; title=&quot;Wordpress 3.2 download&quot;&gt;download a copy&lt;/a&gt; of WordPress 3.2, unzip it and upload the unzipped &#39;wp-admin&#39; and the &#39;wp-includes&#39; folders to your server. Once complete you should be able to see your WordPress login page again! Yay!&lt;/p&gt;&lt;p&gt;You may be asked to update your database, go ahead and click the update button and login. Now take in all the goodness that is version 3.2 of WordPress! The guys have done a superb job with the admin panel, love the new design. Glad to hear they have also &lt;a href=&quot;http://en.blog.wordpress.com/2011/05/19/just-write/&quot; title=&quot;IE6 is no more!&quot;&gt;dropped IE6 support&lt;/a&gt; too. About bloody time!&lt;/p&gt;&lt;p&gt;So a quick summary for the tr;dr&#39;s out there:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Maintenance mode on? Delete &#39;.maintenance&#39;.&lt;/li&gt;&lt;li&gt;Delete &#39;wp-admin&#39; and the &#39;wp-includes&#39; folders (&lt;strong&gt;Not &#39;wp-content&#39;!&lt;/strong&gt;)&lt;/li&gt;&lt;li&gt;Download a copy of WordPress and unzip it.&lt;/li&gt;&lt;li&gt;Upload the unzipped &#39;wp-admin&#39; and the &#39;wp-includes&#39; folders.&lt;/li&gt;&lt;li&gt;Navigate to the admin page, update database and login.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Extra&lt;/strong&gt;: In some cases you may also need to upload the wp-settings.php file then navigate to the admin page.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Phew, crisis averted! Now&#39;s a good time to reiterate what is mentioned on the updates page. Always remember to back-up your database and files before you run an update. If something does break with the auto update you can always revert to your backup!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Aloha Editor and WordPress</title>
    <link href="https://nooshu.com/blog/2011/06/09/aloha-editor-and-wordpress/" />
    <updated>2011-06-09T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/06/09/aloha-editor-and-wordpress/</id>
    <content type="html">&lt;p&gt;This is a short update on a post I wrote back in August 2010: &lt;a href=&quot;https://nooshu.com/blog/2010/08/17/aloha-editor-content-editing-the-html5-way/&quot;&gt;Aloha Editor, content editing the HTML5 way&lt;/a&gt;. When I last looked at the project there were some excellent examples of it working on static pages but no real integration into back-end systems had taken place. As with any open-source project things progress quickly; on the 5th June a plug-in called &lt;a href=&quot;http://wordpress.org/extend/plugins/front-end-editor/&quot;&gt;Front-end Editor&lt;/a&gt; for WordPress was updated to include Aloha Editor! You can now edit blog posts directly in the page rather than through the administration panel. The plug-in is a joint effort from authors Jotschi and Scribu, excellent work guys.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Aloha Editor in WordPress using Front-end Editor&quot; decoding=&quot;async&quot; height=&quot;331&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/06/09/aloha-editor-and-wordpress/DzpJSYvNSA-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/06/09/aloha-editor-and-wordpress/DzpJSYvNSA-300.webp 300w, https://nooshu.com/blog/2011/06/09/aloha-editor-and-wordpress/DzpJSYvNSA-518.webp 518w&quot; title=&quot;Edit a post directly in the page, the future is here.&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Now I&#39;d love to show you a picture of it working in my blog template, but that would be too easy wouldn&#39;t it. :) Unfortunately the plug-in doesn&#39;t work with my custom theme, I&#39;m yet to figure out the reason why. Boo! I know it isn&#39;t a plug-in issue as it works with the &lt;a href=&quot;http://2010dev.wordpress.com/&quot;&gt;default WordPress theme&lt;/a&gt;. This could be an issue for others wanting to use the plug-in, so as soon as I figure out why I&#39;ll post an update.&lt;/p&gt;&lt;p&gt;One issue I did notice was Aloha likes to use its own version of jQuery; manually including it in your theme will cause Aloha to throw an error. A quick workaround is to wrap a &lt;a href=&quot;http://codex.wordpress.org/Conditional_Tags&quot;&gt;WordPress conditional tag&lt;/a&gt; around the jQuery script element like so:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//If you aren&#39;t logged in, include jQuery&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;is_user_logged_in&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Insert jQuery script tag here&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since you need to be logged in for Aloha to be initialised this will fix the error and allow you to use the latest version of jQuery in your theme.&lt;/p&gt;&lt;p&gt;As soon as I fix Front-End Editor for my current theme I&#39;m sure it will be one of my favourite plug-ins. Last but not least a big thank you to the Aloha Editor team, I&#39;m looking forward to seeing how the project progresses.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>A little bit of MicroJS</title>
    <link href="https://nooshu.com/blog/2011/05/29/a-little-bit-of-microjs/" />
    <updated>2011-05-29T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/05/29/a-little-bit-of-microjs/</id>
    <content type="html">&lt;p&gt;Recently there was a flurry of talk around a little something called MicroJS, mainly thanks to a website of the same name created by &lt;a href=&quot;http://mir.aculo.us/&quot;&gt;Thomas Fuchs&lt;/a&gt;, author of the &lt;a href=&quot;http://script.aculo.us/&quot;&gt;script.aculo.us&lt;/a&gt; user interface library. I&#39;m not sure if Thomas coined the term &quot;MicroJS&quot; or if it&#39;s been in use for a while, but it describes the micro-framework methodology perfectly. So what is a micro-framework? The best way to answer that is to first ask what is a JavaScript framework?&lt;/p&gt;&lt;p&gt;JavaScript frameworks have been around since around 2005 (looking at the Prototype and jQuery history); their purpose is to allow developers to easily add interactivity to a page by creating a JavaScript abstraction layer to build from. The library takes care of any cross browser issues and quirks, allowing developers to focus on the &quot;cool&quot; stuff, building websites. As a developer I can&#39;t thank all the library authors enough; without them my working life would be so much more stressful (and my hair would be even grayer than it is now!).&lt;/p&gt;&lt;p&gt;Once you know what a JavaScript framework is, it doesn&#39;t take a genius to guess what a micro-framework is. Where as a full framework will have many tools (methods) a developer can use, a micro-framework only focuses on a very specific set of functions. I like the knife analogy: a full-framework like Dojo would be a Swiss Army knife where as a micro-framework could be considered a small pen knife. So which do you use in a project? This completely depends on the project in hand. I&#39;ve put together a small list of pros and cons for each (let me know if you have any others):&lt;/p&gt;&lt;h3 id=&quot;full-framework&quot;&gt;Full Framework&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Extensive set of features to cater for most eventualities.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Consistent API across features.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Large user base with lots of community support.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Huge number of working examples available.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Many developers to fix bugs and add additional functionality.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Single point of reference of documentation.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Large code base could be very daunting to new users.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Large page footprint.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Many features of the library may not be needed.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;micro-framework&quot;&gt;Micro-Framework&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Small page weight, usually less than 5K.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Small set of features so very quick to pick up and use.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Main focus on a very specific set of functionality.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;No feature creep or excess code.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Use the right tool for the right job.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Small number of developers, bugs and issues may take a while to be fixed.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Development of the framework may stop completely.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;May be very little support from the author and the community.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Fellow developers in your team may not be familiar with the framework.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Mixing micro-frameworks could conflict if badly coded.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Multiple frameworks could mean multiple HTTP requests.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Multiple points of reference for API docs.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Multiple frameworks could lead to an overlap in functionality.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Many of the cons for micro-frameworks stem from using more than one at a time, but if you only plan on using one then they can be ignored.&lt;/p&gt;&lt;p&gt;Micro-frameworks caught my eye recently due to a couple of small projects I&#39;d been working on. The projects all used vanilla JavaScript as there was no need for a helper library. I later realised I needed to attach a few events and manipulate the DOM but I wanted to avoid including jQuery in the project as I only needed a small fraction of its functionality. Luckily the MicroJS website came to the rescue.&lt;/p&gt;&lt;p&gt;The examples below are taken from &lt;a href=&quot;https://github.com/ded/qwery&quot;&gt;Query&lt;/a&gt;, &lt;a href=&quot;https://github.com/ded/bonzo&quot;&gt;Bonzo&lt;/a&gt;, &lt;a href=&quot;https://github.com/kbjr/Events.js&quot;&gt;Events.js&lt;/a&gt; and &lt;a href=&quot;https://github.com/fat/bean&quot;&gt;Bean&lt;/a&gt;. As you can see the usage for each is pretty self-explanatory with lots more functionality is available from each library&#39;s website. If you&#39;re a jQuery user the syntax will look &lt;a href=&quot;http://www.dustindiaz.com/qwery&quot;&gt;very familiar&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CSS selector and DOM utility&lt;/strong&gt;&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/*
Query - Dustin Diaz
https://github.com/ded/qwery
CSS selector engine
*/&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#myid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.myclass&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#myid .myclass div a&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;a, div, strong&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/*
Query Paired with Bonzo - Dustin Diaz
https://github.com/ded/bonzo
DOM utility
*/&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#myid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.myclass&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#myid .myclass div a&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;anchorClass&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;a, div, strong&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Events&lt;/strong&gt;&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/*
Events.js - James Brumond
https://github.com/kbjr/Events.js
Event handler library
*/&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;Events&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;load&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Page loaded, do something&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Invoke the page load event&lt;/span&gt;
Events&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;invoke&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;load&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Mouse click event&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;Events&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;selectedElements&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;click&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Mouse has been clicked&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Very handy keystroke event&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;Events&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;keystroke.Ctrl+Shift+Alt+S&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;saveMySlices&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/*
Bean - Dustin Diaz
https://github.com/fat/bean
Event handler library
*/&lt;/span&gt;
bean&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;selectedElements&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;click&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Mouse has been clicked&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//DOM has loaded&lt;/span&gt;
bean&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;DOMContentLoaded&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Page loaded, do something&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Invoke an event on an element&lt;/span&gt;
bean&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fire&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;selectedElement&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;click&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Remove the event from an element&lt;/span&gt;
bean&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;selectedElement&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;click&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&#39;ll be trying out a few more of these micro-frameworks for my personal projects in the future as the ability to select only the functionality you need really appeals to me. Are there any other micro-frameworks you&#39;ve used that you&#39;d recommend? Leave me a comment and I&#39;ll check them out.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: It just so happens that a handy library website has emerged inspired by the MicroJS website called &lt;a href=&quot;http://www.everyjs.com/&quot;&gt;EveryJS&lt;/a&gt;. The website lists all library&#39;s rather than just micro versions. Not all are listed but I&#39;m sure they will be added soon so it&#39;s one to keep your eye on.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Chrome Developer Tools at Google IO 2011</title>
    <link href="https://nooshu.com/blog/2011/05/18/chrome-developer-tools-at-google-io-2011/" />
    <updated>2011-05-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/05/18/chrome-developer-tools-at-google-io-2011/</id>
    <content type="html">&lt;p&gt;I&#39;ve been a long time user of Firefox&#39;s magnificent &lt;a href=&quot;http://getfirebug.com/&quot;&gt;Firebug extension&lt;/a&gt;; I honestly cannot remember how I used to develop websites without it (can you?). Changing web pages on the fly and debugging issues in seconds rather than minutes, we really are spoiled. I thought it would be hard for any other debugging tool to come close to Firebug in terms of ease of use and available tool set. Oh how I was wrong!&lt;/p&gt;&lt;p&gt;Paul Irish and Pavel Feldman hosted a talk at this years Google I/O about Chrome Dev Tools and what&#39;s new in the world of debugging in Chrome. I must admit, there were a number of jaw dropping moments in the video where it dawned on me how powerful the tools actually are. Here are a few of my favourite points:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;http://www.youtube.com/watch?v=N8SS-rUEZPg&amp;feature=player_embedded#t=431s&quot;&gt;7:11&lt;/a&gt;: Chrome monitoring the changes to the style sheets and letting you know what has changed.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://www.youtube.com/watch?v=N8SS-rUEZPg&amp;feature=player_detailpage#t=475s&quot;&gt;7:55&lt;/a&gt;: Ability to revert back to different CSS versions. The Dev Tools automatically creates a new version as you change code.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://www.youtube.com/watch?v=N8SS-rUEZPg&amp;feature=player_detailpage#t=1262s&quot;&gt;21:02&lt;/a&gt;: A whole host of JavaScript debugging tools. Edit the script in the browser (with code colouring.. nice) rather than edit in IDE, upload, test, repeat...&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://www.youtube.com/watch?v=N8SS-rUEZPg&amp;feature=player_detailpage#t=1831s&quot;&gt;30:31&lt;/a&gt;: My favourite part, the remote debugging feature. Chrome can act as a server meaning you can connect too and edit pages remotely. At first you may think &quot;well, what&#39;s the point?&quot;. As this is being done at the WebKit level it will soon be available for all WebKit browsers including mobile devices. Debug mobiles browser directly from your desktop. Wow!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&#39;m definitely going to have to wean myself off Firebug and give the Chrome Dev Tools a blast. As mentioned in the video, the tools are being developed all the time with new features landing daily (on dev channel). Looks like Firebug has some serious competition! If you&#39;re a developer with 43 minutes to spare, watch the video, you won&#39;t regret it.&lt;/p&gt;&lt;p&gt;A few helpful links mentioned in the video:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;http://leaverou.me/css3patterns/&quot;&gt;Lea Verous CSS3 pattern gallery&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://code.google.com/chrome/devtools/docs/overview.html&quot;&gt;Full documentation for Dev Tools&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://goo.gl/IYNuQ&quot;&gt;The slides – Chrome Dev Tools: Reloaded&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>WordPress Stats broken? Grab yourself a Jetpack!</title>
    <link href="https://nooshu.com/blog/2011/03/22/wordpress-stats-broken-grab-yourself-a-jetpack/" />
    <updated>2011-03-22T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/03/22/wordpress-stats-broken-grab-yourself-a-jetpack/</id>
    <content type="html">&lt;p&gt;Last week one of my favourite WordPress plug-ins broke: &lt;a href=&quot;http://wordpress.org/extend/plugins/stats/&quot;&gt;WordPress Stats&lt;/a&gt;. For anyone who doesn&#39;t use this fantastic plug-in it&#39;s a very clean and simple way of viewing visitor statistics directly on your dashboard. It isn&#39;t as extensive as Google Analytics and for that I&#39;m grateful as it doesn&#39;t need to be. I first thought it was a hosting issue but that turned out to be incorrect. The plug-in, in it&#39;s current form has been discontinued as it has been rolled into a compilation of useful WordPress tools called &lt;a href=&quot;http://wordpress.org/extend/plugins/jetpack/&quot;&gt;Jetpack&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So what do you get in Jetpack? Well here are three of my favourites:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;WordPress.com Stats&lt;/strong&gt;: exactly what it says on the tin...&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Shortcode Embeds&lt;/strong&gt;: easily add media from YouTube, Vimeo and Slideshare.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;LaTeX&lt;/strong&gt;: need equations in a blog post? This ones for you then.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There are eight plug-ins currently available with more coming soon.&lt;/p&gt;&lt;p&gt;The last plug-in I mentioned, LaTeX may sound like it&#39;s come straight from a .xxx domain name but bare with me; under that slightly suspect name is a powerful tool for anyone who needs to format equations in a post. Here&#39;s a small demonstration:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: LaTeX support now disabled since moving to Jekyll. &lt;a href=&quot;http://jekyllrb.com/docs/extras/&quot;&gt;See here&lt;/a&gt; for information on how to enable it.&lt;/p&gt;&lt;p&gt;LaTeX has allowed me to embed one of the most beautiful equations in all of mathematics; Euler&#39;s Equation directly into my blog post. It links Real Numbers, Euler&#39;s Number, Imaginary Numbers and Pi in a staggeringly simple equation. There&#39;s a mass of information on LaTeX &lt;a href=&quot;http://en.wikibooks.org/wiki/LaTeX&quot;&gt;here&lt;/a&gt; with everything you&#39;d ever need pretty much covered.&lt;/p&gt;&lt;p&gt;So if you&#39;re looking to give your WordPress blog a bit of a boost (horrible pun I know), give &lt;a href=&quot;http://wordpress.org/extend/plugins/jetpack/&quot;&gt;Jetpack&lt;/a&gt; a whirl!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Fractal magic with WebGL</title>
    <link href="https://nooshu.com/blog/2011/03/16/fractal-magic-with-webgl/" />
    <updated>2011-03-16T23:42:08Z</updated>
    <id>https://nooshu.com/blog/2011/03/16/fractal-magic-with-webgl/</id>
    <content type="html">&lt;p&gt;I&#39;ve been so busy recently I haven&#39;t had chance to work on any personal projects (bar a small amount of &lt;a href=&quot;http://nodejs.org/&quot;&gt;node.js&lt;/a&gt;, but that&#39;s a future blog post); so I thought I do a quick post on a very impressive project that caught my eye last week: &lt;a href=&quot;http://fractal.io/&quot;&gt;Fractal Lab&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Fractal Lab interactive examples&quot; decoding=&quot;async&quot; height=&quot;689&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/03/16/fractal-magic-with-webgl/nGAGhZhUhK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/03/16/fractal-magic-with-webgl/nGAGhZhUhK-300.webp 300w, https://nooshu.com/blog/2011/03/16/fractal-magic-with-webgl/nGAGhZhUhK-600.webp 600w, https://nooshu.com/blog/2011/03/16/fractal-magic-with-webgl/nGAGhZhUhK-814.webp 814w&quot; title=&quot;Pure mathematical worlds generated using Fractal Lab&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Fractal Lab, as the name suggests, is a fractal rendering tool using OpenGL Shading Language (GLSL) by a very talented programmer called &lt;a href=&quot;http://www.subblue.com/about&quot;&gt;Tom Beddard&lt;/a&gt;. Now I&#39;m not going to lie to you, the mathematics behind all this goes way over my head! OpenGL on the other hand is something I&#39;ve read about with increasing frequency recently due to the WebGL explosion happening on the &#39;net at the moment. Big thanks to all the developers of Firefox, Chrome (Webkit) and Opera for that! Internet Explorer 9 on the other hand &lt;a href=&quot;http://learningwebgl.com/blog/?p=993&quot;&gt;isn&#39;t planning on supporting it&lt;/a&gt;. Hopefully that will change in the future.&lt;/p&gt;&lt;p&gt;Fractal Lab allows you to render out 2D &amp; 3D fractals directly to your browser and navigate through them in real time! Be warned though, this will push your browser and graphics card really hard. It mentions this on the site; it&#39;s possible to lock-up your GPU with certain GLSL fractal shaders, so be careful (my poor laptops on-board graphics card did exactly that, so he&#39;s not lying) :)&lt;/p&gt;&lt;p&gt;There&#39;s hours of fun and experimentation to be had with this superb example or WebGL. Thank you to Tom for developing it.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The developing WebGL demoscene</title>
    <link href="https://nooshu.com/blog/2011/03/07/the-developing-webgl-demoscene/" />
    <updated>2011-03-07T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/03/07/the-developing-webgl-demoscene/</id>
    <content type="html">&lt;p&gt;One of my key memories from playing a friends &lt;a href=&quot;http://en.wikipedia.org/wiki/Amiga&quot;&gt;Amiga&lt;/a&gt; oh so many years ago (apart from &lt;a href=&quot;http://en.wikipedia.org/wiki/Cannon_Fodder&quot;&gt;Cannon Fodder&lt;/a&gt;) is the cool intro demos created by the &lt;a href=&quot;http://www.scene.org/&quot;&gt;demoscene&lt;/a&gt;. These were usually found on, &lt;em&gt;cough&lt;/em&gt;, slightly dodgy versions of games that were &quot;acquired&quot; from various sources :)&lt;/p&gt;&lt;p&gt;What&#39;s always amazed me about these demos is the amount of space the programmers had to play with and the hardware available at the time; both extremely limited. While the space and hardware limitations are no longer an issue, people are still developing these amazing demos. One reason for this is due to an exciting area in Web Development that&#39;s currently flourishing: &lt;a href=&quot;http://en.wikipedia.org/wiki/WebGL&quot;&gt;WebGL&lt;/a&gt;. The &lt;a href=&quot;http://www.khronos.org/news/press/releases/khronos-releases-final-webgl-1.0-specification&quot;&gt;technical specification&lt;/a&gt; was finalised last week and developers have started to take notice of this very powerful visual programming language. Developing demos in the browser environment is a new limitation and programmers are looking at WebGL to push the boundaries.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The demoscene in action via WebGL&quot; decoding=&quot;async&quot; height=&quot;814&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/03/07/the-developing-webgl-demoscene/YHgovLRRk5-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/03/07/the-developing-webgl-demoscene/YHgovLRRk5-300.webp 300w, https://nooshu.com/blog/2011/03/07/the-developing-webgl-demoscene/YHgovLRRk5-600.webp 600w, https://nooshu.com/blog/2011/03/07/the-developing-webgl-demoscene/YHgovLRRk5-814.webp 814w&quot; title=&quot;WebGL port of &#39;glass&#39;, an old 64kb demo from 2000.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The &lt;a href=&quot;http://possan.se/junk/webglass/index.html&quot;&gt;demo above&lt;/a&gt; has been converted into WebGL by a very talented programmer called &lt;a href=&quot;http://possan.se/&quot;&gt;Per-Olov Jernberg&lt;/a&gt; (Possan). You can even take a look at the &lt;a href=&quot;https://github.com/possan/webglass&quot;&gt;source on Github&lt;/a&gt; if your that way inclined! You can view the original from April 2000 &lt;a href=&quot;http://pouet.net/prod.php?which=94&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;With modern browsers like Chrome, Firefox 4 and &lt;a href=&quot;http://my.opera.com/chooseopera/blog/2011/03/03/a-sneak-preview-of-our-upcoming-webgl-demo&quot;&gt;now Opera&lt;/a&gt; adding WebGL support and improving their JavaScript engines, there&#39;s never been a better time for Web Developers to get involved in this new demoscene. All that&#39;s needed is some JavaScript, Canvas and a little creativity. I genuinely look forward to seeing all the old school programmers diving into WebGL. I personally know very little about this new language so I&#39;ll be able to learn so much by doing a quick &quot;view source&quot;.&lt;/p&gt;&lt;p&gt;There&#39;s already a WebGL competition in progress called &lt;a href=&quot;http://gl64k.com/&quot;&gt;gl64k&lt;/a&gt; where programmers have to create an interesting visual demo with only 64k (65,536 bytes) at their disposal. Awesome stuff will be emerging from this new technology in the next couple of years and I for one can&#39;t wait to see what!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Human History in 100 Seconds</title>
    <link href="https://nooshu.com/blog/2011/02/26/human-history-in-100-seconds/" />
    <updated>2011-02-26T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/02/26/human-history-in-100-seconds/</id>
    <content type="html">&lt;p&gt;Reddit is a great place to look when you&#39;re searching for inspiration. Occasionally a gem will emerge from the background noise that will blow you away; and I often find it&#39;s the simplest ideas that have the most impact. For example: take 424,000 Wikipedia articles and cross-reference their date and location and you get &lt;a href=&quot;http://www.ragtag.info/2011/feb/2/history-world-100-seconds/&quot;&gt;&quot;A History of the World in 100 Seconds&quot;&lt;/a&gt;. A simple idea with inspiring results.&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;250&quot; mozallowfullscreen=&quot;&quot; src=&quot;https://player.vimeo.com/video/19088241&quot; title=&quot;Vimeo video&quot; webkitallowfullscreen=&quot;&quot; width=&quot;500&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;The visualisation starts off slowly but around 1400AD there&#39;s an explosion of activity. It&#39;s interesting to see that there&#39;s one little island in the middle of it all where a high percentage of the activity occurs. You may be able to guess its name, but if not, it is of course the United Kingdom. By the end of the video you can clearly see a map of the world and it&#39;s even possible to pinpoint many individual countries.&lt;/p&gt;&lt;p&gt;I&#39;d love to see a version that only focuses on Europe; there are so many references to it in the data but the visualisation zoom level doesn&#39;t do it justice. The authors &lt;a href=&quot;http://www.ragtag.info/&quot;&gt;Gareth Lloyd&lt;/a&gt; and &lt;a href=&quot;http://www.heychinaski.com/blog/&quot;&gt;Tom Martin&lt;/a&gt; have done a fantastic job of making a staggering amount of data into an interesting visual work of art.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Auto-resize a Facebook Application iframe</title>
    <link href="https://nooshu.com/blog/2011/02/25/auto-resize-a-facebook-application-iframe/" />
    <updated>2011-02-25T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/02/25/auto-resize-a-facebook-application-iframe/</id>
    <content type="html">&lt;p&gt;Huzzah! The end of another busy week full of lots of (mostly) enjoyable work; mainly involving Facebook in some way or another. Lots of clients have really jumped on Facebook recently and the demand for their own &quot;apps&quot; has increased dramatically.&lt;/p&gt;&lt;p&gt;Setting up an app inside an iframe is easy, but one thing you don&#39;t want is scroll bars as they look damn ugly! You can set an iframes height using the &lt;a href=&quot;http://developers.facebook.com/docs/reference/javascript/fb.canvas.setsize/&quot;&gt;&lt;code&gt;setSize()&lt;/code&gt; method&lt;/a&gt; but what about if the iframe varies in size between pages and user interactions (accordions, show / hides etc); you need some way of automating this. Luckily Facebook has already added this functionality to the &lt;a href=&quot;http://developers.facebook.com/docs/reference/javascript/&quot;&gt;JavaScript SDK&lt;/a&gt;, you just need to enable it. The API docs for this seemed a little flaky, with quite a few comments from people who couldn&#39;t get it to work; so here&#39;s a solution that worked for me:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div id&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fb-root&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;script type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;text/javascript&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;fbAsyncInit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Your app details here&lt;/span&gt;
        &lt;span class=&quot;token constant&quot;&gt;FB&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;appId&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;your_app_id&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;cookie&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;xfbml&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Resize the iframe when needed&lt;/span&gt;
        &lt;span class=&quot;token constant&quot;&gt;FB&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAutoResize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Load the SDK asynchronously&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; e &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;script&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;async &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;src &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;protocol &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
          &lt;span class=&quot;token string&quot;&gt;&#39;//connect.facebook.net/en_US/all.js&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;fb-root&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;script&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Copy and paste this into the body of your iframe and you&#39;re halfway there. The last thing to do is check your application settings, under one of the advanced settings pages you will find an auto resize check box that you will need to enable (sorry I can&#39;t remember exactly which page!). Once enabled your iframe should resize automatically depending on the page content. There are a few parameters you can tweak with the setAutoResize() method, they are &lt;a href=&quot;http://developers.facebook.com/docs/reference/javascript/fb.canvas.setautoresize/&quot;&gt;documented here&lt;/a&gt;. Fingers crossed it works for you too!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Important Update&lt;/strong&gt;: One thing I just noticed with the code, it didn&#39;t work in IE! Not great. Luckily there was a quick fix. Change script &lt;code&gt;type=&quot;application/javascript&quot;&lt;/code&gt; to &lt;code&gt;type=&quot;text/javascript&quot;&lt;/code&gt;. IE doesn&#39;t like application/javascript!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Google Fractalmaps</title>
    <link href="https://nooshu.com/blog/2011/02/01/google-fractalmaps/" />
    <updated>2011-02-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/02/01/google-fractalmaps/</id>
    <content type="html">&lt;p&gt;I&#39;ve been a fan of Google for quite a while now. What with their superb Chrome browser, Gmail service, &lt;a href=&quot;http://code.google.com/intl/fr-FR/more/table/&quot;&gt;extensive API&#39;s&lt;/a&gt; and most importantly their motto, &quot;Don&#39;t be evil&quot; what&#39;s not to like! Only time will tell on the last point so fingers crossed. While browsing the &lt;a href=&quot;http://googleresearch.blogspot.com/&quot;&gt;Google Research Blog&lt;/a&gt; I spotted a very interesting article: &lt;a href=&quot;http://googleresearch.blogspot.com/2011/01/julia-meets-html-5.html&quot;&gt;Julia Meets HTML5&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;julia map random colours&quot; decoding=&quot;async&quot; height=&quot;814&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2011/02/01/google-fractalmaps/ghcFiAiv03-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2011/02/01/google-fractalmaps/ghcFiAiv03-300.webp 300w, https://nooshu.com/blog/2011/02/01/google-fractalmaps/ghcFiAiv03-600.webp 600w, https://nooshu.com/blog/2011/02/01/google-fractalmaps/ghcFiAiv03-814.webp 814w&quot; title=&quot;The Julia Map with a random colour scheme (nice feature).&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Julia of course refers to Julia fractals rather than a person called Julia. Named after &lt;a href=&quot;http://en.wikipedia.org/wiki/Gaston_Julia&quot;&gt;Gaston Julia&lt;/a&gt;, a French mathematician who studied them in the 1920&#39;s. His works were largely forgotten until Benoît Mandelbrot mentioned them in his now famous works on fractals.&lt;/p&gt;&lt;p&gt;Google Labs have created an extremely slick way of navigating around various types of fractals. From the plain old boring(!), Mandelbrot and it&#39;s many variations to the Julia and Newton fractal. Select a fractal and colour scheme then zoom and drag just like a standard Google Map.&lt;/p&gt;&lt;p&gt;According to the article the demo uses the Google Maps API; I guess it may use it loosely in it&#39;s user interface but there are an infinite number of zoom levels with fractals. When you zoom into a coastline on a Google Maps you will eventually reach a limit where you will be presented with a fuzzy image (or a blank tile); this will never happen on the Julia Map. The &#39;tile&#39; information isn&#39;t downloaded from a server like Google Maps, it&#39;s calculated on the fly by the browsers JavaScript engine. The faster the JavaScript Engine the better. If you have a browser that supports the new Web Workers API you will notice a huge difference in performance, as the CPU intensive fractal calculation will be offloaded to separate worker threads rather than the single main UI thread.&lt;/p&gt;&lt;p&gt;I could spend hours investigating the infinite mathematical landscape of the Julia Map, why not &lt;a href=&quot;http://juliamap.googlelabs.com/#ll=0,0&amp;z=0&amp;p=0,ff0000&amp;f=mandelbrot&quot;&gt;try it out for yourself&lt;/a&gt; and post your results on Twitter under hashtag #juliamap.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Monitoring a value change on an input element</title>
    <link href="https://nooshu.com/blog/2011/01/15/monitoring-a-value-change-on-an-input-element/" />
    <updated>2011-01-15T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2011/01/15/monitoring-a-value-change-on-an-input-element/</id>
    <content type="html">&lt;p&gt;For a recent project I ran into a slight issue with the jQuery change event. It&#39;s not an issue with jQuery, but I was quite surprised when it didn&#39;t work as I expected. I&#39;d set up a selection of form inputs that a user could change; once changed various calculations would be made and the values outputted to a second set of disabled input elements (&lt;a href=&quot;https://nooshu.com/lab/experiments/monitoring-input-values/&quot;&gt;demo&lt;/a&gt;). The inputs are disabled as they are to be calculated rather than entered manually.&lt;/p&gt;&lt;p&gt;I&#39;d attached the jQuery change event to the output elements like so:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#outputs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;input:text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;change&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;highlightFade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;red&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So the plan was, once an input value had been changed using the jQuery().val() method the change event would fire and it would quickly colour in and out the element (using &lt;a href=&quot;http://jquery.offput.ca/highlightFade/&quot;&gt;highlightFade&lt;/a&gt;) to notify the user that it&#39;s value had changed. Unfortunately the change event wasn&#39;t firing, so I had to find another way to monitor the output elements.&lt;/p&gt;&lt;p&gt;What I ended up doing was using an array to hold the previous values of the input elements then checking to see if they had changed in the calculation. If changed then fire the event:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Array to store the old values&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; oldValues &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//Push these values into an array which is checked on calculation&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#valueOutput input:text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; thisValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;//If the old value is different to the new, change the colour&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldValues&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; thisValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
       &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;highlightFade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;red&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;//Push the new value into the array&lt;/span&gt;
   oldValues&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; thisValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s quite hard to explain exactly what&#39;s happening so I&#39;ve put together a &lt;a href=&quot;https://nooshu.com/lab/experiments/monitoring-input-values/&quot;&gt;small demo&lt;/a&gt;. Try changing one of the top input fields then tab (or click out) of the input element to recalculate. Is there an easier (or better) way to do this? Comment below.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>A three.js community on Reddit</title>
    <link href="https://nooshu.com/blog/2010/12/30/a-three-js-community-on-reddit/" />
    <updated>2010-12-30T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/12/30/a-three-js-community-on-reddit/</id>
    <content type="html">&lt;p&gt;Well 2011 is nearly upon us; which leaves me thinking... where did 2010 go! I hope I&#39;m not the only one who thinks this year has gone quick! One big web application that died in 2010 was &lt;a href=&quot;http://digg.com/news&quot;&gt;digg.com&lt;/a&gt;. I used to use it every day for the latest technology and funny videos, but then they changed it. The design became very blue and digg seemed to change from a simple community driven link aggregator to a, well, I&#39;m not sure what it changed to in all honesty. So it was time to find an alternative.&lt;/p&gt;&lt;p&gt;Luckily for me (and all digg users) there&#39;s already a well established alternative: &lt;a href=&quot;http://www.reddit.com/&quot;&gt;reddit.com&lt;/a&gt;. It may not look as flashy as digg, but it makes up for it with ease of use and functionality. Need to set up a community for your web application, current technology, interest or well, just about anything really; then reddit is the place for you.&lt;/p&gt;&lt;p&gt;So that&#39;s exactly what I&#39;ve done for Mr. doobs &lt;a href=&quot;https://github.com/mrdoob/three.js/&quot;&gt;three.js JavaScript library&lt;/a&gt;. I created a &lt;a href=&quot;http://www.reddit.com/r/threejs/&quot;&gt;Threejs community&lt;/a&gt; that anyone can submit links to relating to three.js. Over the past few weeks I&#39;ve been adding a couple of links a day, there really are a great selection of examples out there if you take the time to hunt then down.&lt;/p&gt;&lt;p&gt;So if you&#39;ve created a cool new example using three.js or any other link relating to this excellent JavaScript library &lt;a href=&quot;http://www.reddit.com/r/threejs/&quot;&gt;submit it here&lt;/a&gt;. Digg is dead. Long live reddit.com!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Alteredq: A WebGL Ninja</title>
    <link href="https://nooshu.com/blog/2010/12/10/alteredq-a-webgl-ninja/" />
    <updated>2010-12-10T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/12/10/alteredq-a-webgl-ninja/</id>
    <content type="html">&lt;p&gt;Anyone who&#39;s been following the progress of the &lt;a href=&quot;http://mrdoob.com/blog/post/693&quot;&gt;Three.js 3D JavaScript engine&lt;/a&gt; will know of &lt;a href=&quot;http://www.twitter.com/alteredq/&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;alteredq&lt;/a&gt;, as he&#39;s been helping with its development along-side Mr. doob. His latest demo using Three.js really impressed me:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;webgl ninja demonstration using Three.js&quot; decoding=&quot;async&quot; height=&quot;574&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/12/10/alteredq-a-webgl-ninja/wkq60FU3qc-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/12/10/alteredq-a-webgl-ninja/wkq60FU3qc-300.webp 300w, https://nooshu.com/blog/2010/12/10/alteredq-a-webgl-ninja/wkq60FU3qc-600.webp 600w, https://nooshu.com/blog/2010/12/10/alteredq-a-webgl-ninja/wkq60FU3qc-814.webp 814w&quot; title=&quot;A WebGL ninja! People, I think we may have just found the end of the internet.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The demo uses Three.js, WebGL and Web Workers; so that pretty much ticks every box for me. The model is from &lt;a href=&quot;http://developer.amd.com/archive/gpu/MeshMapper/pages/default.aspx&quot;&gt;AMD&#39;s MeshMapper&lt;/a&gt; which is no longer supported. I can&#39;t say I&#39;ve ever heard / used it before but it&#39;s certainly on my to-do list to try out.&lt;/p&gt;&lt;p&gt;To view the demo &lt;a href=&quot;http://alteredqualia.com/three/examples/materials_normalmap.html&quot;&gt;see here&lt;/a&gt;; be warned though you will need an up to date browser that supports WebGL and Web Workers. Grab a recent version of Chrome and enable WebGL and it should work.&lt;/p&gt;&lt;p&gt;If you really want to push the boundaries you could try the latest &lt;a href=&quot;http://tools.google.com/dlpage/chromesxs&quot;&gt;Canary Build of Chrome&lt;/a&gt; (10.0.605.0 at time of writing) which now has an optimised version of V8, called &lt;a href=&quot;http://blog.chromium.org/2010/12/new-crankshaft-for-v8.html&quot;&gt;&quot;Crankshaft&quot;&lt;/a&gt;. It uses runtime information to see which code will benefit from the most optimisation and is 2 times quicker in some tests! The Canary Build release channel of Chrome is meant for developers / early adopters who want to try out the latest features. Unlike the dev and beta release channels the Canary Build is installed independently, so it won&#39;t overwrite your current chrome settings.&lt;/p&gt;&lt;p&gt;Bring on the revolution; to paraphrase a well known mobile company: The future&#39;s bright, the future&#39;s WebGL.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Game On London 2010</title>
    <link href="https://nooshu.com/blog/2010/12/07/game-on-london-2010/" />
    <updated>2010-12-07T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/12/07/game-on-london-2010/</id>
    <content type="html">&lt;p&gt;Yesterday evening I had the pleasure of attending the &lt;a href=&quot;http://www.eventbrite.com/event/1047985553/&quot;&gt;Open Web Gaming event&lt;/a&gt; #gameon10 hosted by &lt;a href=&quot;https://gaming.mozillalabs.com/&quot;&gt;Mozilla Labs&lt;/a&gt; and &lt;a href=&quot;http://www.sixtostart.com/&quot;&gt;Six to Start&lt;/a&gt;. The evening involved five short main talks and numerous lightning talks by various attendees.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Mozilla Labs Open Gaming special event&quot; decoding=&quot;async&quot; height=&quot;308&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/12/07/game-on-london-2010/6Pw7hZ-wpJ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/12/07/game-on-london-2010/6Pw7hZ-wpJ-300.webp 300w, https://nooshu.com/blog/2010/12/07/game-on-london-2010/6Pw7hZ-wpJ-600.webp 600w, https://nooshu.com/blog/2010/12/07/game-on-london-2010/6Pw7hZ-wpJ-814.webp 814w&quot; title=&quot;Christian Heilmann getting ready for his talk at #gameon10.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There were an interesting mix of speakers, from the big name companies like Google and Mozilla, down to start-ups and independents:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://icant.co.uk/&quot;&gt;Christian Heilmann&lt;/a&gt;&lt;/strong&gt;: Very interesting talk on why developers should be developing using new open web technologies. He had some very interesting ideas for Angry Birds too (see below).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://sixtostart.com/&quot;&gt;Ernesto Jimenez&lt;/a&gt;&lt;/strong&gt;: A hands on talk on the Do&#39;s and Don&#39;ts when developing with the HTML5 canvas element. Mainly about things he&#39;d learnt the hard way.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://rawkets.com/&quot;&gt;Rob Hawkes&lt;/a&gt;&lt;/strong&gt;: Rawkets is a simple shooter game Rob developed for a University project that&#39;s really taken off. It uses various open technologies including node.js and Web Sockets on the server side.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://lomalogue.com/&quot;&gt;Rik Lomas&lt;/a&gt;&lt;/strong&gt;: Is the line between TV and computer entertainment bluring? With the help of &lt;a href=&quot;https://picklive.com/&quot;&gt;Picklive&lt;/a&gt; it certainly is. A fantasy football game played in real time, built on Javascript, jQuery, Strophe and XMPP.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.monocubed.com/&quot;&gt;Paul Truong&lt;/a&gt;&lt;/strong&gt;: Paul&#39;s talk didn&#39;t quite go to plan with a couple of teething problems with the demonstrations, but it was still very informative. I had no idea MacBook Pro&#39;s had a built in accelerometer, and using it for gaming is a stroke of genius (although I can&#39;t see many people tipping their MacBook Pro, it&#39;s a tad heavy for a controller). With the iPad you&#39;re on to a winner.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;Paul Truong at #gameon10&quot; decoding=&quot;async&quot; height=&quot;325&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/12/07/game-on-london-2010/F9MWA6QLWk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/12/07/game-on-london-2010/F9MWA6QLWk-300.webp 300w, https://nooshu.com/blog/2010/12/07/game-on-london-2010/F9MWA6QLWk-600.webp 600w, https://nooshu.com/blog/2010/12/07/game-on-london-2010/F9MWA6QLWk-814.webp 814w&quot; title=&quot;Paul Truong talking about Processing.js.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I particularly liked Christian Heilmann&#39;s talk as he had some great ideas for &lt;a href=&quot;http://www.rovio.com/index.php?page=angry-birds&quot;&gt;Angry Birds&lt;/a&gt; if it was built on open web technology. You could very easily allow people to build their own levels, add multi-player support, add a frustration rating for levels and user comments. Adding the social level building aspect to the game takes a bulk of the development time off the game developers; this is exactly what &lt;a href=&quot;http://www.mediamolecule.com/&quot;&gt;Media Molecule&lt;/a&gt; have done with &lt;a href=&quot;http://www.littlebigplanet.com/&quot;&gt;Little Big Planet&lt;/a&gt; and they now have 1 million+ levels! As an extra you could setup level building competitions and bring out a version of the Angry Birds based purely on the best custom built levels. Everyone&#39;s a winner!&lt;/p&gt;&lt;p&gt;The evening was great fun and all the speakers were brilliant. My only criticism would be that the talks seemed very rushed. Making the talks fifteen minutes instead of ten would have allowed for a slightly slower pace with a few more demos. Big thank you to Mozilla Labs, Six to Start and all the speakers.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Periodic Mashup</title>
    <link href="https://nooshu.com/blog/2010/12/01/periodic-mashup/" />
    <updated>2010-12-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/12/01/periodic-mashup/</id>
    <content type="html">&lt;p&gt;Every so often I stop work and stick my head above the parapet and see what the rest of the Web Developer community is doing, and every time I do I&#39;m amazed by some of the ideas and applications that are being built. They aren&#39;t always useful but who cares; the technology is available so why not try it and push the boundaries a little.&lt;/p&gt;&lt;p&gt;![A visual breakdown of the new Girl Talk album.]( &quot;Want to see what samples were used in the new Girl Talk album?&quot;)&lt;/p&gt;&lt;p&gt;The first application I really like is &lt;a href=&quot;http://mashupbreakdown.com/&quot;&gt;Mashupbreakdown&lt;/a&gt; by developer &lt;a href=&quot;http://twitter.com/brahn&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Benjamin Rahn&lt;/a&gt;. Mashupbreakdown is a visual representation of the samples used in the latest album by Girl Talk called &quot;All Day&quot;. That in itself is very cool, but where does it get the sample information from? Well that&#39;s the really clever part; it gets it from &lt;a href=&quot;http://en.wikipedia.org/wiki/All_Day#Track_listing&quot;&gt;Wikipedia&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Wikipedia has a page for the &quot;All Day&quot; album that contains all the sample information used in each track. Mashupbreakdown scrapes this page and and visualises it. Since Wikipedia can be edited by anyone, if you spot a mistake / new sample you can easily update the page information and the application with change accordingly!&lt;/p&gt;&lt;p&gt;At the moment it only shows one album but there&#39;s no reason the application can&#39;t be adapted to handle more albums. Benjamin has a post on &lt;a href=&quot;http://blog.mashupbreakdown.com/making-more-breakdowns&quot;&gt;his blog&lt;/a&gt; appealing for more breakdowns so I&#39;ll be checking back with the site now and then to see if any other breakdowns have appeared. Great work Benjamin.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The periodic table of HTML elements&quot; decoding=&quot;async&quot; height=&quot;634&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/12/01/periodic-mashup/FzHeuoFP7b-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/12/01/periodic-mashup/FzHeuoFP7b-300.webp 300w, https://nooshu.com/blog/2010/12/01/periodic-mashup/FzHeuoFP7b-600.webp 600w, https://nooshu.com/blog/2010/12/01/periodic-mashup/FzHeuoFP7b-814.webp 814w&quot; title=&quot;When chemistry and HTML combine you get the periodic table of HTML elements!&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The second application that really caught by eye is the &lt;a href=&quot;http://joshduck.com/periodic-table.html?url=nooshu.com&quot;&gt;Periodic Table of Elements&lt;/a&gt; by developer &lt;a href=&quot;http://www.twitter.com/joshduck&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Josh Duck&lt;/a&gt;. When I find a site I like, I immediately do a &quot;view source&quot; (don&#39;t all Web Develepers?) to see how it&#39;s been built. The Periodic Table of Elements takes any URL you pass it and tells you what HTML elements were used on the page. It&#39;s basically a prettified / simplified version of view source! The application may not be that useful but kudos to Josh for taking the time to build it. Definitely one of those ideas that I wish I&#39;d thought of!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Nasty Canvas Image Data Error</title>
    <link href="https://nooshu.com/blog/2010/11/23/nasty-canvas-image-data-error/" />
    <updated>2010-11-23T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/11/23/nasty-canvas-image-data-error/</id>
    <content type="html">&lt;p&gt;Last night I decided to revisit an experiment I wrote earlier in the year: &lt;a href=&quot;https://nooshu.com/lab/2010-04-30-using-image-data-inside-the-html5-canvas-element/&quot;&gt;Using Image Data Inside the HTML5 Canvas Element&lt;/a&gt;. It involved loading 2 - 3 images and swapping out the image data depending on the mouse position. I ran into a slight issue though, it wasn&#39;t working:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;uncaught exception: &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Exception&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;. &lt;span class=&quot;token string&quot;&gt;&quot;Component returned failure code: 0x80040111 (NS_ERROR_NOT_AVAILABLE) [nsIDOMCanvasRenderingContext2D.drawImage]&quot;&lt;/span&gt;  nsresult: &lt;span class=&quot;token string&quot;&gt;&quot;0x80040111 (NS_ERROR_NOT_AVAILABLE)&quot;&lt;/span&gt;  location: &lt;span class=&quot;token string&quot;&gt;&quot;JS frame :: /lab/experiments/canvas-image-data/js/ci.js :: anonymous :: line 27&quot;&lt;/span&gt;  data: no&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Not good! I double checked the code on my local machine and it worked. Very odd! I noticed that it wasn&#39;t working when you first hit the page but was on page reload. Then it struck me; it must be a caching issue. I was correct.&lt;/p&gt;&lt;p&gt;It turns out that the JavaScript was firing before all the images had fully loaded.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lakeImage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; originalLakeImageData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getImageData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; iWidth&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; iHeight&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above lines were trying to access ImageData that didn&#39;t exist yet and the browser was falling over because of it. Doh! Luckily I&#39;d used jQuery for a couple of functions in the experiment so I quickly wrapped the code in a load method, problem solved!&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Your code here&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This method waits until the whole page has loaded (including images) before the code inside it is fired, guaranteeing the image data is there to be manipulated when needed. &lt;a href=&quot;https://nooshu.com/lab/experiments/canvas-image-data/&quot;&gt;View the updated demo&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Full Frontal 2010</title>
    <link href="https://nooshu.com/blog/2010/11/17/full-frontal-2010/" />
    <updated>2010-11-17T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/11/17/full-frontal-2010/</id>
    <content type="html">&lt;p&gt;Last Friday I had the pleasure of attending &lt;a href=&quot;http://2010.full-frontal.org/&quot;&gt;Full Frontal 2010&lt;/a&gt; which was held at the Duke of York&#39;s cinema, Brighton. A super venue for an extremely informative and entertaining 1 day JavaScript conference. After attending in &lt;a href=&quot;https://nooshu.com/blog/2010/01/19/full-frontal-2009-in-brighton/&quot;&gt;2009&lt;/a&gt; I didn&#39;t want to miss out this year. If you&#39;ve never been to the Duke of York&#39;s cinema it&#39;s well worth a visit; be warned though, the seats are very comfy and it&#39;s quite hard to stay awake at times!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Full Frontal web conference with Remy Sharp&quot; decoding=&quot;async&quot; height=&quot;499&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/11/17/full-frontal-2010/ItwosOTyYf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/11/17/full-frontal-2010/ItwosOTyYf-300.webp 300w, https://nooshu.com/blog/2010/11/17/full-frontal-2010/ItwosOTyYf-600.webp 600w, https://nooshu.com/blog/2010/11/17/full-frontal-2010/ItwosOTyYf-814.webp 814w&quot; title=&quot;Slightly embarrassing moment for Remy via the Twitter board.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There were some truly excellent talks, ranging from IE6 and ChromeFrame to the latest developments in HTML5 and the mobile web. Big thank you to all the speakers they did a superb job:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Alex Russell&lt;/strong&gt;: Excellent talk on &lt;a href=&quot;http://code.google.com/chrome/chromeframe/&quot;&gt;ChromeFrame&lt;/a&gt;, an IE browser plug-in that replaces the IE render engine with that of Chrome (if enabled via a meta tag). He didn&#39;t have many kind words to say about IE but then again who does.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Jan Lehnardt&lt;/strong&gt;: Very informative talk on the mobile web, specifically some of the issues surrounding it. He then switched to &lt;a href=&quot;http://couchdb.apache.org/&quot;&gt;CouchDB&lt;/a&gt; and how it could be used on a mobile device. CouchDB is a very powerful document-oriented database that can be accessed from anywhere that allows HTTP requests.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Paul Rouget&lt;/strong&gt;: My favourite talk of the day. Paul is a development guru who works for Mozilla and his talk was on a few of the features we should look out for in &lt;a href=&quot;http://www.mozilla.com/firefox/beta/&quot;&gt;Firefox 4&lt;/a&gt;. This included the Audio Data API, FileAPI and WebGL. WebGL looks, well, amazing!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Paul Bakaus&lt;/strong&gt;: Who would have thought you could use JavaScript and the DOM to create a powerful gaming platform. Well Paul showed us you can by demoing his latest prototype using the &lt;a href=&quot;http://www.youtube.com/watch?v=Ol3qQ4CEUTo&quot;&gt;Aves Engine&lt;/a&gt; to create a “Sims” like game… in JavaScript!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Dan Webb&lt;/strong&gt;: Dan works or Twitter as a Front End Engineer. His talk was on how to get around the pesky same-origin problem when pulling in data from other sites. We have JSON-P sure, but there are other methods too; like postMessage, CORS and that old favourite the iframe.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brian LeRoux&lt;/strong&gt;: Brian is a developer on a product called &lt;a href=&quot;http://phonegap.com/&quot;&gt;PhoneGap&lt;/a&gt; which aims to plug the gaps for mobile developers and encourage handset creators into providing better API&#39;s.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Seb Lee-Delisle&lt;/strong&gt;: Seb is a very well known Flash developer but he also has many other skill sets up his sleeve. His talk ranged from 2D particles all the way to the &lt;a href=&quot;http://unity3d.com/&quot;&gt;Unity 3D Game Development tool&lt;/a&gt;. Incredibly inspirational talk with stunning demos. The future looks very exciting indeed!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The highlight for me was defiantly Paul Rougets talk on &quot;Batshit crazy stuff you&#39;ll be able to do in browsers&quot;. He demonstrated a selection of custom builds of Firefox 4 and left everyone gob smacked with what HTML5 has in store. Using WebGL and the Audio Data API to create a 3D scene that reacts to the music as well as illustrating how useful the new Drag and Drop File API can be. As a bonus he even patched a version of Firefox to interface directly with his web-cam and take a photo and manipulated it via the browser; no plug-ins required! Just wow!&lt;/p&gt;&lt;p&gt;Big thank you to &lt;a href=&quot;http://twitter.com/rem&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Remy&lt;/a&gt; and &lt;a href=&quot;http://twitter.com/julieanne&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Julie&lt;/a&gt; for taking the time to organise this years event; hopefully I get to go again in 2011.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Internet Explorer 9 Preview 6, we have a winner</title>
    <link href="https://nooshu.com/blog/2010/11/08/internet-explorer-9-preview-6-we-have-a-winner/" />
    <updated>2010-11-08T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/11/08/internet-explorer-9-preview-6-we-have-a-winner/</id>
    <content type="html">&lt;p&gt;No you aren&#39;t dreaming; winner and Internet Explorer (IE) have been mentioned in the same sentence. I know it&#39;s hard to believe, but the browser that has been the bane of every Web Developer&#39;s life since 2001 has finally grown up and accepted web standards. According to the &lt;a href=&quot;http://test.w3.org/html/tests/reporting/report.htm&quot;&gt;W3C HTML5 Test Suite&lt;/a&gt; released last week, IE9 p6 is the most compliant browser. Now before the huge (geek) party and dancing on the grave of IE6 starts, there are a couple of major points to note:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;IE9 is still only a preview release, not a complete browser.&lt;/li&gt;&lt;li&gt;When released IE9 will only work on Vista and Windows 7.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Point 2 is unfortunately a big party pooper. Windows XP is the operating system that &lt;a href=&quot;http://www.w3schools.com/browsers/browsers_os.asp&quot;&gt;refuses to die&lt;/a&gt;, 48% of users who visited W3schools in October were on XP. That&#39;s a large percentage of people who are excluded from installing IE9. We can always hope that these users are using a standards compliant browser from another company (Firefox, Chrome, Safari, Opera) but that may be a little optimistic.&lt;/p&gt;&lt;p&gt;So, even though IE9 is a huge step forward by Microsoft, it doesn&#39;t look like older versions of IE are going to die out any time soon. Luckily the UK government are forward thinkers... wait, no, &lt;a href=&quot;http://www.guardian.co.uk/technology/pda/2010/jul/30/internet-explorer-6-uk-government&quot;&gt;that&#39;s a lie&lt;/a&gt;. So the browser that&#39;s been crippling the web for the best part of a decade looks like it may be around for another 10 years. I&#39;ll put the party poppers back in the draw then.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress plug-ins and svn:externals</title>
    <link href="https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/" />
    <updated>2010-11-03T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/</id>
    <content type="html">&lt;p&gt;So last week I decided to play with the &lt;a href=&quot;https://nooshu.com/blog/2010/10/24/svn-post-commit-hook-on-dreamhost/&quot;&gt;post-commit hook in Subversion&lt;/a&gt; which allows you to easily update your WordPress install after committing changes to your repository. After some trial and error this works perfectly. One issue you may run into with WordPress when it&#39;s committed to SVN is using the auto-update plug-in function.&lt;/p&gt;&lt;p&gt;Committing your plug-ins to SVN tends to break this (very cool) auto-update functionality. SVN adds a hidden &lt;code&gt;.svn&lt;/code&gt; directory to each directory you commit. WordPress auto-update tries to delete the updating plug-ins folder before downloading and extracting the new version into the &lt;code&gt;wp-content/plugins directory&lt;/code&gt;. WordPress isn&#39;t able to delete the &lt;code&gt;.svn&lt;/code&gt; directories so the auto update process fails, leaving you without the plug-in you were trying to update (since it deleted it before failing).&lt;/p&gt;&lt;p&gt;So what&#39;s the solution? Well you could download the latest plug-in version and manually extract / commit it to your repository; but that&#39;s too long winded (and boring). The ideal solution lies in the use of the svn:externals property. It allows you to attach an external repository location to a folder in your own repository. Since all WordPress plug-ins are hosted in the WordPress repository you can update all your plug-in&#39;s directly from there with a quick &quot;SVN Update&quot;! Perfect!&lt;/p&gt;&lt;p&gt;You can do this via &lt;a href=&quot;http://scott.yang.id.au/2006/04/svnexternal-wordpress-plugins/&quot;&gt;SSH and the command line&lt;/a&gt; or you can do it via a SVN client with a GUI (I use TortoiseSVN). I&#39;ll be showing you how to do it via the GUI.&lt;/p&gt;&lt;p&gt;First you need to remove the plug-in you want to update from your repository if it&#39;s been added. This may seem a little strange as you are trying to update it; but we need to update from an external repository and if a version already exists it will fail:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Delete the plug-in you wish to update&quot; decoding=&quot;async&quot; height=&quot;411&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/6qB48ACGQm-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/6qB48ACGQm-300.webp 300w, https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/6qB48ACGQm-600.webp 600w, https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/6qB48ACGQm-754.webp 754w&quot; title=&quot;Delete the plug-in you need updating from the repository first&quot; width=&quot;754&quot;&gt;&lt;/p&gt;&lt;p&gt;Next you need to setup your svn:externals property on the &lt;code&gt;wp-content/**plugins**&lt;/code&gt; directory via the properties menu:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;tortoise properties menu&quot; decoding=&quot;async&quot; height=&quot;268&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/pRibtzIf-V-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/pRibtzIf-V-300.webp 300w, https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/pRibtzIf-V-600.webp 600w&quot; title=&quot;Edit the properties on the &#39;plugins&#39; directory.&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;Now for the tedious bit. For all the plug-ins you want SVN to auto-update, you will need to find the WordPress repository URL for. So for the example, to update Akismet and All in One SEO pack you will enter the following:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;akismet http://plugins.svn.wordpress.org/akismet/trunk/
all-in-one-seo-pack http://plugins.svn.wordpress.org/all-in-one-seo-pack/trunk/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img alt=&quot;Using Tortoise SVN externals&quot; decoding=&quot;async&quot; height=&quot;411&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/3qrd_7eXCe-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/3qrd_7eXCe-300.webp 300w, https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/3qrd_7eXCe-600.webp 600w, https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/3qrd_7eXCe-779.webp 779w&quot; title=&quot;Add the plug-in repositories to the externals properties, one per line.&quot; width=&quot;779&quot;&gt;&lt;/p&gt;&lt;p&gt;Once you&#39;ve added all the plug-ins you want to auto-update (remember to remove them first if they already exist in your repository!) commit your changes. Then watch the magic happen via an update:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Updating WordPress plugins directly from the repository&quot; decoding=&quot;async&quot; height=&quot;412&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/RxR-6VYBei-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/RxR-6VYBei-300.webp 300w, https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/RxR-6VYBei-600.webp 600w, https://nooshu.com/blog/2010/11/03/wordpress-plug-ins-and-svnexternals/RxR-6VYBei-814.webp 814w&quot; title=&quot;Plug-ins checked out from the WordPress repository.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Akismet and All in One SEO Pack have been checked out from the WordPress repository trunk. Every time you do an update SVN will check for new versions of the plug-ins. Great stuff! There is a slight warning with checking out from trunk; the developer could be working on a new release and unfinished code may have been committed, so the plug-in may be broken if updating from trunk! To get around this you may want to checkout from the plug-in tags (final versions) instead. To do that change the repository URLs to:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;akismet http://plugins.svn.wordpress.org/akismet/tags/2.4.0/
all-in-one-seo-pack http://plugins.svn.wordpress.org/all-in-one-seo-pack/tags/1.6.9/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Unfortunately this will involve changing your svn:externals property every time a new version of a plug-in comes out, but it&#39;s less likely your plug-ins will break. &lt;strong&gt;Note&lt;/strong&gt;: Remember to replace the tag version number with the latest version.&lt;/p&gt;&lt;p&gt;I hope this little tip helps you streamline your deployment!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; On a side note, if anybody knows how to auto update from a tag automatically please leave a comment, I&#39;d love to know if it&#39;s possible.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>SVN post-commit hook on Dreamhost</title>
    <link href="https://nooshu.com/blog/2010/10/24/svn-post-commit-hook-on-dreamhost/" />
    <updated>2010-10-24T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/10/24/svn-post-commit-hook-on-dreamhost/</id>
    <content type="html">&lt;p&gt;Over the past couple of days I decided to start using the SVN post-commit hook for deploying sites to my staging servers. Once you get it working it will save you a massive amount of time. Make changes on your local server, then, when you&#39;re ready, commit your changes to the repository and the server will auto-update the staging site. Nice! For instructions on how to set this up on your Dreamhost server take a look &lt;a href=&quot;http://wiki.dreamhost.com/Subversion#Automatic_Post-commit_Checkout&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Unfortunately a slight over-site on my part didn&#39;t make deployment easy. I followed the instructions to the letter (more than once!) but received the same error on commit:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;post-commit hook failed &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exit code &lt;span class=&quot;token number&quot;&gt;255&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; with no output.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At first I thought this was a permissions issue but that turned out not to be the case. The solution in the end was because I was saving the do_update.cgi script in Windows format in Notepad++! Doh! Windows and UNIX new lines just don&#39;t mix!&lt;/p&gt;&lt;p&gt;To save a file in UNIX compatibility mode in Notepad++ click &quot;Edit&quot; then &quot;EOL Conversion&quot;. Make sure UNIX Format is greyed out (why greyed out? no idea) when you save your CGI script. Upload to the server and you&#39;re done. Won&#39;t be forgetting to do that again any time soon!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Page not found with custom post types</title>
    <link href="https://nooshu.com/blog/2010/10/20/page-not-found-with-custom-post-types/" />
    <updated>2010-10-20T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/10/20/page-not-found-with-custom-post-types/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;IMPORTANT NOTE&lt;/strong&gt;: Using the &lt;code&gt;flush_rewrite_rules&lt;/code&gt; on the init hook is VERY expensive and shouldn&#39;t be used in this way. It is possible this call will slow down your website dramatically, maybe even bring your site down! The correct way to solve this issue is by flushing permalinks / rewrite rules. You can do this by visiting the permalinks page, running wp rewrite flush on the command line, or using &lt;code&gt;flush_rewrite_rules&lt;/code&gt; only once.&lt;/p&gt;&lt;p&gt;Just a quick post on a very frustrating problem I was having with WordPress and the new &lt;a href=&quot;http://codex.wordpress.org/Custom_Post_Types&quot;&gt;custom post types&lt;/a&gt; introduced in version 3.0. Everything was rolling along smoothly until it came to actually viewing one of the custom posts... 404 error. Weird, maybe a .htaccess issue... nope that&#39;s all good, spelling error.. nope. Long story short, this tinkering went on for a good couple of hours and the problem wouldn&#39;t go away.... cry! I tried everything I could think of but nothing helped. I could view the post pages with permalinks turned off, but it failed with them turned on.&lt;/p&gt;&lt;p&gt;After hours of frustration I stumbled upon a similar forum post with the answer:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token function&quot;&gt;flush_rewrite_rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token boolean constant&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hours of frustration over such a simple fix. Sods law I guess. Add this code to the function you use to register your custom post type like so (usually in your themes functions.php):&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;post_type_myposttype&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$labels&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;/* Labels here */&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;register_post_type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;myposttype&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;labels&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;singular_label&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;__&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;My Post Type&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;/* More settings etc */&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;rewrite&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;slug&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;custom-post-slug&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;query_var&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token boolean constant&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;supports&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;title&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;editor&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;author&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;thumbnail&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;excerpt&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;custom-fields&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;revisions&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* IMPORTANT: Only use once if you have too, see important note at the top of the page for details.  */&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;flush_rewrite_rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token boolean constant&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Initialise custom post type&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;init&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;post_type_myposttype&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;No more 404 errors! Huzzah! &lt;strong&gt;One last tip&lt;/strong&gt;: make sure you turn off any caching plug-ins (DB-Cache reloaded, Hypercache etc) before you start redeveloping your WordPress blog. Yes I made that mistake too! Yesterday evening is one I wish to forget...&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: If you are adding custom taxonomies to your post types you may need to add the flush rewrite rules to the function when you initialise them:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;create_custom_taxonomies&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;register_taxonomy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;taxonomy1&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;posttypename&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;hierarchical&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token boolean constant&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;label&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;Taxonomy1&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;register_taxonomy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;taxonomy2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;posttypename&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;hierarchical&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token boolean constant&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;label&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;Taxonomy2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/* IMPORTANT: This is bad! Don&#39;t do this! Read the important update at the top of the page, and update 2 below for details */&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;flush_rewrite_rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token boolean constant&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* Please read &quot;Update 2&quot; before adding this line */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;init&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;create_custom_taxonomies&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Update 2&lt;/strong&gt;: Adding &lt;code&gt;flush_rewrite_rules&lt;/code&gt; in the places mentioned above will force a flush for every page load. &lt;em&gt;This is bad!&lt;/em&gt; For a better solution see Kens comment below.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Add your register_post_type to init, then add a function to with register_activation_hook that itself adds an action to init (priority 11) that in turn flushes the rewrite. This way you don&#39;t flush on every single page load (which is bad).&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Alternatively you could add the &lt;code&gt;flush_rewrite_rules&lt;/code&gt; where suggested and reload your page; this will flush the rewrite rules and fix the 404 errors. Remember to &lt;strong&gt;remove or comment out the &lt;code&gt;flush_rewrite_rules&lt;/code&gt;&lt;/strong&gt; line after as it&#39;s no longer needed! Thanks Ken for this update.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>In memory of Benoit Mandelbrot</title>
    <link href="https://nooshu.com/blog/2010/10/18/in-memory-of-benoit-mandelbrot/" />
    <updated>2010-10-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/10/18/in-memory-of-benoit-mandelbrot/</id>
    <content type="html">&lt;p&gt;It was a sad day for Mathematics over the weekend, the visionary Benoit Mandelbrot died of cancer aged 85 years. Many people may not recognise his name but they will certainly recognise the fractals he helped to discover. Below is the beautiful Julia set, named after the French Mathematician Gaston Julia. There&#39;s also a &lt;a href=&quot;http://en.wikipedia.org/wiki/Mandelbrot_set&quot;&gt;Mandelbrot set&lt;/a&gt; named after the man himself; it&#39;s actually possible to &quot;zoom in&quot; to the Julia Set from the Mandelbrot set (see the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mandelbrot_set#Image_gallery_of_a_zoom_sequence&quot;&gt;Wikipedia article&lt;/a&gt;)... amazing!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;The beautiful Julia fractal&quot; decoding=&quot;async&quot; height=&quot;446&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/10/18/in-memory-of-benoit-mandelbrot/GBtEl8fZ4M-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/10/18/in-memory-of-benoit-mandelbrot/GBtEl8fZ4M-300.webp 300w, https://nooshu.com/blog/2010/10/18/in-memory-of-benoit-mandelbrot/GBtEl8fZ4M-600.webp 600w&quot; title=&quot;The Julia set; incredibly beautiful.&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;In February 2010, Mandelbrot was invited to speak at Technology, Entertainment, Design. See below for &amp;&quot;Fractals and the art of roughness&quot; a truly inspiring TED talk. RIP Benoit Mandelbrot.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>jCarousel missing item width</title>
    <link href="https://nooshu.com/blog/2010/10/12/jcarousel-missing-item-width/" />
    <updated>2010-10-12T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/10/12/jcarousel-missing-item-width/</id>
    <content type="html">&lt;p&gt;On a recent project the UX team put together a set of wireframes where the use of &lt;a href=&quot;http://developer.yahoo.com/ypatterns/selection/carousel.html&quot;&gt;carousels&lt;/a&gt; featured heavily. Luckily when you choose a JavaScript library like jQuery you have quite a few carousel plug-ins to choose from. I&#39;ve used various different ones in the past, but my favorite is called &lt;a href=&quot;http://sorgalla.com/jcarousel/&quot;&gt;jCarousel&lt;/a&gt;. You only have to take a quick look at the jCarousel homepage to see that it will do pretty much anything you ask of it. So in theory it should be a case of copy / paste the code into your site and you&#39;re away. Unfortunately Web Development doesn&#39;t always work that way; a small oversight can leave you racking your brains for hours... arghh!&lt;/p&gt;&lt;p&gt;The design and layout layout had already been created in the usual fashion. All that was left to do was to layer on all the JavaScript goodies (the fun part!). While implementing jCarousel I ran into a curious console error message:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&quot;jCarousel: No width/height set for items. This will cause an infinite loop. Aborting...&quot;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;When I checked the carousel CSS, the item width had been set:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.product&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; left&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inline&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;text-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.75em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 75px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Strange. The width had been set, so the error message must be from something else. After a few minutes investigation it turned out that the carousel didn&#39;t have a parent with a set width. When fired the carousel is wrapped with two divs; one wraps the carousel, the other wraps the carousel and the navigation. Neither of these divs have an explicit width set which is what was causing the problem. It was a very quick fix in the end:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.jcarousel-container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px 0 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 300px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* Fixed! */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Upon a little more investigation it seems you can fix this issue in other ways too. Instead of using a class of &quot;product&quot; to set the item widths I tried using the following:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;#carousel li&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* New selector also fixed it! */&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/*...*/&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.75em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 75px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Very strange! The CSS in both cases changes the same elements, it just ignored the width in the &#39;product&#39; class.&lt;/p&gt;&lt;p&gt;So for anyone else encountering the same &#39;No width/height set for items&#39; issue here&#39;s a quick check list for you to try:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Make sure you set a width on the items (duh!). Maybe try different CSS selectors if necessary.&lt;/li&gt;&lt;li&gt;Set a width on the &#39;jcarousel-container&#39; class or one of its parents.&lt;/li&gt;&lt;li&gt;If all else fails, adapt one of the skin examples bundled with jCarousel.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The last one is very annoying if you have your carousel already styled up, but it is guaranteed to work.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Goodbye Aptana, hello Komodo Edit</title>
    <link href="https://nooshu.com/blog/2010/10/01/goodbye-aptana-hello-komodo-edit/" />
    <updated>2010-10-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/10/01/goodbye-aptana-hello-komodo-edit/</id>
    <content type="html">&lt;p&gt;I&#39;ve been using &lt;a href=&quot;http://www.aptana.com/&quot;&gt;Aptana IDE&lt;/a&gt; for a good few years now, before that I used &lt;a href=&quot;http://www.eclipse.org/&quot;&gt;Eclipse IDE&lt;/a&gt;. As Aptana is based on Eclipse, moving between them was very easy with only minor differences between the two. Unfortunately I find the Eclipse platform as a whole very bloated. As a Web Developer there are just so many functions and features I&#39;m never going to use. The bloat kills the IDEs startup time and after a while this becomes very annoying. So I decided it&#39;s time to find an alternative.&lt;/p&gt;&lt;p&gt;Talking to a fellow developer recently he recommended &lt;a href=&quot;http://www.activestate.com/komodo-edit&quot;&gt;Komodo Edit&lt;/a&gt; (free), which is essentially a cut down version of Komodo IDE (not free). The list of features it supports is huge, way to many to list in this blog post but here are the ones that really caught my eye:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Supports multiple client-side and server-side technologies (HTML, CSS, JavaScript, PHP, Python)&lt;/li&gt;&lt;li&gt;No fuss auto-completion on many languages (this was sometimes an issue for me in Aptana).&lt;/li&gt;&lt;li&gt;Supports various JavaScript libraries for auto-completion (enabled via preferences).&lt;/li&gt;&lt;li&gt;Support for code snippets at the click of a button; helps cut down on some of the monkey work.&lt;/li&gt;&lt;li&gt;XPI extensions available. What makes Firefox so great is it&#39;s many extensions. This is also possible in Komodo.&lt;/li&gt;&lt;li&gt;There&#39;s an extension for &lt;a href=&quot;http://code.google.com/p/zen-coding/&quot;&gt;Zen coding&lt;/a&gt; available (yay!).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;The Komodo Edit interface&quot; decoding=&quot;async&quot; height=&quot;672&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/10/01/goodbye-aptana-hello-komodo-edit/BWa5-tr9S7-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/10/01/goodbye-aptana-hello-komodo-edit/BWa5-tr9S7-300.webp 300w, https://nooshu.com/blog/2010/10/01/goodbye-aptana-hello-komodo-edit/BWa5-tr9S7-600.webp 600w, https://nooshu.com/blog/2010/10/01/goodbye-aptana-hello-komodo-edit/BWa5-tr9S7-814.webp 814w&quot; title=&quot;Komodo Edit Interface: simple but functional.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;It hasn&#39;t been all plain sailing though, there are a few features that I miss from Aptana:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Integration with Subversion (I&#39;m guessing this is a Komodo IDE feature).&lt;/li&gt;&lt;li&gt;JSlint for the JavaScript, but I have seen an extension available so this may be possible.&lt;/li&gt;&lt;li&gt;Aptanas auto-complete feature for CSS showed what browsers were supported by the typed property, which was quite handy at times.&lt;/li&gt;&lt;li&gt;Aptana used to add the trailing &lt;code&gt;;&lt;/code&gt; in CSS to properties (it&#39;s amazing how much you miss this feature when it isn&#39;t there!).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These aren&#39;t major issues, certainly not big enough to keep me using Aptana. Hopefully as the Komodo community grows more extensions and (useful) features will be developed. As long it stays quick to load and use, I&#39;ll be happy!&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; After using Komodo for a few more days I&#39;ve noticed a couple more little bugs / annoyances:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Auto-completing a paragraph tag automatically inserts &lt;code&gt;class=&#39;classname&#39;&lt;/code&gt;, not quite sure why I&#39;d need a class for every paragraph. Maybe there&#39;s a preference for this?&lt;/li&gt;&lt;li&gt;When you paste in a long line of text it seems to &quot;drop off&quot; the end of the page and isn&#39;t recognised by the horizontal scrollbar. The only way to solve this is to use the right scrollbar arrow. Very weird!&lt;/li&gt;&lt;li&gt;When indenting multiple lines of code using the &#39;Tab&#39; key it seems to fail and only indent the line directly below the selected text. Another strange one.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There&#39;s a beta release of version 6 out available on the website, maybe they have been reported and fixed. I&#39;ll install it and see what happens. Fingers crossed!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>JS1k: We have a winner!</title>
    <link href="https://nooshu.com/blog/2010/09/27/js1k-we-have-a-winner/" />
    <updated>2010-09-27T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/09/27/js1k-we-have-a-winner/</id>
    <content type="html">&lt;p&gt;If you&#39;ve been following my blog posts over the past couple of months you will have seen &lt;a href=&quot;http://js1k.com/home&quot;&gt;JS1k&lt;/a&gt; being mentioned quite a few times. If not the idea is simple; create something cool with JavaScript using only 1024 bytes. Now you may think that you can&#39;t do much with 1Kb, but you&#39;d be wrong! Just take a look at the 1st place entry:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;JS1k winner image using canvas&quot; decoding=&quot;async&quot; height=&quot;150&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/09/27/js1k-we-have-a-winner/m_1W2mOYjt-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/09/27/js1k-we-have-a-winner/m_1W2mOYjt-300.webp 300w, https://nooshu.com/blog/2010/09/27/js1k-we-have-a-winner/m_1W2mOYjt-600.webp 600w, https://nooshu.com/blog/2010/09/27/js1k-we-have-a-winner/m_1W2mOYjt-800.webp 800w&quot; title=&quot;All this with 1024 bytes of JavaScript? That&#39;s crazy talk!&quot; width=&quot;800&quot;&gt;&lt;/p&gt;&lt;p&gt;The author &lt;a href=&quot;http://marijn.haverbeke.nl/js1k.html&quot;&gt;Marijn Haverbeke&lt;/a&gt; has created a very cool little platform game that&#39;s actually very playable. As you jump from block to block and move through the level to collect the gold coins, the game gets progressively harder. How exactly did he do that I hear you ask; well using this code of course:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;c&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;h&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;t&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;w&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;800&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;u&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;H&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;random&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;$ &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;2d&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;J&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;Y&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;if(D)for(x=405,i=y=I=0;i&amp;lt;1e4;)L=&#92;
H[i++]=i&amp;lt;9|L&amp;lt;w&amp;R()&amp;lt;.3?w:R()*u+80|0;$=++t%99-u;$=$*$/8+20;y+=Y;x+=y-H[(x+X)/u|0]&gt;9?0:X;j=H[o=&#92;
x/u|0];Y=y&amp;lt;j|Y&amp;lt;0?Y+1:(y=j,J?-10:0);with(C){A=function(c,x,y,r){r&amp;&amp;a(x,y,r,0,7,0);fillStyle=c.P&#92;
?c:&#39;#&#39;+&#39;ceff99ff78f86eeaaffffd45333&#39;.substr(c*3,3);f();ba()};for(D=Z=0;Z&amp;lt;21;Z++){Z&amp;lt;7&amp;&amp;A(Z%6,w/&#92;
2,235,Z?250-15*Z:w);i=o-5+Z;S=x-i*u;B=S&gt;9&amp;S&amp;lt;41;ta(u-S,0);G=cL(0,T=H[i],0,T+9);T%6||(A(2,25,T-7&#92;
,5),y^j||B&amp;&amp;(H[i]-=.1,I++));G.P=G.addColorStop;G.P(0,i%7?&#39;#7e3&#39;:(i^o||y^T||(y=H[i]+=$/99),&#92;
&#39;#c7a&#39;&#92;));G.P(1,&#39;#ca6&#39;);i%4&amp;&amp;A(6,t/2%200,9,i%2?27:33);m(-6,h);qt(-6,T,3,T);l(47,T);qt(56,T,56,&#92;
h);A(G);i%3?0:T&amp;lt;w?(A(G,33,T-15,10),fc(31,T-7,4,9)):(A(7,25,$,9),A(G,25,$,5),fc(24,$,2,h),D=B&amp;y&#92;
&gt;$-9?1:D);ta(S-u,0)}A(6,u,y-9,11);A(5,M=u+X*.7,Q=y-9+Y/5,8);A(8,M,Q,5);fx(I+&#39;¢&#39;,5,15)}D=y&gt;h?1:D&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;onkeydown&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;onkeyup&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;keyCode&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;J&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;38&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;J&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;37&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;39&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;See, simple... urghh! That&#39;s the minified source code so it won&#39;t make much sense, but Marijn has been kind enough to post the unminified code for everyone to look at in a &lt;a href=&quot;http://marijn.haverbeke.nl/js1k.html&quot;&gt;blog post&lt;/a&gt; about the entry. Very impressive! I&#39;ll be taking a close look at the source when I get some spare time. Many congratulations to Marijn, superb work.&lt;/p&gt;&lt;p&gt;There are 10 winners in total listed on the JS1k page, all equally impressive, but here are my 5 personal favourites:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://js1k.com/demo/224&quot;&gt;3D Snowman&lt;/a&gt;&lt;/strong&gt; by &lt;a href=&quot;http://twitter.com/romancortes&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;romancortes&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://js1k.com/demo/644&quot;&gt;Mandelbrot with Colours&lt;/a&gt;&lt;/strong&gt; by &lt;a href=&quot;http://stack.hu/&quot;&gt;Gabor Turi&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://js1k.com/demo/450&quot;&gt;Colourful Visualisation&lt;/a&gt;&lt;/strong&gt; by &lt;a href=&quot;http://benalman.com/&quot;&gt;“Cowboy” Ben Alman&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://js1k.com/demo/699&quot;&gt;Tiny Chess&lt;/a&gt;&lt;/strong&gt; by &lt;a href=&quot;http://nanochess.110mb.com/&quot;&gt;Óscar Toledo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://js1k.com/demo/15&quot;&gt;Wobbling Tunnel&lt;/a&gt;&lt;/strong&gt; by &lt;a href=&quot;http://marcinignac.com/&quot;&gt;Marcin Ignac&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Some excellent submissions there, I sure you&#39;d agree. Take a look at all the submissions on the &lt;a href=&quot;http://js1k.com/demos&quot;&gt;JS1k demo page&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I&#39;d like to say a big thank you to &lt;a href=&quot;http://qfox.nl/&quot;&gt;Peter van der Zee&lt;/a&gt; for creating the JS1k competition, I&#39;ve really enjoyed it. I&#39;m already looking forward to next year. On a side note, just imagine what&#39;s possible with 2048 bytes!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Adding a custom header image to your WordPress theme</title>
    <link href="https://nooshu.com/blog/2010/09/23/adding-a-custom-header-image-to-your-wordpress-theme/" />
    <updated>2010-09-23T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/09/23/adding-a-custom-header-image-to-your-wordpress-theme/</id>
    <content type="html">&lt;p&gt;Every new iteration of WordPress (version 3.0.1 at the time of writing) brings a host of new features and bug fixes. One feature that I completely missed in 3.0 was the new custom header functionality. Before 3.0 it would be a case of hacking together your own solution using custom fields or using a separate plug-in. Thankfully the WordPress team have added this functionality directly to the core.&lt;/p&gt;&lt;p&gt;It just so happens that I&#39;ve had a request for a customisable header image on an future project, so decided to have a play and see how difficult it is to implement. Well it isn&#39;t difficult at all, very simple in fact.&lt;/p&gt;&lt;p&gt;First you will need to edit your functions.php located in your theme directory. Go ahead and create one if it isn&#39;t in the directory. Now copy and paste the code below into the file. I&#39;ve commented the code but it&#39;s all quite self explanatory. Change the height and width of the image and change the directories where needed.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Check see if the customisetheme_setup exists&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme_setup&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Any theme customisations contained in this function&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;customisetheme_setup&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Define default header image&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;define&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;HEADER_IMAGE&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;%s/header/default.jpg&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Define the width and height of our header image&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;define&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;HEADER_IMAGE_WIDTH&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apply_filters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme_header_image_width&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;960&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;define&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;HEADER_IMAGE_HEIGHT&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apply_filters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme_header_image_height&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;220&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Turn off text inside the header image&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;define&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;NO_HEADER_TEXT&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Don&#39;t forget this, it adds the functionality to the admin menu&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;add_custom_image_header&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme_admin_header_style&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Set some custom header images, add as many as you like&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//%s is a placeholder for your theme directory&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$customHeaders&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;//Image 1&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;perfectbeach&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;url&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;%s/header/default.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;thumbnail_url&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;%s/header/thumbnails/pb-thumbnail.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;description&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;__&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;Perfect Beach&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;//Image 2&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;tiger&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;url&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;%s/header/tiger.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;thumbnail_url&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;%s/header/thumbnails/tiger-thumbnail.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;description&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;__&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;Tiger&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;//Image 3&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;lunar&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;url&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;%s/header/lunar.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;thumbnail_url&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;%s/header/thumbnails/lunar-thumbnail.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;description&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;__&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;Lunar&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Register the images with Wordpress&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;register_default_headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$customHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;endif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme_admin_header_style&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Function fired and inline styles added to the admin panel&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Customise as required&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;customisetheme_admin_header_style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;style&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;
            &lt;span class=&quot;token selector&quot;&gt;#wpbody-content #headimg&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;HEADER_IMAGE_HEIGHT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;HEADER_IMAGE_WIDTH&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px solid #333&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;endif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//Execute our custom theme functionality&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;after_setup_theme&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;customisetheme_setup&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By adding the code above you will now see a new option under &#39;Appearance&#39; in the admin panel called &#39;Header&#39;. From there you should be able to see all the images and options we just set in the functions.php file. The only thing left to do now is add the header image to the theme.&lt;/p&gt;&lt;p&gt;Where you place the following code will be depend on your theme setup; I&#39;ve decided to place it in the header.php file as I want the image to appear on every page. You may only want the image to appear on specific templates e.g. pages, archive, category (you could also use &lt;a href=&quot;http://codex.wordpress.org/Conditional_Tags&quot;&gt;WordPress conditional tags&lt;/a&gt; to do this).&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;header&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- other header code here.... --&gt;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- This line adds the header to the theme --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;headerimg&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&amp;lt;?php header_image(); ?&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&amp;lt;?php echo HEADER_IMAGE_WIDTH; ?&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&amp;lt;?php echo HEADER_IMAGE_HEIGHT; ?&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Header image alt text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And there you have it, one header image that you can customise via the WordPress admin panel. An ID is attached to the image for CSS styling where needed.&lt;/p&gt;&lt;p&gt;The example above is quite a simplified version of the header image functionality. It is possible to place text over the image and even have a different image per blog post by using the custom thumbnail functionality, but these are beyond the scope of this blog post. Copy &amp; paste and enjoy!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Drupal Love! Stuff I&#39;ve learnt!</title>
    <link href="https://nooshu.com/blog/2010/09/09/drupal-love-stuff-ive-learnt/" />
    <updated>2010-09-09T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/09/09/drupal-love-stuff-ive-learnt/</id>
    <content type="html">&lt;p&gt;I created a &lt;a href=&quot;https://nooshu.com/blog/2010/07/13/new-to-drupal-here-are-a-few-helpful-links/&quot;&gt;post a couple of months ago&lt;/a&gt; about how I was learning &lt;a href=&quot;http://drupal.org/&quot;&gt;Drupal&lt;/a&gt; for an up and coming project; well that project finally went live this week. Huzzah! It wasn&#39;t all smooth sailing; there was lots of trial and error, head scratching and a few mini panics, but I got there in the end. It&#39;s amazing what you learn over the course of a project. Any future Drupal projects will be so much quicker and easier to setup due to the fact that I now actually know how to use Drupal!&lt;/p&gt;&lt;p&gt;So anyway here are a few things I learnt along the way:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;There are some &lt;a href=&quot;http://drupal.org/node/190815&quot;&gt;very simple yet powerful&lt;/a&gt; templating rules you can use to style your website. Learning how they work will really help you style specific pages and blocks.&lt;/li&gt;&lt;li&gt;Looking for some functionality that is missing by default? I bet there&#39;s already a module that does the job. Check out &lt;a href=&quot;http://drupalmodules.com/&quot;&gt;Drupal modules&lt;/a&gt;, this excellent website lists and rates thousands of Drupal modules which you can install and use.&lt;/li&gt;&lt;li&gt;If you do find a module that adds the functionality you need, double check that there aren&#39;t other modules about that do it better. Since Drupal has been around for some time, many modules haven&#39;t been updated in quite a while so they may just be occupying the name space. It&#39;s always best to check before you dive into using one particular module as it may not always be the best route to take.&lt;/li&gt;&lt;li&gt;Learn how to use the &lt;a href=&quot;http://drupal.org/project/cck&quot;&gt;Content Construction Kit&lt;/a&gt; (CCK). If you are missing a type of CCK field, look for an additional module; I&#39;m sure there will be one you can install that does the job. You really can make content types do anything you want with CCK.&lt;/li&gt;&lt;li&gt;Learn how to use the &lt;a href=&quot;http://drupal.org/project/views&quot;&gt;Views module&lt;/a&gt;. I really can&#39;t express this point enough. The Views module is without a doubt one of the most powerful additions to any CMS I&#39;ve ever come across. With it you can take any content and display it however you like on a page. The layout is a little intimidating at first, but once you get your head round it you will have a dynamic site up and running in no time at all.&lt;/li&gt;&lt;li&gt;Install the &lt;a href=&quot;http://drupal.org/project/imagecache&quot;&gt;ImageCache module&lt;/a&gt;. It ties in with both CCK and Views allowing you to upload and display images on your site however you need to. If you add a new image preset the module will automatically iterate over old images so you never have to go in and manually adjust images if you change your site design / functionality.&lt;/li&gt;&lt;li&gt;One thing I missed from Drupal was the ability to add parent / child nodes. Luckily theres a module called &lt;a href=&quot;http://drupal.org/project/nodehierarchy&quot;&gt;Node Hierarchy&lt;/a&gt; that will add this functionality. It even handles the breadcrumbs and Views for you. &lt;strong&gt;Note&lt;/strong&gt;: Make sure you don&#39;t have ‘Taxonomy breadcrumb&#39; enabled with Node Hierarchy as the breadcrumbs won&#39;t work. I learnt that the hard way!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Hopefully other budding Drupal users found the tips helpful. I&#39;m still a Drupal novice, but now I know the basics the site possibilities are endless.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The beauty of the Mandelbrot set</title>
    <link href="https://nooshu.com/blog/2010/08/18/the-beauty-of-the-mandelbrot-set/" />
    <updated>2010-08-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/08/18/the-beauty-of-the-mandelbrot-set/</id>
    <content type="html">&lt;p&gt;In the past I&#39;ve &lt;a href=&quot;https://nooshu.com/lab/2010-03-10-mandelbrot-set-renderer-using-javascript-and-the-canvas-api/&quot;&gt;dabbled with fractals&lt;/a&gt; using the lovely HTML5 canvas element and JavaScript, so this video really caught my attention. You start with a &quot;simple&quot; &lt;a href=&quot;http://en.wikipedia.org/wiki/Mandelbrot_set&quot;&gt;Mandelbrot fractal&lt;/a&gt;, pick a point and zoom in. It all sounds so easy doesn&#39;t it! Well that&#39;s what &lt;a href=&quot;http://www.hd-fractals.com/&quot;&gt;Team Fresh&lt;/a&gt; did a few months ago, and as it says in the video description it took 2 days to setup the &lt;strong&gt;6 months&lt;/strong&gt; to render! Wow that&#39;s a lot of render time; the results were certainly worth the wait:&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;281&quot; mozallowfullscreen=&quot;&quot; src=&quot;https://player.vimeo.com/video/12185093&quot; title=&quot;Vimeo video&quot; webkitallowfullscreen=&quot;&quot; width=&quot;500&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;As you zoom further into the Mandelbrot it becomes more and more complex. You can really tell in the last 4 - 5 minutes as it started to hurt my eyes after a while. So how far &#39;in&#39; did the video zoom? Only 6.066 e228 (2^760)... that&#39;s a huge number (huge doesn&#39;t even come close to describing it)! The size of the Milky Way galaxy is &#39;only&#39; 1.0 e21! At times tt looks like the camera is panning across the fractal when in fact it is constantly zooming into one point. And that point is... well I think you better just &lt;a href=&quot;http://vimeo.com/12185093&quot;&gt;read the description&lt;/a&gt; for the video as it&#39;s a very long set of co-ordinates! Excellent work Team Fresh, I&#39;ll certainly be checking the website often.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Aloha Editor - Content editing the HTML5 way</title>
    <link href="https://nooshu.com/blog/2010/08/17/aloha-editor-content-editing-the-html5-way/" />
    <updated>2010-08-17T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/08/17/aloha-editor-content-editing-the-html5-way/</id>
    <content type="html">&lt;p&gt;The number of times I&#39;ve built a website based on a CMS only to have the client utter the words &quot;Oh I&#39;m confused, it&#39;d be so much quicker if you made the changes&quot;. Right, so the CMS was a bit of a waste of time then! But fingers crossed thanks to &lt;a href=&quot;http://aloha-editor.com/&quot;&gt;Aloha Editor&lt;/a&gt; that may be a thing of the past. Often the problem clients have is actually finding the content that needs to be edited in the CMS; Aloha to the rescue!&lt;/p&gt;&lt;p&gt;What Aloha actually does is make the content editable right on the page; no need to jump into the CMS, find the page, make the changes then preview the result. Just click on the text you wish to edit and type away. Done! It&#39;s hard to believe it actually works until you try it. Why not give it a go on their &lt;a href=&quot;http://aloha-editor.com/demos/aloha-world-example/&quot;&gt;demo page&lt;/a&gt;. It incorporates a few HTML5 technologies which could cause issues with older IE versions, but the demo page seems to work (with a few quirks).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;An example of Aloha Editor in action&quot; decoding=&quot;async&quot; height=&quot;357&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/08/17/aloha-editor-content-editing-the-html5-way/WR10Rbk5Vo-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/08/17/aloha-editor-content-editing-the-html5-way/WR10Rbk5Vo-300.webp 300w, https://nooshu.com/blog/2010/08/17/aloha-editor-content-editing-the-html5-way/WR10Rbk5Vo-600.webp 600w&quot; title=&quot;Quickly and easily edit the demo webpage.&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;The core has been written to be very small and streamlined, with additional functionality being added by the way of user contributed plug-ins. There&#39;s already an &lt;a href=&quot;http://www.aloha-editor.com/api/&quot;&gt;extensive API&lt;/a&gt; for developers to play with so I&#39;m sure many of the missing features will be plugged very soon.&lt;/p&gt;&lt;p&gt;There are alternatives about such as &lt;a href=&quot;http://tinymce.moxiecode.com/&quot;&gt;TinyMCE&lt;/a&gt; and &lt;a href=&quot;http://ckeditor.com/&quot;&gt;CKEditor&lt;/a&gt;, but they&#39;re integrated directly into a CMS&#39; administration pages rather than as an inline editor like Aloha. Now before you get too excited there are a few cons:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It isn&#39;t as simple as just dropping the scripts into a page, it needs to be integrated into a backend system for the changes to be saved.&lt;/li&gt;&lt;li&gt;Still in early development so could be quite buggy.&lt;/li&gt;&lt;li&gt;Doesn&#39;t work with Opera Browser at the moment but this is being worked on.&lt;/li&gt;&lt;li&gt;Some features aren&#39;t available yet, like image / media insertion. But I&#39;m sure these will be developed as it matures.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So there are still a few issues to iron out but it all looks very promising! There were even murmurs on Twitter today about a WordPress plug-in and a Drupal v7 module (not 6 though I presume) in development. So it won&#39;t be long before we&#39;ll be able to try it out on a live site; hopefully it&#39;s also simple enough for clients to use too!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>JS1k: Original source</title>
    <link href="https://nooshu.com/blog/2010/08/16/js1k-original-source/" />
    <updated>2010-08-16T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/08/16/js1k-original-source/</id>
    <content type="html">&lt;p&gt;Unfortunately I haven&#39;t been able to put together another &lt;a href=&quot;http://js1k.com/&quot;&gt;JS1k demo&lt;/a&gt; as I&#39;ve had lots of &#39;real&#39; work going on at the moment (boo!); so I thought I&#39;d post a fully commented version of the JavaScript code for anyone who is interested to look over.&lt;/p&gt;&lt;p&gt;Some of the entries have been breathtaking, making my &lt;a href=&quot;http://js1k.com/demo/50&quot;&gt;poor little demo&lt;/a&gt; look a little simple! Never mind. I look forward to looking over the uncompressed source of the other entries. There are just so many things to learn from them!&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * This uncompressed source is close to my entry submitted to JS1k.
 * The number of particles and colours are different, but the rest is
 * near enough the same. The code is structured in a much neater way.
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;token function function-variable&quot;&gt;animate&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Grab our canvas object and context&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; canvas &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;c&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; c &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;2d&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Set the canvas width to the same as the browser&lt;/span&gt;
    canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;innerWidth&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;innerHeight&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Make the body background black and hide the scroll bars&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;background &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;black&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;overflow &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hidden&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Grab the width / height of the canvas for later use&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Arrays used to store the position / speed / direction and radius of the particles&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; posY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    posX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    speedX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    speedY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    directionX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    directionY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    radius &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Our initialise function&lt;/span&gt;
        &lt;span class=&quot;token function function-variable&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;//Look at the width and generate a number of particles&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; width&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

                &lt;span class=&quot;token comment&quot;&gt;//For each ballplace it in a random location&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; generatedX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;floor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; generatedY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;floor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                &lt;span class=&quot;token comment&quot;&gt;//Set the initial particle start positions&lt;/span&gt;
                posX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; generatedX&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                posY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; generatedY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                &lt;span class=&quot;token comment&quot;&gt;//Each particle has a random x and y speed&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; generatedSpeedX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; generatedSpeedY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                &lt;span class=&quot;token comment&quot;&gt;//Set the initial speeds&lt;/span&gt;
                speedX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; generatedSpeedX&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                speedY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; generatedSpeedY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                &lt;span class=&quot;token comment&quot;&gt;//Set the x and y directions of each particle&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; directionX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; directionX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; directionY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; directionY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                &lt;span class=&quot;token comment&quot;&gt;//Each particle has a different radius&lt;/span&gt;
                radius&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;//Fire our draw function every 10ms&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;draw&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Fun this function to draw a frame&lt;/span&gt;
        &lt;span class=&quot;token function function-variable&quot;&gt;draw&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;//Clear the last frame by filling it black (clearRect caused issues in chrome)&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;black&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fillRect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                &lt;span class=&quot;token comment&quot;&gt;//Loop through each particle&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

                    &lt;span class=&quot;token comment&quot;&gt;//Change its x direction if it gets to the boundry&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; width &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; posX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        directionX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;token comment&quot;&gt;//Change its y direction if it gets to the boundry&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; height &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; posY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        directionY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

                    &lt;span class=&quot;token comment&quot;&gt;//Save the canvas settings before we modify anything (clip)&lt;/span&gt;
                    c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;//Before path&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//For each particle begin a path around its position&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;posY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;radius&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;token comment&quot;&gt;//Clip the path ready to be filled later&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Set the red radial gradient alpha to 1&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAlpha &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Create our red radial gradient up the top left corner&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; radGradRed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createRadialGradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        radGradRed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;red&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        radGradRed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;black&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; radGradRed&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Create a rectangle filled using the red radial&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fillRect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Set the global alpha to 0.6 for the next box (so the red shines through)&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAlpha &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Create our green radial gradient up the top right corner&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; radGradGreen &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createRadialGradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        radGradGreen&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;green&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        radGradGreen&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;black&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; radGradGreen&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Create a rectangle filled using the green radial&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fillRect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Set the global alpha to 0.4 for the next box (red / green shines through)&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAlpha &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Create our blue radial gradient at the bottom middle&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; radGradBlue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createRadialGradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        radGradBlue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;blue&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        radGradBlue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;black&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; radGradBlue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                        &lt;span class=&quot;token comment&quot;&gt;//Create a rectangle filled using the green radial&lt;/span&gt;
                        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fillRect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                    &lt;span class=&quot;token comment&quot;&gt;//Restore the canvas to how it was before the clipping path&lt;/span&gt;
                    c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;restore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                    &lt;span class=&quot;token comment&quot;&gt;//Set the new x and y position of the selected particle&lt;/span&gt;
                    posX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; directionX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;speedX&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    posY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; directionY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;speedY&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;//Set the new global alpha to 0.9 for the static particles&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAlpha &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;//Create a red particle top left&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;red&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;//Create a green particle top right&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;green&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;//Create a blue particle middle bottom&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;blue&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;height&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//Call the init function and get the particles moving&lt;/span&gt;
animate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or for those of you who are a fans of &lt;a href=&quot;http://jsfiddle.net/&quot;&gt;jsFiddle&lt;/a&gt; I&#39;ve &lt;a href=&quot;http://jsfiddle.net/JS1k/595/&quot;&gt;pasted it here&lt;/a&gt; for you look at.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>JS1k: JavaScript Optimisations</title>
    <link href="https://nooshu.com/blog/2010/08/05/js1k-javascript-optimisations/" />
    <updated>2010-08-05T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/08/05/js1k-javascript-optimisations/</id>
    <content type="html">&lt;p&gt;Following up on my post on &lt;a href=&quot;http://js1k.com/&quot;&gt;JS1k&lt;/a&gt; I thought I&#39;d put together a small list of optimisations you can use if you&#39;re looking to minify your code before &lt;a href=&quot;http://javascriptcompressor.com/&quot;&gt;compressing it&lt;/a&gt;. When you only have 1024 bytes to play with every byte counts!&lt;/p&gt;&lt;p&gt;Very obvious, store your objects for later use. You may need to use the document / context later so store them in a single byte variable.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; canvas &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;c&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;2d&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//To:&lt;/span&gt;
d&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;document
t&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;d&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;c&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
c&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;2d&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you are using Math object / functions, don&#39;t repeat &#39;Math&#39; over and over. It may not look much at first but it will pay off and save you loads of bytes&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; colour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;floor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;255&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//To:&lt;/span&gt;
&lt;span class=&quot;token constant&quot;&gt;M&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Math
&lt;span class=&quot;token constant&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;M&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;random
&lt;span class=&quot;token constant&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;M&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;floor
colour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;255&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I found myself using 2 Pi and false quite a lot in the arc method, so why not minify them too.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posX&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; posY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; radius&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//To (uses Math example above too):&lt;/span&gt;
&lt;span class=&quot;token constant&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;M&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
f&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posX&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; posY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; radius&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; f&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For your &#39;draw&#39; function use an anonymous function within setInterval(). This will save you around 5 bytes.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Code goes here...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;d&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//To:&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Code goes here...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When it comes to the canvas API it is necessary to minimise it&#39;s usage at times since the methods can&#39;t be stored like we did with the Math object. For that I used a mixture of arrays and &lt;del datetime=&quot;2010-08-05T16:57:12+00:00&quot;&gt;for&lt;/del&gt; while loops.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;red&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;closePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;blue&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;143&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;230&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;closePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;//Add more if needed...&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//To:&lt;/span&gt;
&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;red&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;blue&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;143&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;230&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
i&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; f&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;//We stored 2Pi and false in a var above&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//removed closepath() as this is done when fill is called&lt;/span&gt;
    ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Another quite obvious one, use the ternary operator instead of an if else statement.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//To:&lt;/span&gt;
Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Remember you can minify the Math functions&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An excellent couple of tips from Ben Almans &lt;a href=&quot;http://benalman.com/news/2010/08/organ1k-js1k-contest-entry/&quot;&gt;blog post&lt;/a&gt;.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/*Backwards iterating while loop*/&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;//19 bytes&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//To:&lt;/span&gt;
i&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;//17 bytes&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/*Use &#39;this&#39; instead of &#39;window&#39;*/&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//From:&lt;/span&gt;
w&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;window

&lt;span class=&quot;token comment&quot;&gt;//To:&lt;/span&gt;
w&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hopefully you&#39;ve found some of these JavaScript short-cuts useful. Have any more to add to the list?&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Learning HTML5 Canvas</title>
    <link href="https://nooshu.com/blog/2010/07/29/learning-html5-canvas/" />
    <updated>2010-07-29T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/07/29/learning-html5-canvas/</id>
    <content type="html">&lt;p&gt;For a good while now I&#39;ve been wanting to explore generating visualisations using JavaScript, and there really hasn&#39;t been an easier time to do it with web technologies like &lt;a href=&quot;http://en.wikipedia.org/wiki/Canvas_element&quot;&gt;Canvas&lt;/a&gt; and &lt;a href=&quot;http://processing.org/&quot;&gt;Processing&lt;/a&gt; (via &lt;a href=&quot;http://processingjs.org/&quot;&gt;processing.js&lt;/a&gt;). As with all things in life there&#39;s never enough hours in the day to do everything. I&#39;ve had the excellent book by Daniel Shiffman, &#39;&lt;a href=&quot;http://www.amazon.co.uk/Learning-Processing-Beginners-Programming-Interaction/dp/0123736021/ref=pd_bxgy_b_img_b&quot;&gt;Learning Processing&lt;/a&gt;&#39; which I&#39;m about 4 chapters into; stareing back at me on my desk for 8 months now. It&#39;s all bright and orange and full of cool stuff! But alas no time... until now. Time for me to pull my finger out and learn something new.&lt;/p&gt;&lt;p&gt;In terms of canvas I&#39;ve already gone through quite a few tutorials on the API and how it works, so I&#39;m not quite a complete beginner. If you are just starting out with canvas one set of tutorials I found particularly helpful is located at the &lt;a href=&quot;https://developer.mozilla.org/en/canvas_tutorial&quot;&gt;Mozilla developer center&lt;/a&gt; (MDC). I must admit I&#39;m struggling to find intermediate tutorials; lots of inspirational demonstrations but the code can make your head hurt due to its complexity.&lt;/p&gt;&lt;p&gt;I&#39;ve compiled a list of canvas related demos and sites below, which I&#39;ve been reading. I&#39;m going to concentrate on canvas for the moment then come back to Processing at a later date.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;html5 canvas with liquid particles&quot; decoding=&quot;async&quot; height=&quot;234&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dAw0G5iLVI-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dAw0G5iLVI-300.webp 300w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dAw0G5iLVI-600.webp 600w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dAw0G5iLVI-814.webp 814w&quot; title=&quot;Liquid Particles demo by Daniel Puhe&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;visualisations&quot;&gt;Visualisations&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://spielzeugz.de/html5/liquid-particles.html&quot;&gt;Liquid Particles&lt;/a&gt;&lt;/strong&gt;: Loving the simplicity and the bright colours. Try leaving the particles to collect into a tight ball then rip it apart!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.nihilogic.dk/labs/juicydrop/&quot;&gt;JuicyDrop&lt;/a&gt;&lt;/strong&gt;: If you ever tried &lt;a href=&quot;http://www.milkdrop.co.uk/&quot;&gt;Milkdrop for Winamp&lt;/a&gt; you will love this port to canvas. There are four examples of the thousands of Milkdrop visualisations available.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://9elements.com/io/projects/html5/canvas/&quot;&gt;HTML 5 Audio Experiment&lt;/a&gt;&lt;/strong&gt;: Another particle based visualisation, this time mixed with audio. I don&#39;t think the particles react to the audio (correct me if I&#39;m wrong!) as it seems to be a repeating pattern. But still very cool!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://sebleedelisle.com/demos/html5landscape.html&quot;&gt;3D landscape on HTML5 Canvas&lt;/a&gt;&lt;/strong&gt;: Superb 3D visualisation by Seb Lee-Delisle. Canvas may only have a 2D context at the moment but with a little JavaScript magic you can create 3D visualisations.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;canvas html5 video in 3d space&quot; decoding=&quot;async&quot; height=&quot;313&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/b_pR2iDVT9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/b_pR2iDVT9-300.webp 300w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/b_pR2iDVT9-600.webp 600w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/b_pR2iDVT9-814.webp 814w&quot; title=&quot;Blowing up a HTML5 Video in 3D space using canvas by Craftymind&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;technical-demos&quot;&gt;Technical Demos&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.craftymind.com/2010/04/20/blowing-up-html5-video-and-mapping-it-into-3d-space/&quot;&gt;Blowing up HTML5 video&lt;/a&gt;&lt;/strong&gt;: If the addition of plug-in less video wasn&#39;t enough in HTML 5, check out this demo. Blow up a streaming video with a mouse click.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.andrew-hoyer.com/experiments/cloth&quot;&gt;Cloth simulation&lt;/a&gt;&lt;/strong&gt;: It may not sound that exciting but the demo is very clever. Pull and push the cloth with your mouse to see how the mesh changes. Maybe one day we will see Maya ported to HTML5 (it could take a while!)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://alteredqualia.com/visualization/evolve/&quot;&gt;Image Evolve&lt;/a&gt;&lt;/strong&gt;: This has to be one of my favourite HTML5 canvas demos so far. Upload an image and watch how the computer generates the same image using overlapping polygons. The image improves over time; leave it a couple of days and a canvas based Mona Lisa can be yours!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/samples/raycaster/RayCaster.html&quot;&gt;Raycaster&lt;/a&gt;&lt;/strong&gt;: If you&#39;ve ever played &lt;a href=&quot;http://en.wikipedia.org/wiki/Wolfenstein_3D&quot;&gt;Wolfenstein 3D&lt;/a&gt; this demo will look familiar. It may not look too pretty but it&#39;s a great proof of concept.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;retro asteroids in HTML5&quot; decoding=&quot;async&quot; height=&quot;263&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/QjdVLQ1GE6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/QjdVLQ1GE6-300.webp 300w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/QjdVLQ1GE6-600.webp 600w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/QjdVLQ1GE6-778.webp 778w&quot; title=&quot;HTML 5 Asteroids by Doug McInnes&quot; width=&quot;778&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;games&quot;&gt;Games&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://dougmcinnes.com/html-5-asteroids/&quot;&gt;HTML 5 Asteroids&lt;/a&gt;&lt;/strong&gt;: If you are into retro arcade games there are plenty to choose from built with HTML5. Here&#39;s the classic &#39;Astroids&#39; for your time wasting pleasure.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://arandomurl.com/2010/07/25/html5-pacman.html&quot;&gt;HTML5 Pacman&lt;/a&gt;&lt;/strong&gt;: Another classic, this time Pacman. A of version of Pacman hit the news recently when Google replaced their logo with a playable version of &lt;a href=&quot;http://mashable.com/2010/05/25/google-pac-man/&quot;&gt;Pacman and cost 4.2 million hours of productivity&lt;/a&gt;. Not bad people!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.benjoffe.com/code/demos/canvascape/textures&quot;&gt;Experimenting With Textures&lt;/a&gt;&lt;/strong&gt;: Much like the Raycaster technical demo above only a little more advanced. The game actually looks to use some of the original Wolfenstein 3D textures. Now where are the German soldiers?&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.benjoffe.com/code/games/torus/&quot;&gt;Torus&lt;/a&gt;&lt;/strong&gt;: Looking for a 3D version of Tetris? Look no further as Ben Joffe has done exactly that.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img alt=&quot;Fire by mr doob&quot; decoding=&quot;async&quot; height=&quot;207&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dxiBaWYwOM-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dxiBaWYwOM-300.webp 300w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dxiBaWYwOM-600.webp 600w, https://nooshu.com/blog/2010/07/29/learning-html5-canvas/dxiBaWYwOM-814.webp 814w&quot; title=&quot;One of many experiments by Mr doob, this one is called &#39;Fire&#39;.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;compilation-sites-and-experiments&quot;&gt;Compilation Sites &amp; Experiments&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://mrdoob.com&quot;&gt;Mr Doob&lt;/a&gt;&lt;/strong&gt;: If you haven&#39;t seen Mr Doobs site you&#39;ve been missing out. Lots of HTML5 and Flash experiments for you to look at. You can learn lots by doing a quick view source!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.chromeexperiments.com/&quot;&gt;Chrome Experiments&lt;/a&gt;&lt;/strong&gt;: To promote their Chrome browser Google launched Chrome Experiments. The demos available really show off the speed of the &lt;a href=&quot;http://code.google.com/p/v8/&quot;&gt;V8 JavaScript engine&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.hakim.se/experiments/&quot;&gt;Hakim.se&lt;/a&gt;&lt;/strong&gt;: If you like particle visualisations you will love the site by Hakim El Hattab. The HTML5 - Keylight demo is superb.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://www.andrew-hoyer.com/experiments&quot;&gt;Andrew Hoyer&lt;/a&gt;&lt;/strong&gt;: I mentioned this site earlier with the cloth simulation; but Andrews other experiments are also worth looking at. If you like mathematics you will love these demos. On a side note I&#39;m impressed with the site design too!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Wow there&#39;s a lot to look at there, with more and more coming out every day. So little time! If only I didn&#39;t have to sleep...&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Nooshu.com now with added HTML5 goodness</title>
    <link href="https://nooshu.com/blog/2010/07/17/nooshu-com-now-with-added-html5-goodness/" />
    <updated>2010-07-17T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/07/17/nooshu-com-now-with-added-html5-goodness/</id>
    <content type="html">&lt;p&gt;So I finally pulled my finger out and took the HTML5 plunge over the past day or so; the Nooshu WordPress theme now has a sprinkling of HTML5. Mmmm I have that warm fuzzy feeling of completeness! In all honesty it&#39;s been an easy way to learn about the new &lt;a href=&quot;http://dev.w3.org/html5/spec/Overview.html&quot;&gt;HTML5 specifications&lt;/a&gt; (although they aren&#39;t complete and won&#39;t become a W3C recommendation until 2022… no that isn&#39;t a typo!).&lt;/p&gt;&lt;p&gt;HTML5 has added a host of new tags which have much more semantic meaning. So instead of using ‘divs&#39; to section off areas of your page layout, you can now use:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Some of the new HTML5 Tags --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;header&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;header&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;footer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;footer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;article&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;article&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;section&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;section&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;nav&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;nav&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s certainly a welcome addition to the modern web. My favourite part of HTML5 has to be the doctype (other than the canvas element):&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- No more --&gt;&lt;/span&gt;
&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;HTML&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;PUBLIC&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-//W3C//DTD HTML 4.01//EN&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://www.w3.org/TR/html4/strict.dtd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Much better! --&gt;&lt;/span&gt;
&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Look how simple it is! No more copy / paste from a previous project for this doctype.&lt;/p&gt;&lt;p&gt;My main concern when converting the theme to HTML5 was how it would work in IE; as IE8 supports parts of HTML5 but IE6 and IE7 not at all. When IE comes across a tag it doesn&#39;t recognise it just ignores it. Since the tags aren&#39;t recognised you won&#39;t be able to style your lovely new HTML5 template in IE6 &amp; IE7. Lucky you can patch this missing functionality using JavaScript. Remy Sharp has created a script called &lt;a href=&quot;http://remysharp.com/2009/01/07/html5-enabling-script/&quot;&gt;html5shim&lt;/a&gt;, that adds the missing tags to IE. Just add the following code to your header and you will now be able to style your HTML5 tags:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--[if lt IE 9]&gt;
    &amp;lt;script src=&quot;http://html5shim.googlecode.com/svn/trunk/html5.js&quot;&gt;&amp;lt;/script&gt;
&amp;lt;![endif]--&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The script must be in the header and must fire before the DOM is created. The script simply creates all the new HTML5 elements using the document method ‘createElement&#39;, you don&#39;t even need to attach the created elements to the DOM! Now IE will recognise the tags and you can get on with your page styling. Yay!&lt;/p&gt;&lt;p&gt;Those who use WordPress 3.0 may have noticed that the default Kubric theme is no more and it&#39;s been replaced with a new theme called twenty ten. The theme uses the HTML5 doctype but it doesn&#39;t use any of the new tags. I guess because of the IE issues. Maybe this will be changed in future releases.&lt;/p&gt;&lt;p&gt;The Nooshu theme is still a work in progress, and I&#39;ll modify areas as I learn more about the new HTML5 features. Here are a selection of links that I found incredibly useful while working on the theme:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://diveintohtml5.org/&quot;&gt;Dive into HTML5&lt;/a&gt;&lt;/strong&gt;: Read through this article and you will be well on your way to learning HTML5. It delves into the history of HTML and progresses forward right up to implementing the new specifications.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://html5doctor.com/&quot;&gt;HTML5 Doctor&lt;/a&gt;&lt;/strong&gt;: Looking for HTML5 related news or need some clarification of a new tag? Look no further than HTML5 Doctor. Lots of articles available for you to read and there&#39;s even the option to ask a question if you don&#39;t find the answers you are looking for.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://gsnedders.html5.org/outliner/&quot;&gt;HTML 5 Outliner&lt;/a&gt;&lt;/strong&gt;: So you&#39;ve created your shiny new HTML5 template; why not check it with the outliner to see if it makes sense to a user / search engine.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://validator.w3.org/&quot;&gt;W3C Markup Validator&lt;/a&gt;&lt;/strong&gt;: It&#39;s been a while since I&#39;ve used a validator since I have one &lt;a href=&quot;https://addons.mozilla.org/firefox/addon/249&quot;&gt;built into Firefox&lt;/a&gt;, but the W3C validator was very handy for pointing out attributes and tags that are no longer supported in the specifications. It also gives you a quick benchmark to aim at.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you have any other useful HTML5 tips and links let me know via the contact form.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Looking for a new CMS for small websites</title>
    <link href="https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/" />
    <updated>2010-07-15T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/</id>
    <content type="html">&lt;p&gt;Recently I&#39;ve been looking at other small CMS solutions for future projects. For a while now I&#39;ve been using &lt;a href=&quot;http://www.madebyfrog.com/&quot;&gt;Frog CMS&lt;/a&gt;; unfortunately development seems to have stopped which is a shame. At the weekend I stumbled across quite a &lt;a href=&quot;http://www.madirish.net/?article=227&quot;&gt;major security concern&lt;/a&gt; with Frog involving a Cross-site request forgery (CSRF). Using Google it is even possible to find ready to be used scripts (which I won&#39;t link to here). Being hacked using CSRF is unlikely to happen as it requires a user who regularly uses the site to click on a rogue link, but even so it&#39;s quite concerning.&lt;/p&gt;&lt;p&gt;Now I could keep using Wolf CMS (which is a variation on Frog) but my thoughts are why keep all your eggs in one basket. If Frog is vulnerable then its likely that Wolf will be too. So time to look about for a new small CMS.&lt;/p&gt;&lt;p&gt;The CMS will be used for very small websites (5 - 20 pages) where MODx, Drupal and WordPress are just overkill; It gets updated regularly and is based on PHP / MySQL. I&#39;ve been having a search and have found a few that look really promising:&lt;/p&gt;&lt;h3 id=&quot;textpattern&quot;&gt;&lt;a href=&quot;http://textpattern.com/&quot;&gt;Textpattern&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Very clean looking CMS (love the typography on its homepage... not that it matters) and having tried the &lt;a href=&quot;http://php.opensourcecms.com/scripts/details.php?scriptid=86&amp;name=Textpattern&quot;&gt;demo&lt;/a&gt; I can see the admin interface is simple to use and all the features are easy to find. I think the admin area needs a little work in terms of design, but the comments on Opensourcecms all seem to be very positive so I&#39;ll definatly be installing it locally and having a closer look.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Text pattern homepage&quot; decoding=&quot;async&quot; height=&quot;679&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/3DLrNoQXTQ-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/3DLrNoQXTQ-300.webp 300w, https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/3DLrNoQXTQ-600.webp 600w, https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/3DLrNoQXTQ-814.webp 814w&quot; title=&quot;A flexible, elegant and easy-to-use CMS. We shall see!&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;concrete5&quot;&gt;&lt;a href=&quot;http://www.concrete5.org/&quot;&gt;Concrete5&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Another interesting CMS, the video on the homepage makes it look like some sort of action movie, maybe a little over the top but whatever. I was unable to find a live demo of the CMS unfortunately, but the screencasts from the video looked very promising. There&#39;s also an option of Concrete5 hosting the CMS, which removes the (sometimes) tricky setup process; great for non-technical people.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;concrete5 homepage&quot; decoding=&quot;async&quot; height=&quot;687&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/WD14ybBJTd-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/WD14ybBJTd-300.webp 300w, https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/WD14ybBJTd-600.webp 600w, https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/WD14ybBJTd-814.webp 814w&quot; title=&quot;Dramatic homepage video, but how well does it work? Live demo please!&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;cms-made-simple&quot;&gt;&lt;a href=&quot;http://www.cmsmadesimple.org/&quot;&gt;CMS Made Simple&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Of the new CMS&#39; I&#39;ve looked at CMS Made Simple is my favorite. The &lt;a href=&quot;http://www.opensourcecms.com/demo/2/10/CMS+Made+Simple&quot;&gt;live demo&lt;/a&gt; looked good with a very intuitive administrator area. Some of the comments on Opensourcecms were quite negative; so I&#39;ll have to do some testing of the installation and template system before I commit to building a live site with it.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;CMS Made Simple homepage&quot; decoding=&quot;async&quot; height=&quot;587&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/pYOinR3N7b-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/pYOinR3N7b-300.webp 300w, https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/pYOinR3N7b-600.webp 600w, https://nooshu.com/blog/2010/07/15/looking-for-a-new-cms-for-small-websites/pYOinR3N7b-814.webp 814w&quot; title=&quot;Some negative comments but it looks very promising.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;For my next small project I&#39;ll be using one of these small CMS&#39;, but which one will it be? When i decide I&#39;ll be sure to update you. Got any other suggestions? Why not leave a comment.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>New to Drupal? Here are a few helpful links</title>
    <link href="https://nooshu.com/blog/2010/07/13/new-to-drupal-here-are-a-few-helpful-links/" />
    <updated>2010-07-13T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/07/13/new-to-drupal-here-are-a-few-helpful-links/</id>
    <content type="html">&lt;p&gt;For the past week or so I&#39;ve been expanding my horizons and started using a new CMS (to me at least), it&#39;s name is &lt;a href=&quot;http://drupal.org/&quot;&gt;Drupal&lt;/a&gt;. Now Drupal has been around for many years, originally known as &#39;Drop&#39; until it was renamed to &#39;Drupal&#39; in 2001. The CMS is now on version 6 (with alpha versions of 7 available from the download page). As I&#39;ve only just started using this extremely powerful CMS I thought I&#39;d put together a quick list of links and tips of what I&#39;ve learnt so far. Hopefully other beginners like myself will find them useful.&lt;/p&gt;&lt;h3 id=&quot;screencasts&quot;&gt;Screencasts&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Lynda.com&lt;/strong&gt;: I can&#39;t stress enough how useful &lt;a href=&quot;http://www.lynda.com/&quot;&gt;Lynda.com&lt;/a&gt; has been while learning Drupal (or any other piece of software for that matter). It has &lt;a href=&quot;http://www.lynda.com/home/ViewCourses.aspx?lpk0=413&quot;&gt;3 superb sets of screencasts&lt;/a&gt; available that will take you from a complete beginner a fairly competent Drupal developer. They aren&#39;t free but they are well worth the money!&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Learn by the Drop&lt;/strong&gt;: Another set of &lt;a href=&quot;http://learnbythedrop.com/gettingstarted&quot;&gt;brilliant screencasts&lt;/a&gt; that you can use to get up to speed on how to use Drupal. The site contains both premium and free videos for the &#39;want to be&#39; Drupal developer.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;websites&quot;&gt;Websites&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Stack Overflow&lt;/strong&gt;: Now I know this is a bit of a catch-all site as it can be used for any programming language, but you will often find many questions you have, have already been answered on &lt;a href=&quot;http://stackoverflow.com/&quot;&gt;Stack Overflow&lt;/a&gt;. The Drupal specific questions are available &lt;a href=&quot;http://stackoverflow.com/questions/tagged/drupal&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Drupal Snippets&lt;/strong&gt;: &lt;a href=&quot;http://drupalsnippets.com/&quot;&gt;Drupal Snippets&lt;/a&gt; is a whole site dedicated to.... erm... snippets for Druplal. I must admit I&#39;ve yet to use the site, but I bookmarked it as soon as I found it as I&#39;m sure it will come in extremely useful after learning the basics.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;modules&quot;&gt;Modules&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;CCK&lt;/strong&gt;: Download and install this module and developing dynamic websites in Drupal becomes so (so!) much easier. &lt;a href=&quot;http://drupal.org/project/cck&quot;&gt;CCK (Content Construction Kit)&lt;/a&gt; is a module that expands Drupals native Custom Content Types functionality opening a whole world of possibilities for your site and users. In essence CCK makes it easier to &#39;push&#39; data into your site in any way you see fit. It&#39;s even being included into the core of Drupal 7.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Views&lt;/strong&gt;: Over the past couple of days I&#39;ve been blown away by the power and versatility of the &lt;a href=&quot;http://drupal.org/project/views&quot;&gt;Views module&lt;/a&gt;. Where CCK is used to push data into your site database, Views &#39;pulls&#39; the data back out and display it in any format you like. It&#39;s quite intimidating at first as there are tonnes of options and settings, but once you get your head around it, the Drupal world is your oyster. A free screencast on how to use it is available &lt;a href=&quot;http://learnbythedrop.com/drop/22&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Pathauto&lt;/strong&gt;: Coming from WordPress I&#39;m used to its fantastic permalink functionality, once setup it just works. Drupal doesn&#39;t seem to come bundled with anything similar (please correct me here if I&#39;m wrong!) so installing the &lt;a href=&quot;http://drupal.org/project/pathauto&quot;&gt;Pathauto module&lt;/a&gt; will plug that gap. The module automatically generates a path for various content types without you having to worry about it.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;themes&quot;&gt;Themes&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Zen&lt;/strong&gt;: If you are a Front End Developer like myself you will be wanting a solid foundation with which to build and style your site. You could just edit one of the bundled themes if that&#39;s your thing, but I prefer to start a fresh. The &lt;a href=&quot;http://drupal.org/project/zen&quot;&gt;Zen theme&lt;/a&gt; is a fantastic theme with which to base your styling off. One big tip, don&#39;t edit the main Zen folder, create a sub-theme by &lt;a href=&quot;http://drupal.org/node/226507&quot;&gt;following these directions&lt;/a&gt; (I learnt that the hard way).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So there you go, hopefully fellow Drupal beginners will find the links above useful. Any that I&#39;ve missed off or that you&#39;d recommend? Leave a comment below.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress plug-in: Snipplr Snippets</title>
    <link href="https://nooshu.com/blog/2010/06/24/wordpress-plug-in-snipplr-snippets/" />
    <updated>2010-06-24T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/06/24/wordpress-plug-in-snipplr-snippets/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This plug-in is no longer maintained.&lt;/p&gt;&lt;p&gt;Recently I blogged about the fact that &lt;a href=&quot;https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/&quot;&gt;Snipplr was under new management!&lt;/a&gt;, and what great news that is. You can see the changes being made already, fingers crossed they manage to nail the spam issue.&lt;/p&gt;&lt;p&gt;Back when Snipplr was young, Tyler Hall wrote a WordPress plug-in for Snipplr that allowed you to embed your snippets directly into your posts. What a great idea; unfortunately it hasn&#39;t been updated since 2006 and much has changed in the world of WordPress. Enabling the plug-in in it&#39;s current state is a little flakey; so I&#39;ve decided to update it for WordPress 3.0+.&lt;/p&gt;&lt;p&gt;The plug-in is called &#39;Snipplr Snippets&#39; and is available to download from the &lt;a href=&quot;http://wordpress.org/extend/plugins/snipplr-snippets/&quot;&gt;WordPress plug-in repository&lt;/a&gt;. I&#39;ve used many of the functions that were in the old plug-in so it isn&#39;t a complete rewrite but much of the structure has changed (big thank you to Marcin Dominas helped with a couple of issues i was having at first).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;View of the snippler snippets side bar.&quot; decoding=&quot;async&quot; height=&quot;565&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/06/24/wordpress-plug-in-snipplr-snippets/-UfUeGEo3v-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/06/24/wordpress-plug-in-snipplr-snippets/-UfUeGEo3v-300.webp 300w, https://nooshu.com/blog/2010/06/24/wordpress-plug-in-snipplr-snippets/-UfUeGEo3v-600.webp 600w, https://nooshu.com/blog/2010/06/24/wordpress-plug-in-snipplr-snippets/-UfUeGEo3v-814.webp 814w&quot; title=&quot;Snipplr Snippets sidebar widget in action.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The administrator area has been updated and there are a couple of new features available which hopefully are self explanatory. I&#39;ve also updated to the latest version of GeSHi for the syntax highlighting, if you don&#39;t like the look a feel of the outputted code simply disable the plug-in CSS in the site header (admin panel), copy to your style.css and modify as needed. For the plug-in to work you will need a &lt;a href=&quot;http://snipplr.com/&quot;&gt;Snipplr account&lt;/a&gt; and an &lt;strong&gt;API key&lt;/strong&gt; (available from the settings page).&lt;/p&gt;&lt;p&gt;Once you&#39;ve installed the plug-in and enabled it, it&#39;s dead simple to include a snippet into your blog post like so:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;//Remove the spaces inside the brackets.
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; snippet &lt;span class=&quot;token assign-left variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;## ]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&#39;ve tested the plug-in and it seems to work fine, there was a warning from the CodeColorer plug-in because it also uses GeSHi for syntax highlighting but I haven&#39;t noticed anything break because of it. If it does please let me know and I&#39;ll fix it. If you have any suggestions / bugs leave a comment and I&#39;ll see what look into implementing / fixing them. &lt;a href=&quot;http://wordpress.org/extend/plugins/snipplr-snippets/&quot;&gt;Download here&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Using the DeviceAtlas API with WordPress</title>
    <link href="https://nooshu.com/blog/2010/06/22/using-the-deviceatlas-api-with-wordpress/" />
    <updated>2010-06-22T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/06/22/using-the-deviceatlas-api-with-wordpress/</id>
    <content type="html">&lt;p&gt;Over the past couple of weeks I&#39;ve had the absolute pleasure (no not really!) of adapting a WordPress site and making it accessible on as many mobile phones as possible. Now I must admit I&#39;ve not had much experience building mobile sites so this is all new ground for me; and by mobile website I don&#39;t mean for the iPhone only (for more information on this plus lots of swear words see &#39;&lt;a href=&quot;http://www.quirksmode.org/blog/archives/2010/02/the_iphone_obse.html&quot;&gt;The iPhone Obsession&lt;/a&gt;&#39; by PPK).&lt;/p&gt;&lt;p&gt;Luckily there are already many plug-ins available for WordPress that can get you started on your way, one that really grabbed my attention was &#39;&lt;a href=&quot;http://mobiforge.com/running/story/the-dotmobi-wordpress-mobile-pack&quot;&gt;WordPress Mobile Pack&lt;/a&gt;&#39; by MobiForge. The plug-in contains a mobile optimised theme that you can adapt for your needs and the &#39;mobile switcher&#39; functionality. Mobile switcher detects if a user is browsing from a desktop or a mobile phone and switched the theme accordingly. Great, we&#39;re already half way there!&lt;/p&gt;&lt;p&gt;One of the specifications for the site was that it must work on a set of 4 different screen widths (480px, 320px, 240px and 176px) and this is where the DeviceAtlas API comes in very useful. Device Atlas keep a huge database of mobile phone user agent strings plus specifications associated with a particular mobile phone that you can tap into and use in your web application.&lt;/p&gt;&lt;p&gt;Now one thing I must mention is that according to the mobiForge page listed above, WordPress Mobile Pack includes DeviceAtlas integration. This turns out not to be the case. I couldn&#39;t find any settings regarding DeviceAtlas in the plug-in admin area and very little mentioned on the forums. I assume the page hasn&#39;t been updated for a while and the functionality has been removed. This isn&#39;t a problem though as it&#39;s fairly simple to integrate the DeviceAtlas API into your PHP application i.e. WordPress. Here&#39;s how you do it.&lt;/p&gt;&lt;p&gt;First you must register with DeviceAtlas and order a developer licence (there&#39;s a free option available &lt;a href=&quot;http://deviceatlas.com/products&quot;&gt;here&lt;/a&gt;). Once registered grab a copy of the &lt;a href=&quot;http://deviceatlas.com/downloads&quot;&gt;PHP API&lt;/a&gt; and the latest Device JSON file (under &#39;My account&#39;). Unpack the API into the root of your web application (or wherever you like, just make sure the includes are set correctly) and copy the JSON file into the json directory along side it. You are now ready to include the API into the base WordPress Mobile Pack theme. Copy and paste the following into your header.php above the doctype:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Include the DA API&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;require_once&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;Mobi/Mtld/DA/Api.php&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Include the JSON file with the device data&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$tree&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name static-context&quot;&gt;Mobi_Mtld_DA_Api&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getTreeFromFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;json/DeviceAtlas.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Grab the User Agent string&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$ua&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$_SERVER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;HTTP_USER_AGENT&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Great now we know the user agent that the user is using to view the website (well it may not be 100% accurate but better than nothing). So now you want to query the JSON file and pick out the relevant phone specifications you need. I&#39;m just looking for the width of the device, so I included this in the head tag:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Query the JSON file ($tree) with the User Agent string and look for the display width&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$displayWidth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name static-context&quot;&gt;Mobi_Mtld_DA_Api&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPropertyAsInteger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$ua&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;displayWidth&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Mobi_Mtld_Da_Exception_InvalidPropertyException&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$displayWidth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;240&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;//Default to 240 if unknown&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Add stylesheet tweaks depending display width&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$displayWidth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;480&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bloginfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;stylesheet_directory&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;/480.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;elseif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$displayWidth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;320&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bloginfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;stylesheet_directory&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;/320.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;elseif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$displayWidth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;240&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bloginfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;stylesheet_directory&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;/240.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;elseif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$displayWidth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;176&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bloginfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;stylesheet_directory&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;/176.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;endif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All this code does is find the display width of the phone viewing the website and add a CSS file with relevant site tweaks, allowing you to optimise for that screen size. If the device isn&#39;t found I&#39;ve defaulted to a screen size in the middle of the screen sizes.&lt;/p&gt;&lt;p&gt;Looking through the API docs there are plenty of ways to distinguish between your users, using the following code will allow you to target specific phone brands:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Output example $make = &#39;Nokia&#39;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$make&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name static-context&quot;&gt;Mobi_Mtld_DA_Api&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getProperty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$ua&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;vendor&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So now you can target only Nokia users. Or instead of loading a different CSS file you could include a different sized image for different screen widths; there are tonnes of options available.&lt;/p&gt;&lt;p&gt;I must admit I really dislike having to &#39;sniff&#39; for the user agent string and having a different CSS file / image per device width, it reminds me of the Netscape / IE browser war days, oh how much fun that was! I guess it&#39;s a necessary evil.&lt;/p&gt;&lt;p&gt;A word or warning when it comes to DeviceAtlas, there seems to be distinct lack of support available. I wrote a couple of emails asking questions and got no reply, and lots of questions on the forum are left unanswered. So if you get stuck you may need to figure it out for yourself I&#39;m afraid.&lt;/p&gt;&lt;p&gt;All in all I&#39;m pleased with the results of my first adventure into the world of mobile web development. I still have lots to learn so if you have any tips and tricks please let me know!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Headspace2 JavaScript error</title>
    <link href="https://nooshu.com/blog/2010/06/17/headspace2-javascript-error/" />
    <updated>2010-06-17T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/06/17/headspace2-javascript-error/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: An update of Headspace2 has just been released that seems to solve this issue as well as a few others.&lt;/p&gt;&lt;p&gt;After updating to WordPress 3.0 (yay!) I decided to check the plug-ins that I&#39;ve written to see if they still worked in version 3.0; to my dismay &lt;a href=&quot;http://wordpress.org/extend/plugins/post-thesaurus/&quot;&gt;Post Thesaurus&lt;/a&gt; had stopped working... nooooo! Not good! A quick scan using Firebug revealed the issue: the Headspace2 plug-in.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://wordpress.org/extend/plugins/headspace2/&quot;&gt;Headspace2&lt;/a&gt; is a superb plug-in that allows you to tweak the SEO potential of your site. There are tonnes of options available; All In One SEO is easier to use, but I prefer the flexibility of Headspace2. The problem was occurring in the headspace-tags.js file:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;$&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_tag_element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; is undefined
&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;wp&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;content&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;plugins&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;headspace2&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;js&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;headspace&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;tags&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;js&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;ver&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.6&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;.32&lt;/span&gt;
Line &lt;span class=&quot;token number&quot;&gt;76&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This error was stopping the rest of the JavaScript on a post page from firing (hence breaking Post Thesaurus). Looking at line 76 of headspace-tags.js you will see:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Highlights headspace tags using the WordPress tag field as source&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;highlight_tags&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; words &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_tag_element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;,&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The code seems to be falling over when there are no tags, so I just added a ternary operator which checks the array length first.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; words&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; wordArray &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_tag_element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wordArray&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; words &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_tag_element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;,&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; words &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If there are no tags create an empty array, else run the usual code. Adding this code fixed the issue and Post Thesaurus works again. Phew! Panic over. I&#39;m sure this error will be picked up by the developer straight away and fixed in the next update.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Snipplr under new management!</title>
    <link href="https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/" />
    <updated>2010-06-16T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/</id>
    <content type="html">&lt;p&gt;A few months ago I wrote a blog post regarding Snipplr, and how it was &lt;a href=&quot;https://nooshu.com/blog/2010/03/18/snipplr-how-to-annoy-your-site-users/&quot;&gt;annoying it&#39;s user base&lt;/a&gt;. By the look of it other people noticed the severe lack of development and abundance of spam and the site has been sold to &lt;a href=&quot;http://envato.com/&quot;&gt;envato.com&lt;/a&gt; (which I must say I love the site design).&lt;/p&gt;&lt;p&gt;With Envato behind Snipplr hopefully it will go from strength to strength. They already have a huge number of popular sites behind them including &lt;a href=&quot;http://net.tutsplus.com/&quot;&gt;Nettuts&lt;/a&gt; and &lt;a href=&quot;http://freelanceswitch.com/&quot;&gt;FreelanceSwitch&lt;/a&gt; which I read regularly.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Snipplr under new management&quot; decoding=&quot;async&quot; height=&quot;211&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/YFXbYAts9E-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/YFXbYAts9E-300.webp 300w, https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/YFXbYAts9E-600.webp 600w, https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/YFXbYAts9E-814.webp 814w&quot; title=&quot;Under new management, huzzah! There have been improvements already! (no ads!)&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;There&#39;s already been activity on the &lt;a href=&quot;http://twitter.com/snipplr&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;Snipplr twitter account&lt;/a&gt; asking for feedback, so here are a few of my requests:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Remove spam snippets &amp; comments.&lt;/li&gt;&lt;li&gt;Better sign-up process to stop spam.&lt;/li&gt;&lt;li&gt;Fix the export function in the user preferences.&lt;/li&gt;&lt;li&gt;Better syntax highlighting (I hate having to click plain text before copy / paste).&lt;/li&gt;&lt;li&gt;Fix the blog links from the homepage, they have never worked for me.&lt;/li&gt;&lt;li&gt;Ability to remove comment spam from your own snippets.&lt;/li&gt;&lt;li&gt;Better design... the original orange was better than the current version.&lt;/li&gt;&lt;li&gt;Fix the gigs functionality, or at least explain how it works. I&#39;ve never had much luck.&lt;/li&gt;&lt;li&gt;Use OpenID, I hate remembering all those passwords.&lt;/li&gt;&lt;li&gt;Ability for the API to output snippets as JSON-P.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Those are all I can think of at the moment but I&#39;m sure there are others. Really looking forward to seeing how Snipplr evolves in the coming weeks and months!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>High Resolution Icons for Google Chrome</title>
    <link href="https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/" />
    <updated>2010-06-15T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/</id>
    <content type="html">&lt;p&gt;I&#39;ve been using Google Chrome as my default browser for quite a few months, I still use Firefox for Web Development but for general browsing you really can&#39;t beat the speed of Chrome. One of my favourite Chrome features is the &#39;Create application shortcuts...&#39; function. Creating an application shortcut essentially adds an icon to your desktop, Chrome will then run the web application like it is a free-standing desktop application (you still need to be connected to the internet unless it uses some form of offline storage).&lt;/p&gt;&lt;p&gt;If you are a Gmail user I highly recommend creating an application shortcut to see this function in action. One thing you will notice when you create the shortcut with Gmail is it uses a high resolution icon. If you create a shortcut using a standard website, by default the favicon.ico is used. As the favicon is limited to 16 by 16 resolution it will look very ugly blown up on your desktop.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Various application shortcuts I&#39;ve created&quot; decoding=&quot;async&quot; height=&quot;161&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/VMGL_ibNwK-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/VMGL_ibNwK-300.webp 300w, https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/VMGL_ibNwK-518.webp 518w&quot; title=&quot;You can really see the difference between the Gmail icon (left) and the standard favicons.&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;The image above really illustrates the difference between a 16 x 16 icon and a high resolution version. Now of course you could just change the icon in the Windows properties.... but that&#39;s boring. Why not offer your users a high resolution image along with the standard favicon? It really is very simple:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;icon&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/icon_32x32.png&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;sizes&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;32x32&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;icon&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/icon_48x48.png&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;sizes&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;48x48&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simply place this code in your head tag of your web application and link to the corresponding icons. Now when your user creates an application shortcut they will have a nice high resolution icon on their desktop.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Hi resolution Chrome icon&quot; decoding=&quot;async&quot; height=&quot;148&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/DsGbmhqPGa-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/DsGbmhqPGa-300.webp 300w, https://nooshu.com/blog/2010/06/15/high-resolution-icons-for-google-chrome/DsGbmhqPGa-518.webp 518w&quot; title=&quot;You can really see the difference when you add a high resolution icon&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;I&#39;ve added a high resolution icon for my blog administration, now if only &lt;a href=&quot;http://www.formula1.com/&quot;&gt;F1 live timings&lt;/a&gt; would do the same. For those of you who want to add a high resolution icon to your WordPress blog add the following code to your functions.php:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;wp_hi_res_admin_icons&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&amp;lt;link rel=&quot;icon&quot; href=&quot;/wp-content/themes/&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;__FILE__&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;/images/icon_name_32.png&quot; sizes=&quot;32x32&quot; /&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&#92;n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&amp;lt;link rel=&quot;icon&quot; href=&quot;/wp-content/themes/&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;__FILE__&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;/images/icon_name_48.png&quot; sizes=&quot;48x48&quot; /&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&#92;n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;login_head&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;wp_hi_res_admin_icons&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;admin_head&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;wp_hi_res_admin_icons&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will add the icon code to your administrator login panel and dashboard; or paste the raw HTML into your header.php so all users can see the icons.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Converting a WordPress post to a custom post type</title>
    <link href="https://nooshu.com/blog/2010/06/03/converting-a-wordpress-post-to-a-custom-post-type/" />
    <updated>2010-06-03T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/06/03/converting-a-wordpress-post-to-a-custom-post-type/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: There is now a plug-in that does this for you called &lt;a href=&quot;http://wordpress.org/extend/plugins/post-type-switcher/&quot;&gt;Post Type Switcher&lt;/a&gt; so you don&#39;t have to get your hands dirty with the SQL (thanks Bruno!). Another plug-in called &lt;a href=&quot;http://sillybean.net/wordpress/new-plugin-convert-post-types/&quot;&gt;Convert Post Types&lt;/a&gt; is also available that does the job for you, Thanks to Steve for that one.&lt;/p&gt;&lt;p&gt;With the release of WordPress 3.0, developers now have the ability to create custom post types by adding a few lines of code to a themes functions.php. The feature is a huge step forwards for WordPress as it moves closer to being a viable CMS solution (without all the hacks). There have been a number of projects I&#39;ve worked on that have required a custom post type; rather than relying on the client selecting the correct category under &#39;Add new post&#39;, which tends to always leads to issues.&lt;/p&gt;&lt;p&gt;I&#39;m in the process of converting nooshu.com to use custom post types and taxonomies, the main area in particular is my &lt;a href=&quot;https://nooshu.com/portfolio/&quot;&gt;portfolio section&lt;/a&gt;. At the moment the portfolio posts are just standard posts which have a category of &#39;portfolio&#39; attached to them. It works, but I don&#39;t get that warm fuzzy feeling, and it feels like a bit of a hack. So time to start using a custom post type of &#39;portfolio&#39; (yay!).&lt;/p&gt;&lt;p&gt;Unfortunately at the moment there doesn&#39;t seem to be a quick way of converting a published standard post into a shiny new custom post type, at least not from the administration area. After a little investigation work using &lt;a href=&quot;http://www.phpmyadmin.net/home_page/&quot;&gt;phpmyadmin&lt;/a&gt; and the nooshu WordPress database, I discovered it&#39;s actually quite easy. Be warned though, it&#39;s easy to &lt;strong&gt;break your blog&lt;/strong&gt; by editing the database directly, so make sure you back everything up. I take no responsibility for what happens if you break something!&lt;/p&gt;&lt;p&gt;To convert a post into a custom post type; in phpmyadmin look for the &lt;code&gt;wp_posts&lt;/code&gt; table. Inside it do a search, where the &lt;code&gt;post_type&lt;/code&gt; value equals &#39;post&#39;. This will give you a list of all your posts (otherwise you will also have all the revisions and attachments listed). From the list you should be able to pick out the posts you want converting to your new post type, edit one of these posts and change &#39;post_type&#39; from &#39;post&#39; to your new custom post. In my case this would be &#39;portfolio&#39;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;phpmyadmin admin panel for a WordPress post&quot; decoding=&quot;async&quot; height=&quot;489&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/06/03/converting-a-wordpress-post-to-a-custom-post-type/rW65yYcyxT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/06/03/converting-a-wordpress-post-to-a-custom-post-type/rW65yYcyxT-300.webp 300w, https://nooshu.com/blog/2010/06/03/converting-a-wordpress-post-to-a-custom-post-type/rW65yYcyxT-600.webp 600w, https://nooshu.com/blog/2010/06/03/converting-a-wordpress-post-to-a-custom-post-type/rW65yYcyxT-814.webp 814w&quot; title=&quot;Change &#39;post_type&#39; from &#39;post&#39; to your custom post type.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Click save and go back to the previous page. Done! You should now see the post appear under your new custom post type in the WordPress admin panel. If you have any issues just change &#39;post_type&#39; back to &#39;post&#39; and it will reappear under the standard posts. No information is lost when changing the post type.&lt;/p&gt;&lt;p&gt;It&#39;s not an ideal solution, hopefully it will be possible to do it from the administration panel in the future; but it does save having to re-enter all the posts under the custom post type for the moment.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress 3.0 RC1 blank page error</title>
    <link href="https://nooshu.com/blog/2010/06/02/wordpress-3-0-rc1-blank-page-error/" />
    <updated>2010-06-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/06/02/wordpress-3-0-rc1-blank-page-error/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Apologies to the author of Hyper Cache, the error isn&#39;t cause by the plug-in. The error is being caused by DB Cache Reloaded. After updating the site to 3.0 final I was getting the error on the update page, disabling DB Cache Reloaded fixed it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update 2&lt;/strong&gt;: DB Cache Reloaded has now been updated and works flawlessly in 3.0, get it &lt;a href=&quot;http://wordpress.org/extend/plugins/db-cache-reloaded/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I&#39;ve been eagerly awaiting the release of WordPress 3.0 (due any day now!). I plan on making a few changes to my blog once it is released so decided to try out &lt;a href=&quot;http://wordpress.org/development/2010/05/wordpress-3-0-release-candidate/&quot;&gt;Release Candidate 1&lt;/a&gt; on my development server. At first it all went fine, upgraded the database no problem... then a blank page. Weird! After a couple of page reloads the sparkly new dashboard popped up (it&#39;s not that different, although it does look greyer). Clicking around the admin area I could see this blank page error was randomly occurring on different pages; bugger!&lt;/p&gt;&lt;p&gt;My immediate thought was a plug-in I had installed, as I&#39;m sure this rather large error would have been picked up while testing the WordPress core functionality! Looking at my Apache error log the following error was listed:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;client &lt;span class=&quot;token number&quot;&gt;127.0&lt;/span&gt;.0.1&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; PHP Fatal error:  Call to undefined method wpdb::get_blog_prefix&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; dev.nooshu.com&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;trunk&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;httpdocs&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;wp-admin&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;includes&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;user.php on line &lt;span class=&quot;token number&quot;&gt;260&lt;/span&gt;, referer: http://dev.nooshu.com/wp-admin/edit.php?post_type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;portfolio&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The error is too vague to actually help, but after a little investigating (and from a hunch I had) I disabled any plug-ins involved in page caching; namely DB Cache Reloaded &lt;del datetime=&quot;2010-06-17T18:39:24+00:00&quot;&gt;and Hyper Cache&lt;/del&gt;. Unfortunately disabling them still didn&#39;t fix the error, so I decided to remove them completely (db-config.ini, advanced-cache.php and db.php removed from the wp-content directory). Suddenly no more blank page errors occurring! Huzzah!&lt;/p&gt;&lt;p&gt;So if you come across the same problem, then check what caching plug-ins you have installed. When 3.0 final is released I&#39;ll be doing a fresh install of both plug-ins. Hopefully that will solve the issue. They are both quite popular plug-ins so I&#39;m sure others have had the same issue.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Visualising Subversion with Gource</title>
    <link href="https://nooshu.com/blog/2010/05/24/visualising-subversion-with-gource/" />
    <updated>2010-05-24T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/05/24/visualising-subversion-with-gource/</id>
    <content type="html">&lt;p&gt;Over the weekend I stumbled across a &lt;a href=&quot;https://www.ioforth.com/support/things-about-vodpod.htm&quot;&gt;video link&lt;/a&gt;, released (I assume) by Flickr. The video is a visualisation of the last 7 years of commits into the Flickr Subversion repository. Wow, there&#39;s a lot of work been done to Flickr over the past 7 years! What&#39;s even more interesting is you can easily create the same type of visualisation with your own project using an open-source project called Gource.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://code.google.com/p/gource/&quot;&gt;Gource&lt;/a&gt; is a version control visualisation tool developed by Andrew Caudwell. At the moment it supports Git, Mercurial and Bazaar but it is also possible to use it with Subversion (SVN) with a few extra steps. I&#39;ve created a video for a project I&#39;ve been working on with another developer for 9+ months. The video isn&#39;t as active as the Flickr one but its still interesting to watch and see how a project develops right before your eyes.&lt;/p&gt;&lt;div class=&quot;iframe-container&quot;&gt;&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;360&quot; mozallowfullscreen=&quot;&quot; src=&quot;https://player.vimeo.com/video/11986153?title=0&amp;byline=0&amp;portrait=0&quot; webkitallowfullscreen=&quot;&quot; width=&quot;640&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;&lt;a href=&quot;https://vimeo.com/11986153&quot;&gt;Gource Visualisation&lt;/a&gt; from &lt;a href=&quot;https://vimeo.com/user3604111&quot;&gt;Matt Hobbs&lt;/a&gt; on &lt;a href=&quot;https://vimeo.com&quot;&gt;Vimeo&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The videos produced by Gource really illustrate how much work goes into projects over time, with each developer &#39;shooting a laser&#39; at each file created / changed / deleted. Files in the same folder are clustered together, lines in-between these clusters link them together depending on the folder structure.&lt;/p&gt;&lt;p&gt;Creating the videos is fairly simple, I&#39;ll give you a step by step guide how to do it using an SVN repository below. You will need:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;http://code.google.com/p/gource/downloads/list&quot;&gt;Gource&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://subversion.apache.org/packages.html#windows&quot;&gt;Command line SVN client&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://www.python.org/download/releases/3.1.2/&quot;&gt;Python&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://www.fraps.com/download.php&quot;&gt;Fraps&lt;/a&gt; (optional)&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://virtualdub.sourceforge.net/&quot;&gt;VirtualDub&lt;/a&gt; (optional)&lt;/li&gt;&lt;li&gt;SVN repository&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;First you will want to grab a copy of Gource and extract the zip file to somewhere easily accessible. I&#39;ve used &lt;code&gt;c:&#92;gource&lt;/code&gt; as you will be using the command line so it&#39;s simple to find. Extract Python into a similar directory, I used &lt;code&gt;c:&#92;python31&lt;/code&gt;. If you don&#39;t have the SVN command line client installed, install it and note down where it&#39;s installed too.&lt;/p&gt;&lt;p&gt;Next you need to export the SVN repository log file to an XML document. To do this open a command prompt and locate the SVN client directory. Now type:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;svn log http://www.yourrepository.url/svn/ &lt;span class=&quot;token parameter variable&quot;&gt;--verbose&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--xml&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; projectlog.log&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This command has exported the SVN log file to an XML doc that you should see in your SVN client directory. Since Gource doesn&#39;t support SVN natively, the log file needs to be converted into a format Gource will understand. To do this copy the XML log file and this &lt;a href=&quot;http://gource.googlecode.com/files/svn-gource-1.2.tar.gz&quot;&gt;python script&lt;/a&gt; into the python directory. To convert the log file type the following from the command line in the python directory:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;python svn-gource.py --filter-dirs projectlog.log &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; my-project-gource.log&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Great! Now time for the fun part! You now have a log file (my-project-gource.log) that Gource can understand, so copy it to the Gource directory and bring up the command line. To generate the video created above I used the following command line:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gource --log-format custom my-project-gource.log &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--hide&lt;/span&gt; filenames --stop-at-end --disable-progress &lt;span class=&quot;token parameter variable&quot;&gt;-1280x720&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; --disable-progress &lt;span class=&quot;token parameter variable&quot;&gt;--hide&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;date&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The command line is pretty self explanatory, I&#39;ve set the size of the video I want, and hidden a few elements that weren&#39;t needed. The &lt;code&gt;-s 0.1&lt;/code&gt; is an important command as it sets how quickly the visualisation runs through the log file. It&#39;s worth experimenting with different values until you are happy with the results.&lt;/p&gt;&lt;p&gt;If you look in the README.TXT file thats in the Gource directory it gives you a whole list of options that are available to experiment with, there&#39;s hours of fun in there if you really want delve into Gource.&lt;/p&gt;&lt;h3 id=&quot;saving-the-video&quot;&gt;Saving the video&lt;/h3&gt;&lt;p&gt;This part actually took me longer to complete than generating the video in Gource; due to a few video encoding issues I was having (Grr!). Capturing the video is simple (once you know how) and uses a program called Fraps. Fraps is usually used in the online gaming scene, it is used to capture full-screen video and save it into an (uncompressed) video format. Once installed it will sit in your taskbar waiting for a full-screen video to kick in which it can then capture.&lt;/p&gt;&lt;p&gt;Play your Gource video from the command line in full-screen and press F9, this is the hotkey that triggers Fraps to start recording. Once complete, your video will be saved into the &lt;code&gt;&#92;fraps&#92;movies&lt;/code&gt; directory. The video that has been saved is in an uncompressed format so it&#39;s likely it will be a large file (mine was around 700MB for 2 mins at 1280 x 720). This size isn&#39;t very practical so it&#39;s time to convert it to a smaller more portable format. For this we use the excellent VirtualDub by Avery Lee.&lt;/p&gt;&lt;p&gt;Once VirtualDub is installed it only takes a couple of steps to compress the video down:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Run VirtualDub and open your Fraps video file&lt;/li&gt;&lt;li&gt;From the Video menu select &#39;Compression&#39;&lt;/li&gt;&lt;li&gt;Choose the codec / compression settings you wish to use (I used the XviD codec single pass)&lt;/li&gt;&lt;li&gt;From the file menu select &#39;Save as AVI&#39;, once compressed you are done!&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;As simple as that! The file produced will be much smaller which you can then upload to Youtube / Vimeo as you see fit. Gource may not have a huge number or practical uses other than eye candy, but it&#39;s a superb little tool that I will be using again in the future.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>NextGEN Gallery + WordPress custom fields = happy developer</title>
    <link href="https://nooshu.com/blog/2010/05/20/nextgen-gallery-wordpress-custom-fields-happy-developer/" />
    <updated>2010-05-20T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/05/20/nextgen-gallery-wordpress-custom-fields-happy-developer/</id>
    <content type="html">&lt;p&gt;Over the past couple of weeks I&#39;ve been working on a number of WordPress projects, all of which have needed some basic photo gallery functionality. As I&#39;ve mentioned before in &lt;a href=&quot;https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/&quot;&gt;previous posts&lt;/a&gt;, &lt;a href=&quot;http://wordpress.org/extend/plugins/nextgen-gallery/&quot;&gt;NextGEN gallery&lt;/a&gt; is my WordPress gallery of choice, there really is nothing that comes close to it in terms of features and customisation (that I know of, if there are others post a comment!).&lt;/p&gt;&lt;p&gt;To add a gallery (or album) to a post / page is as simple as adding &lt;code&gt;[ nggallery id=x ]&lt;/code&gt; to the post content... super! One issue though... what happens when you allow a client to edit and add new pages; well what usually happens is the tag gets deleted or changed, the gallery breaks, client panics and the virtual world ends (well not quite, but close). A technique I have been using to help solve this issue involves using WordPress custom fields and the excellent &lt;a href=&quot;http://wordpress.org/extend/plugins/get-custom-field-values/&quot;&gt;Get custom field values plug-in&lt;/a&gt;. You can grab the custom field values directly from WordPress using &lt;code&gt;get_post_meta()&lt;/code&gt; but the plug-in makes it much easier.&lt;/p&gt;&lt;p&gt;Digging through the nggfunctions.php file in the NextGEN gallery plug-in folder reveals a whole host of useful functions that can be easily called directly from inside your WordPress theme. So say you wanted a gallery in a post page, but didn&#39;t want the client to have to remember &lt;code&gt;[ nggallery id=x ]&lt;/code&gt; every time, simply add this code next to your content in the single.php file:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;c2c_get_current_custom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;galleryID&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$gID&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;c2c_get_current_custom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;galleryID&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;nggShowGallery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$gID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;endif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Add a custom field of galleryID with an integer value linking to the relevant gallery and you&#39;re done. Now the client can pick out the galleryID custom field from a drop-down since WordPress remembers the custom fields used. Much easier for a client to remember than adding the tag every time.&lt;/p&gt;&lt;p&gt;I highly recommend looking through the nggfunctions.php file, here are just a few functions that are available:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;nggShowAlbum()&lt;/strong&gt;: Show a whole album depending on the ID.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;nggSinglePicture()&lt;/strong&gt;: Show a single picture from an ID.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;nggShowRandomRecent()&lt;/strong&gt;: Show random image(s) from a certain gallery.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;nggShowSlideshow()&lt;/strong&gt;: Show a flash slideshow pulling images from a specified gallery.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I have used nggShowRandomRecent() quite a few times in the past few months, here&#39;s a quick example:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Show 6 random images using &#39;my-template&#39; from gallery 4&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//(order, number of images, template, gallery ID)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;nggShowRandomRecent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;random&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;my-template&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Pick 6 images at random from a selected gallery on page load, very simple and effective.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: When using the random images function make sure you don&#39;t have any WordPress caching plug-ins running on the selected page, else it won&#39;t work as the page gets cached on first load. It took me a good 15 minutes to figure out why the function had suddenly stopped working due to this slight oversight.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Wolf CMS: A fork of Frog CMS</title>
    <link href="https://nooshu.com/blog/2010/05/09/wolf-cms-a-fork-of-frog-cms/" />
    <updated>2010-05-09T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/05/09/wolf-cms-a-fork-of-frog-cms/</id>
    <content type="html">&lt;p&gt;I&#39;ve written about my &lt;a href=&quot;http://www.madebyfrog.com/&quot;&gt;Frog CMS&lt;/a&gt; usage in &lt;a href=&quot;https://nooshu.com/blog/2010/02/03/frog-cms-nice-and-simple-cms/&quot;&gt;previous blog posts&lt;/a&gt;; where other CMS&#39;s can be a little overkill for small websites, Frog CMS fits perfectly into the gap. Unfortunately as much as I like Frog, the development seems to have stopped. The last stable version was released on the 26th April 2009, over a year ago. There&#39;s no need to panic though, a development fork has been created called &lt;a href=&quot;http://www.wolfcms.org/&quot;&gt;Wolf CMS&lt;/a&gt; which is building upon this superb little CMS and making it even better!&lt;/p&gt;&lt;p&gt;At the moment there&#39;s very little difference between Frog and Wolf as you might expect, so migrating from one to the other is fairly simple, a wiki page has been created with instructions on &lt;a href=&quot;http://www.wolfcms.org/wiki/migrating_from_frog_to_wolf_cms&quot;&gt;how to do this&lt;/a&gt;. As it says on the page it&#39;s probably best to decide between the two versions now since there&#39;s still little difference between them, but this could change in future versions (version 0.6.0 introduced a number of large changes). So there may be a point where it isn&#39;t (easily) possible to jump between the two versions.&lt;/p&gt;&lt;p&gt;So what are the differences at the moment? When version 0.6.0 of Wolf CMS was released on the 1st February it added a new core plug-in called &#39;BackupRestore&#39;, allowing admin users to easily backup the Wolf CMS core DB tables. I&#39;ve been using an external plug-in to do this on sites I built, so having this feature added as a core plug-in is nifty addition. Other features include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Admin users can now uninstall plug-ins, including the db tables&lt;/li&gt;&lt;li&gt;HTTPS Support added to the admin area for greater security&lt;/li&gt;&lt;li&gt;You can now preview a page before it is published&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&#39;s great to see that such a useful little CMS hasn&#39;t been left to stagnate and die out, the &lt;a href=&quot;http://www.wolfcms.org/about/project-roadmap.html&quot;&gt;roadmap&lt;/a&gt; for Wolf CMS looks promising, so fingers crossed it has a bright future ahead of it. I think it&#39;s time to migrate my Frog websites over to Wolf...;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Aptana on Steroids using Zen Coding</title>
    <link href="https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/" />
    <updated>2010-05-04T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/</id>
    <content type="html">&lt;p&gt;I&#39;ve mentioned before in previous articles that &lt;a href=&quot;http://www.aptana.org/&quot;&gt;Aptana&lt;/a&gt; is my IDE of choice; one which I&#39;ve been using for a few years. I only use a handful of the many features available in Aptana, all of which are also available in other editors (&lt;a href=&quot;http://notepad-plus.sourceforge.net/uk/site.htm&quot;&gt;Notepad++&lt;/a&gt; being one), but I&#39;ve got so used to the way it works I&#39;m sticking with it for the moment.&lt;/p&gt;&lt;p&gt;One of Aptana&#39;s helpful features is the ability to generate code using the easy to access menu bar at the top of the code window (see image):&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Aptana has a host of code helpers available&quot; decoding=&quot;async&quot; height=&quot;176&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/jbWBfbo4po-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/jbWBfbo4po-300.webp 300w, https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/jbWBfbo4po-600.webp 600w, https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/jbWBfbo4po-814.webp 814w&quot; width=&quot;814&quot; title=&quot;Generate and wrap code at the touch of a button&quot;&gt;&lt;/p&gt;&lt;p&gt;A superb feature it must be said, but it is totally eclipsed when you start using the &lt;a href=&quot;http://code.google.com/p/zen-coding/&quot;&gt;Zen Coding JavaScript Library&lt;/a&gt;. Zen Coding, written by Sergey Chikuyonok isn&#39;t a JavaScript Library in the traditional sense, it isn&#39;t uploaded and linked to in your web page; it&#39;s installed as a plug-in for your selected &lt;a href=&quot;http://code.google.com/p/zen-coding/downloads/list&quot;&gt;text editor&lt;/a&gt;. As you can see from &lt;a href=&quot;http://code.google.com/p/zen-coding/downloads/list&quot;&gt;the list&lt;/a&gt; there are many versions available, so you aren&#39;t restricted to only Aptana.&lt;/p&gt;&lt;p&gt;Let me explain what it does first, then I&#39;ll show you how to install it (for Aptana). As a Front End Web Developer you will often find you&#39;re writing out the same blocks of code again and again. You can just copy / paste then modify, but even that&#39;s quite slow and tedious. Zen Coding uses a technology that you will be very familiar with: CSS selectors. It may seem confusing at first, but once you see it in action it makes perfect sense. Here are a few code examples I use all the time:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
Entering
ul#mainNav&gt;li*5&gt;a
generates the list below
--&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;mainNav&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Using simple CSS selectors we&#39;ve been able to quickly generate an unordered list that can be used for example, as a page navigation. You can even use more advanced selectors like siblings and abbreviated groups to generate more advanced HTML:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
Entering
div#wrapper&gt;div#header+div#content+div#footer
generates this div set below
--&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;wrapper&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;header&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;footer&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--
Or a more advanced example
div#content&gt;(div.primary-section&gt;h2.header+p+ul&gt;li*5)+(div.secondary-section&gt;h3+p)
generates the sections below
--&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;primary-section&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h2&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;header&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;secondary-section&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It may take a little while to get into the routine, it&#39;s all to easy to slip back into typing the code by hand; but once you do you can build the basic mark-up of a page in a matter of minutes.&lt;/p&gt;&lt;p&gt;There&#39;s a very informative article on &lt;a href=&quot;http://www.smashingmagazine.com/2009/11/21/zen-coding-a-new-way-to-write-html-code/&quot;&gt;Smashing Magazine&lt;/a&gt; from last year which you may find useful if you plan on investigating Zen Coding further; I highly recommend adding it to your &quot;toread&quot; list. There are so many more functions available that I haven&#39;t even touched on in this article, you will be amazed.&lt;/p&gt;&lt;p&gt;Now that you&#39;ve seen Zen Coding in action you may be wondering how you install it for use with Aptana. Well luckily it&#39;s dead simple. First create a &#39;new project&#39; in Aptana, call it whatever you like e.g. &#39;Zen Coding&#39;:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Simple to setup Aptana with Zen Coding&quot; decoding=&quot;async&quot; height=&quot;295&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/SPDUAnabUE-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/SPDUAnabUE-300.webp 300w, https://nooshu.com/blog/2010/05/04/aptana-on-steroids-using-zen-coding/SPDUAnabUE-600.webp 600w&quot; width=&quot;600&quot;&gt;&lt;/p&gt;&lt;p&gt;Within the new project create a scripts directory and copy the Zen Coding JavaScript into into the directory. That&#39;s it you&#39;re done! Restart Aptana and start using Zen Coding. Once you&#39;ve typed in a string of CSS selectors you wish to convert to HTML, hit CTRL + E (Expand Abbreviation) and it will do the rest for you. One thing to note, you must keep the new project you&#39;ve just created open at all times, else Aptana won&#39;t be able to access the scripts.&lt;/p&gt;&lt;p&gt;You can customise the key you use expand an abbreviation by editing the &#39;Key&#39; value in &#39;Expand Abbreviation.js&#39;. For more information on available keycodes see the &lt;a href=&quot;http://docs.aptana.com/docs/index.php/Eclipse_Monkey_scripting_with_Ruby#Adding_metadata_to_your_script&quot;&gt;Aptana documentation here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Hopefully you find Zen Coding useful, it&#39;s certainly changed the way I code on a day to day basis.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Recommended books for Front End Web Developers</title>
    <link href="https://nooshu.com/blog/2010/04/27/recommended-books-for-front-end-web-developers/" />
    <updated>2010-04-27T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/04/27/recommended-books-for-front-end-web-developers/</id>
    <content type="html">&lt;p&gt;So you&#39;ve decided to persue a career as a Front End Web Developer (or you already are one and are looking for something new to read); smashing! The life of a Front End Developer is never dull... (cough) okay sometimes it is but there are certainly some exciting technologies out there to play with, especially at the moment with HTML5 and CSS3 gaining popularity.&lt;/p&gt;&lt;p&gt;Like with anything, you need to have a solid foundation of knowledge to build upon when it comes to new technologies, sometimes it&#39;s a little dull but it will pay dividends in the future. Once you have the basic knowledge the world is you web browser. So here are a list of five books I&#39;ve read in the past that I&#39;ve found particularity helpful.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Looking for new Front End books to read, you could try one of these.&quot; decoding=&quot;async&quot; height=&quot;565&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/04/27/recommended-books-for-front-end-web-developers/e5nvLtwZ-S-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/04/27/recommended-books-for-front-end-web-developers/e5nvLtwZ-S-300.webp 300w, https://nooshu.com/blog/2010/04/27/recommended-books-for-front-end-web-developers/e5nvLtwZ-S-600.webp 600w, https://nooshu.com/blog/2010/04/27/recommended-books-for-front-end-web-developers/e5nvLtwZ-S-814.webp 814w&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;dom-scripting&quot;&gt;DOM Scripting&lt;/h3&gt;&lt;p&gt;The reason I&#39;ve put this book first is simply because it&#39;s one of the best technical books I&#39;ve ever read. The way it&#39;s written is simple to understand and concise. All the way through the book it explains not only what you are doing but also &lt;strong&gt;why&lt;/strong&gt; you&#39;re doing it, and why you should also follow the same methodology.&lt;/p&gt;&lt;p&gt;If you are a complete beginner to JavaScript and the DOM this book will get you up and running and understanding in just a couple of chapters. Now some may say &quot;oh I don&#39;t need to know this anymore, that&#39;s what JavaScript librarys are for&quot;, but in my opinion that&#39;s a dangerous road to go down. If ever your chosen library doesn&#39;t do what you need, or if you can&#39;t use a library for whatever reason; then you&#39;re in trouble.&lt;/p&gt;&lt;p&gt;If you know the basics of the DOM and how to manipulate it using JavaScript, you&#39;re much more likely to solve any issues you encounter and you&#39;ll have a much better understanding of what the libraries are actually doing.&lt;/p&gt;&lt;p&gt;Author: Jeremy Keith (&lt;a href=&quot;http://www.amazon.co.uk/DOM-Scripting-Design-JavaScript-Document/dp/1590595335&quot;&gt;Amazon&lt;/a&gt;).&lt;/p&gt;&lt;h3 id=&quot;eric-meyer-on-css&quot;&gt;Eric Meyer on CSS&lt;/h3&gt;&lt;p&gt;There simply couldn&#39;t be a review of front end development books without a book from the wizard of CSS, Eric Meyer. Eric has been a major part of the CSS community for many years; helping to educate developers in the proper usage of CSS and how it can make development so (so!) much easier. I had the pleasure of attending a 2 day workshop of his in London a few years back, and still use what I learnt there on a day to day basis.&lt;/p&gt;&lt;p&gt;&#39;Eric Meyer on CSS&#39; will teach you some of the advanced techniques involved in using CSS in a practical manner. You take a plain website built using tables, clean up the mark-up and layer on features and functionality using CSS over the various chapters; so rather than just getting a list of properties and selectors to read about, you actually get to see a live project changing over time.&lt;/p&gt;&lt;p&gt;Be warned, the book isn&#39;t for absolute beginners, you do really need to know the basics of HTML and CSS to get the most out of it. Not to worry though, all the information you need to learn the basics of many different languages including HTML and CSS is available online at &lt;a href=&quot;http://www.w3schools.com/&quot;&gt;w3schools&lt;/a&gt;. You don&#39;t have to stick to HTML and CSS, they also have JavaScript and the DOM if you&#39;re feeling adventurous.&lt;/p&gt;&lt;p&gt;Author: Eric Meyer (&lt;a href=&quot;http://www.amazon.com/Eric-Meyer-CSS-Mastering-Language/dp/073571245X&quot;&gt;Amazon&lt;/a&gt;)&lt;/p&gt;&lt;h3 id=&quot;javascript-the-definitive-guide&quot;&gt;JavaScript - The Definitive Guide&lt;/h3&gt;&lt;p&gt;The very large book you can see in the image above is &#39;JavaScript - The Definitive Guide&#39;, and it really is a definitive guide. It has of 900+ pages of wonderful JavaScript! Now I wouldn&#39;t expect anyone to read it page by page (although that&#39;s what I did), but it&#39;s defiantly a book you should have in your tool kit for reference. It goes over pretty much all aspects of JavaScript you will ever use and even though it is quite technical it isn&#39;t hard to follow; with plenty of examples and explanation of the code.&lt;/p&gt;&lt;p&gt;Again there&#39;s an argument for do you need to know JavaScript if you just intend on using a library. Well I guess that up to you, but I personally don&#39;t feel comfortable using any sort of framework / abstraction without having at least some understanding of the language it is built on.&lt;/p&gt;&lt;p&gt;Author: David Flanagon (&lt;a href=&quot;http://www.amazon.com/JavaScript-Definitive-Guide-Activate-Pages/dp/0596805527/ref=sr_1_1?ie=UTF8&amp;s=books&amp;qid=1272375736&amp;sr=1-1&quot;&gt;Amazon&lt;/a&gt;)&lt;/p&gt;&lt;h3 id=&quot;learning-jquery&quot;&gt;Learning jQuery&lt;/h3&gt;&lt;p&gt;So assuming you know about HTML, CSS and JavaScript it&#39;s time to learn one of the many JavaScript libraries available. Now I agree with Christian Heilmann on a point he made at Full Frontal 2009 (I think it was Christian who made it), it doesn&#39;t matter what library you use, as long as you use one!&lt;/p&gt;&lt;p&gt;&#39;Learning jQuery&#39; is a fantastic book that will take you through all aspects of jQuery, from setting up and writing basic code to Ajax and writing your own jQuery plug-ins. If you know how to use CSS to select parts of the DOM then jQuery will be simple for you to pick up and use. The book is filled with practical examples and applications, all using best practice methods.&lt;/p&gt;&lt;p&gt;I only had one slight gripe with the book while reading it; I found the font for the code blocks was slightly too big, so you tended to get a lot of wrapping on large code blocks. In terms of the content though, it really is one book that is a must read if you use jQuery or want to use jQuery in your projects.&lt;/p&gt;&lt;p&gt;Author: Jonathan Chaffer &amp; Karl Swedberg (&lt;a href=&quot;http://www.amazon.com/Learning-jQuery-1-3-Jonathan-Chaffer/dp/1847196705/ref=sr_1_1?ie=UTF8&amp;s=books&amp;qid=1272376954&amp;sr=8-1&quot;&gt;Amazon&lt;/a&gt;)&lt;/p&gt;&lt;h3 id=&quot;php-solutions&quot;&gt;PHP Solutions&lt;/h3&gt;&lt;p&gt;Now I know what you may be thinking, PHP isn&#39;t a front end language, it&#39;s a server side language. That is very true, but one thing I can guarantee is one day you will have to integrate the front end templates you&#39;ve built into a server side language; be that PHP, .Net, JSP, Java etc. As I&#39;ve mainly worked with open source technologies, PHP is primarily what I integrate into.&lt;/p&gt;&lt;p&gt;&#39;PHP Solutions&#39; is another excellent Friends of Ed book that takes you through lots of practical uses of PHP that you can use straight out of the book. It covers subjects such as setting up your server and includes, to online galleries and security. You may not use any of the functionality it covers since most frameworks and CMS&#39;s have the functionality built in; but it will give you an understanding of how PHP works as a language which in turn will help you when it comes to integration.&lt;/p&gt;&lt;p&gt;Author: David Powers (&lt;a href=&quot;http://www.amazon.co.uk/PHP-Solutions-Dynamic-Design-Made/dp/1590597311&quot;&gt;Amazon&lt;/a&gt;)&lt;/p&gt;&lt;p&gt;So there you have 5 books I&#39;d recommend to any Front End Web Developer looking to expand their client-side knowledge. Leave a comment if there are any books you&#39;d recommend and I&#39;ll take a look and add it to my Amazon wishlist.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Adding Content using CSS3</title>
    <link href="https://nooshu.com/blog/2010/04/20/adding-content-using-css3/" />
    <updated>2010-04-20T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/04/20/adding-content-using-css3/</id>
    <content type="html">&lt;p&gt;CSS3 is an exciting new browser technology, it&#39;s implementation is improving with every new browser release. Microsoft is adding a whole host of CSS3 selectors to the their next version of Internet Explorer, version 9. Until IE9 is released and the vast majority of users start using it, (2016 maybe?) it&#39;s a case of having to use CSS3 quite cautiously.&lt;/p&gt;&lt;p&gt;Using the &#39;progressive enhancement&#39; methodology, we build a website that works in all browsers first, then &#39;layer on&#39; the cool features and &#39;nice to haves&#39; after, so users with modern browsers get the full site experience and older browsers still have access to all the content. CSS3 sits in the &#39;nice to have&#39; category for the moment.&lt;/p&gt;&lt;p&gt;While looking through the &lt;a href=&quot;http://www.w3.org/TR/css3-content/&quot;&gt;W3C CSS3 Working Draft&lt;/a&gt; I came across a section called &#39;Inserting content into an element&#39;. The CSS3 pseudo-elements listed here are used to add content to the page using CSS. Cool! ....wait a second; for years we&#39;ve been separating layout from content, allowing us to easily edit a whole website layout from a single file. The pseudo-elements seem to be going against this mantra! Uh ohh! It&#39;s controversial, but I believe if used responsibly the pseudo-elements shouldn&#39;t cause any major accessibility issues.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;::before &amp; ::after&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The ::before and ::after pseudo-elements are used to add content before and after the content inside the selected element (surprising huh!):&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;#element::before&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Paradox: &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#element::after&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot; Now my head hurts.&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;View a &lt;a href=&quot;https://nooshu.com/lab/experiments/css3-selectors/&quot;&gt;demo here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The specifications even allow you to add more content by iterating the pseudo-elements, so ::after::after (or ::after(2)) but this isn&#39;t supported in current browsers I have tested. I&#39;m personally hoping that part of the specification isn&#39;t implemented, as you can have too much of a good thing. You could end up in the nightmare situation of pages completely generated using CSS3 using multiple levels of ::before and ::after.&lt;/p&gt;&lt;p&gt;The CSS3 specification has also outlined the pseudo-element ::outside but it hasn&#39;t been implemented yet in the current version of Firefox I&#39;m using (3.6.3).&lt;/p&gt;&lt;p&gt;Outline will allow you to wrap an element inside another element and then add styling to it. Usually you&#39;d modify the source code by adding extra divs to do this, but it isn&#39;t always possible. Outline would have been very useful over the past couple of years for adding &#39;sliding doors&#39; and rounded corners where needed, without having to clutter up the mark-up with &#39;for style&#39; only tags. Oh well it&#39;s a tool for the future.&lt;/p&gt;&lt;p&gt;I can&#39;t say I&#39;m a big fan of these new pseudo-elements as they seem to be stepping on HTML&#39;s toes. It&#39;s hard to think of a situation where you&#39;d want the CSS to be writing content to the pages since it isn&#39;t accessible to screen readers or search engines, but I&#39;m sure there are some. The only example that springs to mind is when you add a &#39;:&#39; after the label on a form field, you don&#39;t want a screen reader to read out the &#39;colon&#39; so you add it using CSS.&lt;/p&gt;&lt;p&gt;I&#39;d love to hear where other people plan on using ::before and ::after, or where you are using them at the moment.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>CodeColorer Auto Expanding Code</title>
    <link href="https://nooshu.com/blog/2010/04/16/codecolorer-auto-expanding-code/" />
    <updated>2010-04-16T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/04/16/codecolorer-auto-expanding-code/</id>
    <content type="html">&lt;p&gt;Just quick post on a small feature I&#39;ve just added to the site which I thought I&#39;d share. It&#39;s a slight addition to the &lt;a href=&quot;http://wordpress.org/extend/plugins/codecolorer/&quot;&gt;CodeColourer plug-in&lt;/a&gt; for WordPress. CodeColourer formats and colourises code blocks in your blog posts for better readability. It comes with several different styles built in, or you can customise it using your own CSS file.&lt;/p&gt;&lt;p&gt;A feature that I saw on another site (although I can&#39;t remember which) was when a user rolled over a code box it would expand to fit the code, allowing the user to see everything. Cool little addition, so I decided to create my own using jQuery. I&#39;ve taken some of the code from the plug-in and pasted it below so you can see it in action:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.codecolorer-container&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; $code &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.codecolorer-container&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Animation decision object&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; decisionObject &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Original width / height of the displayed code&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; originalWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; originalHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Width / height of hidden portion of code&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; mainWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.codecolorer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; mainHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.codecolorer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; lineWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.line-numbers&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;//Only attach events if needed (ie has scroll bars)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mainWidth &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; lineWidth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; originalWidth &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; mainHeight &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; originalHeight&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;/* and so on...... */&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since this action is fired on hover, I didn&#39;t want the code boxes expanding immediately every time a user hovers over a box; as just scrolling down the page would expand the boxes which would become extremely annoying. The boxes should only expand when the cursor is left over the box; lucky there&#39;s a plug-in for jQuery that already adds this functionality, it&#39;s called &lt;a href=&quot;http://blog.threedubmedia.com/2008/08/eventspecialhover.html&quot;&gt;$.event.special.hover&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Special hover simply replaces the standard hover event with one that monitors the speed of the cursor over a set period of time. If the cursor speed drops below the threshold the hover event fires. This is enough to stop random hover events firing when a user navigates over the page, simple!&lt;/p&gt;&lt;p&gt;I&#39;d be interested to see if the code works for others using &lt;a href=&quot;http://wordpress.org/extend/plugins/codecolorer/&quot;&gt;CodeColorer&lt;/a&gt;, there&#39;s no reason why it shouldn&#39;t.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress plug-in: Post Thesaurus</title>
    <link href="https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/" />
    <updated>2010-04-14T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This plug-in is no longer maintained.&lt;/p&gt;&lt;p&gt;Over the past few days I&#39;ve been working on a new WordPress plug-in, one that I personally have desperately needed recently. It&#39;s called &#39;Post Thesaurus&#39; and it does exactly what you&#39;d expect it to do. It creates a little widget on the side of the &#39;Add new post&#39; page which you can use to suggest new words of the same meaning.&lt;/p&gt;&lt;p&gt;After finding I use the word &#39;great&#39; a little too much, I thought it was about time to do something about it. Here are a few screenshots of the plug-in in action:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Post Thesaurus before I&#39;ve searched for a set of words.&quot; decoding=&quot;async&quot; height=&quot;399&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/v1smsAZ_kk-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/v1smsAZ_kk-300.webp 300w, https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/v1smsAZ_kk-600.webp 600w, https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/v1smsAZ_kk-814.webp 814w&quot; title=&quot;Notice the widget in the top right corner.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Post Thesaurus after I&#39;ve completed a word search&quot; decoding=&quot;async&quot; height=&quot;399&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/XYN5XkaAma-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/XYN5XkaAma-300.webp 300w, https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/XYN5XkaAma-600.webp 600w, https://nooshu.com/blog/2010/04/14/wordpress-plug-in-post-thesaurus/XYN5XkaAma-814.webp 814w&quot; title=&quot;The widget has completed a search and populated the results panel.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Big thank you to &lt;a href=&quot;http://words.bighugelabs.com/&quot;&gt;Big Huge Labs&lt;/a&gt; for providing the excellent &lt;a href=&quot;http://words.bighugelabs.com/api.php&quot;&gt;API&lt;/a&gt;. I&#39;ve included an admin page with a few settings (under &#39;settings&#39;). One feature to note is the ability to enter your own API key. I&#39;ve added this just in case the plug-in starts exceeding the 10,000 requests per day a single API key is allowed. &lt;a href=&quot;http://words.bighugelabs.com/getkey.php&quot;&gt;Sign up is simple&lt;/a&gt; and only takes a couple of minutes, then you&#39;ll have 10,000 requests per day all to yourself.&lt;/p&gt;&lt;p&gt;I&#39;ve implemented the API using a little jQuery Ajax goodness and some JSONP. Since jQuery is used by the WordPress admin by default, there&#39;s no additional overhead in having to add it manually.&lt;/p&gt;&lt;p&gt;Grab the plug-in off the WordPress site &lt;a href=&quot;http://wordpress.org/extend/plugins/post-thesaurus/&quot;&gt;here&lt;/a&gt; &lt;small&gt;(Version 1.0.0.0)&lt;/small&gt;.&lt;/p&gt;&lt;p&gt;I hope you find it useful.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Shadowbox and NextGEN Gallery Sitting in a Tree</title>
    <link href="https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/" />
    <updated>2010-04-07T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/</id>
    <content type="html">&lt;p&gt;A project I&#39;m currently working on requires a photo gallery, luckily with WordPress there are quite a few to choose from. Of the gallery plug-ins I&#39;ve used &lt;a href=&quot;http://alexrabe.de/wordpress-plugins/nextgen-gallery/&quot;&gt;NextGEN Gallery&lt;/a&gt; is without doubt my favorite, chocked full of useful features and easy to use; you could even let a client use it! My only issue is the way the images appear in a &#39;lightbox&#39; when you click on them, the &#39;lightbox&#39; is great, but it&#39;s implementation seems a little clunky.&lt;/p&gt;&lt;p&gt;By default NextGEN adds a chunk of code to the WordPress header which is one of my pet hates, that has to go. NextGEN uses &lt;a href=&quot;http://jquery.com/demo/thickbox/&quot;&gt;ThickBox&lt;/a&gt;, which is great, but as it says on the website &quot;Thickbox had its day&quot; so it&#39;s time to use an alternative. One that&#39;s really caught my eye is &lt;a href=&quot;http://www.shadowbox-js.com/&quot;&gt;Shadowbox.js&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Shadowbox.js is a fully featured media viewer that supports the webs most popular formats (images, Flash, QuickTime, Windows Media Video and FLV via JW FLV Player). Pretty much every format you really need. It also has the added bonus of being available either as a standalone script or integrated with your favourite JavaScript library (jQuery, Prototype, MooTools, Dojo, YUI and Ext). For me personally, it ticks every box... and it also looks great and works well!&lt;/p&gt;&lt;p&gt;So now time to bring the two together. First you should install the NextGEN gallery plug-in which you may already have it installed. Next you need view the gallery &#39;options&#39; and select the &#39;Effects&#39; tab. Change the &quot;JavaScript Thumbnail effect:&quot; to Custom and add the following code to the text box:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;rel=&quot;shadowbox[%GALLERY_NAME%]&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img alt=&quot;Settings to change in NextGEN gallery&quot; decoding=&quot;async&quot; height=&quot;469&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/Y71beevrO6-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/Y71beevrO6-300.webp 300w, https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/Y71beevrO6-600.webp 600w, https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/Y71beevrO6-814.webp 814w&quot; title=&quot;Change a couple of setting in the WordPress admin.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The code will be added to each thumbnail generated by the gallery. Changing the dropdown to custom also removes various bits of default NextGEN code from the header, solving that problem too.&lt;/p&gt;&lt;p&gt;Next it&#39;s time to &lt;a href=&quot;http://www.shadowbox-js.com/download.html&quot;&gt;download Shadowbox.js&lt;/a&gt;. You have a few options on this page, as I only want to display images I selected &#39;Base (standalone)&#39;, checked the images checkbox and unchecked &quot;Include support for using CSS selectors to select links&quot;. I don&#39;t plan on using shadowbox with jQuery so I didn&#39;t add the jQuery adapter, this also keeps the filesize to a minimum.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Simple Shadowbox download page&quot; decoding=&quot;async&quot; height=&quot;836&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/PYeZZ9UbKe-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/PYeZZ9UbKe-300.webp 300w, https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/PYeZZ9UbKe-600.webp 600w, https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/PYeZZ9UbKe-814.webp 814w&quot; title=&quot;Check your preferred settings and download.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Hit the download button to download the ZIP. Once downloaded extract the CSS, JS and images into your WordPress theme directory. All thats left to do now is add a little code to your theme.&lt;/p&gt;&lt;p&gt;In your header.php add:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;stylesheet&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&amp;lt;?php bloginfo(&#39;stylesheet_directory&#39;); ?&gt;/shadowbox.css&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or what I prefer to do is copy the CSS from shadowbox.css into my main style.css file, this way you keep everything in one place and you minimise HTTP requests. Then add the JavaScript to your footer.php (or header.php, but I prefer to put all JS in the footer.php).&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;is_single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bloginfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;stylesheet_directory&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;/js/shadowbox.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/javascript&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/javascript&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;Shadowbox&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;endif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note the use of the &#39;is_single()&#39; WordPress conditional tag. Since I&#39;m only planning to use Shadowbox on an individual blog post I only call it on those pages. Feel free to remove this if you plan on using it elsewhere on your site.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Shadowbox and NextGEN gallery&quot; decoding=&quot;async&quot; height=&quot;795&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/UBfgx-Lq2h-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/UBfgx-Lq2h-300.webp 300w, https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/UBfgx-Lq2h-600.webp 600w, https://nooshu.com/blog/2010/04/07/shadowbox-and-nextgen-gallery-sitting-in-a-tree/UBfgx-Lq2h-814.webp 814w&quot; title=&quot;Great looking effects that work really well alongside NextGEN gallery.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once those bits are added you&#39;re all done! If you don&#39;t see the any open, close, next, previous images make sure you are pointing to them correctly in your CSS file (since they are background images). Hurrah! Enjoy your new improved version of NextGEN Gallery complete with Shadowbox.js.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>jQuery Plug-in: Tab Down</title>
    <link href="https://nooshu.com/blog/2010/04/02/jquery-plug-in-tab-down/" />
    <updated>2010-04-02T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/04/02/jquery-plug-in-tab-down/</id>
    <content type="html">&lt;p&gt;This simple plug-in has been on my TODO list for quite a while, as I&#39;ve seen the effect used on many websites. It seems particularly useful for displaying contact details / social media updates. I&#39;m sure there are many similar plug-ins around, but where&#39;s the fun in using someone else&#39;s version. So I created a jQuery Tab Down.&lt;/p&gt;&lt;p&gt;Tab Down simply hides content above the page which a user can then access via a floating tab. It&#39;s easier to illustrate using a couple of demos. &lt;a href=&quot;https://nooshu.com/lab/experiments/jquery-tab-down/&quot;&gt;Demo 1&lt;/a&gt; pushes the page content down with the tab, where as &lt;a href=&quot;https://nooshu.com/lab/experiments/jquery-tab-down/index2.html&quot;&gt;demo 2&lt;/a&gt; tab content floats over the top of the page content. You can easily switch between versions by passing true or false to the &#39;floating&#39; option when calling the tabDown method.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;jQuery tab down plug-in example&quot; decoding=&quot;async&quot; height=&quot;531&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/04/02/jquery-plug-in-tab-down/pXpf-AjpZT-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/04/02/jquery-plug-in-tab-down/pXpf-AjpZT-300.webp 300w, https://nooshu.com/blog/2010/04/02/jquery-plug-in-tab-down/pXpf-AjpZT-597.webp 597w&quot; title=&quot;Demo 1 example with floating set to false.&quot; width=&quot;597&quot;&gt;&lt;/p&gt;&lt;p&gt;The plug-in is simple to use and has a number of options to allow customisation:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#tabContent&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;tabDown&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;floating&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;900&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;easing&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;easeOutCubic&quot;&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;downText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Down&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;upText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Up&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Floating&lt;/strong&gt;: Sets if the tab floats over the main content or pushes it down (default: true)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Time&lt;/strong&gt;: Time in milliseconds for the slide animation (default: 1000)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Container&lt;/strong&gt;: If your tab content is inside a &#39;wrapper&#39; div, let the plug-in know the selector for this div, see demo 1 for an example (default: &#39;body&#39;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Downtext&lt;/strong&gt;: The text used in the tab before sliding down (default: &#39;Down&#39;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uptext&lt;/strong&gt;: The text used in the tab after sliding down (default: &#39;Up&#39;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Easing&lt;/strong&gt;: For smoother animation you can use animation easing, requires the &lt;a href=&quot;http://gsgd.co.uk/sandbox/jquery/easing/&quot;&gt;jQuery easing plug-in&lt;/a&gt; (default: &#39;swing&#39;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;View &lt;a href=&quot;https://nooshu.com/lab/experiments/jquery-tab-down/&quot;&gt;demo 1&lt;/a&gt; and &lt;a href=&quot;https://nooshu.com/lab/experiments/jquery-tab-down/index2.html&quot;&gt;demo 2&lt;/a&gt; &lt;small&gt;(version 0.1 – updated 2nd Apr 2010)&lt;/small&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>NASA Picture of the Day CSS Design Challenge</title>
    <link href="https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/" />
    <updated>2010-03-31T22:29:20Z</updated>
    <id>https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/</id>
    <content type="html">&lt;p&gt;A website I check on a regular basis is the &lt;a href=&quot;http://antwrp.gsfc.nasa.gov/apod/archivepix.html&quot;&gt;NASA&#39;s Astronomy Picture of the Day&lt;/a&gt;, a superb site if you&#39;re interested in the Cosmos; it publishes a new picture each day with an explanation written by a professional astronomer. Amazing stuff... apart from the design is... just horrible! If you want an example of what the web looked like back in the mid nineties, then take a &lt;a href=&quot;http://antwrp.gsfc.nasa.gov/apod/astropix.html&quot;&gt;look&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Now I doubt restyling the website is high up on NASA&#39;s priority list, but there&#39;s no reason a user can&#39;t do it for them (only on their own machine of-course). All you need is Firefox and a custom CSS file.&lt;/p&gt;&lt;p&gt;The CSS file you will need is called userContent.css, and it resides in your Firefox profile under the &#39;chrome&#39; directory. To find your profile directory look under &#39;Help&#39; &gt; &#39;Troubleshooting information...&#39;, there you will see a button to open the profile directory (&lt;strong&gt;Note&lt;/strong&gt;: I wouldn&#39;t change much in here, it can break your profile!). If you still can&#39;t find it you can always look &lt;a href=&quot;http://support.mozilla.com/en-US/kb/Profiles#Where_is_my_profile_stored_&quot;&gt;here&lt;/a&gt;. You may not have a userContent.css by default, so feel free to create a blank one.&lt;/p&gt;&lt;p&gt;This is when the fun starts, first you want to add this code snippet:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@-moz-document&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;antwrp.gsfc.nasa.gov&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* Page specific CSS here */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is a Mozilla specific property that will allow you to include CSS for pages on a specific domain; note the &#39;antwrp.gsfc.nasa.gov&#39;. Feel free to add as little or as much CSS as you want until you are happy with the page layout. Here&#39;s a sample from the userContent.css file I created:&lt;/p&gt;&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@-moz-document&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;antwrp.gsfc.nasa.gov&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* authors and editors */&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;body &gt; hr ~ center:nth-child(5)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.69em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* all b&#39;s */&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;body &gt; hr ~ center:nth-child(5) b&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 8px 0 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;line-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.4&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* first b */&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;body &gt; hr ~ center:nth-child(5) b:nth-child(1)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 32px 0 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* all a&#39;s */&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;body &gt; hr ~ center:nth-child(5) a&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To make the page look in any way presentable I&#39;ve had to rely heavily on the new &lt;a href=&quot;http://www.w3.org/TR/css3-selectors/&quot;&gt;CSS3 selectors&lt;/a&gt; available in most modern browsers; as the state of the mark-up on the pages is truly awful. I guess it all adds to the challenge though, it&#39;s certainly a great way to learn CSS3.&lt;/p&gt;&lt;p&gt;Here are a couple of before and after images I created, nothing spectacular but hopefully it looks slightly better:&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Nasa picture archive, before and after.&quot; decoding=&quot;async&quot; height=&quot;270&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/7TqVv6gsnU-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/7TqVv6gsnU-300.webp 300w, https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/7TqVv6gsnU-600.webp 600w, https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/7TqVv6gsnU-814.webp 814w&quot; title=&quot;Before and after screenshot of the archive page.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Nasa picture of the day, before after&quot; decoding=&quot;async&quot; height=&quot;359&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/SocBu6RAD4-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/SocBu6RAD4-300.webp 300w, https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/SocBu6RAD4-600.webp 600w, https://nooshu.com/blog/2010/03/31/nasa-picture-of-the-day-css-design-challenge/SocBu6RAD4-814.webp 814w&quot; title=&quot;Before and after screenshot of the daily picture page.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The &#39;index&#39; page was a page to far for me, I have no idea what the developer was trying to do with the mark-up, it&#39;s overuse of tables for layout really doesn&#39;t help either. On the end I got bored but feel free to dive in and take a look.&lt;/p&gt;&lt;p&gt;It&#39;s also worth mentioning that it is possible to customise the style of any website using a Firefox plug-in called &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/2108&quot;&gt;Stylish&lt;/a&gt;, you can view many examples of it&#39;s usage on &lt;a href=&quot;http://userstyles.org/&quot;&gt;userstyles.org&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;NOTE: Download no longer available.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>p01.org: Amazing Experiments using JavaScript</title>
    <link href="https://nooshu.com/blog/2010/03/30/p01-org-amazing-experiments-using-javascript/" />
    <updated>2010-03-30T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/30/p01-org-amazing-experiments-using-javascript/</id>
    <content type="html">&lt;p&gt;Some websites you come across make you sit back in and say &#39;wow&#39;. Stumbling across &lt;a href=&quot;http://www.p01.org/releases/&quot;&gt;p01.org&lt;/a&gt; that&#39;s exactly what I said. P01.org is a website showcasing experiments in JavaScript &amp; Canvas by French Web Developer Mathieu Henri, who currently works for Opera. As HTML5 is becoming increasingly popular, more and more of these cool little experiments are popping up; great news for people like myself who are interested in this up and coming technology.&lt;/p&gt;&lt;p&gt;Mathieu&#39;s experiments range over a wide range of areas, from photo manipulation and raytracing to fractal generation and &lt;a href=&quot;http://en.wikipedia.org/wiki/Wolfenstein_3D&quot;&gt;Wolfenstein&lt;/a&gt;... amazing stuff! My personal favorites are the &lt;a href=&quot;http://www.p01.org/releases/512b_jspongy/jspongy.htm&quot;&gt;512b JSpongy&lt;/a&gt; and &lt;a href=&quot;http://www.p01.org/releases/3D_dots_performance_test/&quot;&gt;3D Dots Performance Test&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Experiments by p01.org&quot; decoding=&quot;async&quot; height=&quot;288&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/30/p01-org-amazing-experiments-using-javascript/2egXd1kDoE-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/30/p01-org-amazing-experiments-using-javascript/2egXd1kDoE-300.webp 300w, https://nooshu.com/blog/2010/03/30/p01-org-amazing-experiments-using-javascript/2egXd1kDoE-574.webp 574w&quot; title=&quot;The 3D dots and 512b JSpongy experiments using Canvas&quot; width=&quot;574&quot;&gt;&lt;/p&gt;&lt;p&gt;One thing that isn&#39;t available on the website is an RSS feed for the latest releases, which is a pity as I want to add them to my Google Reader. Luckily with Web Applications like &lt;a href=&quot;http://www.dapper.net/&quot;&gt;dapper.net&lt;/a&gt; it&#39;s easy to create an RSS feed (plus many others) from any website using screen scraping. For anyone else who wants to add p01 to their feed reader, here&#39;s the &lt;a href=&quot;http://www.dapper.net/services/po1org&quot;&gt;RSS feed&lt;/a&gt; I created.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Internet Explorer 9: Is that light at the end of the tunnel?</title>
    <link href="https://nooshu.com/blog/2010/03/25/internet-explorer-9-is-that-light-at-the-end-of-the-tunnel/" />
    <updated>2010-03-25T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/25/internet-explorer-9-is-that-light-at-the-end-of-the-tunnel/</id>
    <content type="html">&lt;p&gt;Last week Microsoft decided to unveil a technical preview of Internet Explorer 9 at &lt;a href=&quot;http://live.visitmix.com/&quot;&gt;Mix 2010&lt;/a&gt;, a conference that demonstrates up and coming Microsoft technology. Since it&#39;s only a preview it&#39;s no where near a complete browser yet, but it gives developers a chance to see what the future holds for the (Microsoft) web.&lt;/p&gt;&lt;p&gt;It comes packed with a whole host of &lt;a href=&quot;http://ie.microsoft.com/testdrive/&quot;&gt;new features&lt;/a&gt;.... border-radius at last! With new CSS3 selectors being included too, the life of the Web Developer is becoming slightly easier (and a lot more interesting!). The JavaScript engine has been improved dramatically, and it actually scores quite well on the &lt;a href=&quot;http://acid3.acidtests.org/&quot;&gt;Acid3 test&lt;/a&gt; (55 / 100 for IE9 compared to 20 / 100 for IE8).&lt;/p&gt;&lt;p&gt;Also included are a whole host of HTML5 features including native video support (video tag), audio embedding (audio tag) and some SVG sprinkled in there for good measure. The video codec being used is h.264, YouTube and Vimeo already support this codec which is great news for users. It&#39;s worth noting that &lt;a href=&quot;http://weblogs.mozillazine.org/roc/archives/2010/01/video_freedom_a.html&quot;&gt;Mozilla doesn&#39;t plan on using h.264&lt;/a&gt; due to licence issues since the codec isn&#39;t free. So there could be trouble in store for developers when using the video tag in the future.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Internet Explorer 9 business chart demo&quot; decoding=&quot;async&quot; height=&quot;612&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/25/internet-explorer-9-is-that-light-at-the-end-of-the-tunnel/c9t-K74PGf-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/25/internet-explorer-9-is-that-light-at-the-end-of-the-tunnel/c9t-K74PGf-300.webp 300w, https://nooshu.com/blog/2010/03/25/internet-explorer-9-is-that-light-at-the-end-of-the-tunnel/c9t-K74PGf-600.webp 600w, https://nooshu.com/blog/2010/03/25/internet-explorer-9-is-that-light-at-the-end-of-the-tunnel/c9t-K74PGf-814.webp 814w&quot; title=&quot;IE9: It&#39;s not pretty but it certainly is a step in the right direction.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Hopefully IE9 will finally kill off IE6 (although I really can&#39;t see that happening) as it will only be available on systems running Vista+ operating systems. The reason for this is because IE9 supports DirectX video acceleration using Direct2D, which (apparently) isn&#39;t possible on XP; forcing users to upgrade to Windows 7. It&#39;s an interesting strategy, lets hope it works.&lt;/p&gt;&lt;p&gt;I&#39;m not usually one to praise Microsoft when it comes to Internet Explorer, but IE9 looks very promising.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Coolclock: HTML5 Clock that is Actually Quite Cool*</title>
    <link href="https://nooshu.com/blog/2010/03/25/coolclock-html5-clock-that-is-actually-quite-cool/" />
    <updated>2010-03-25T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/25/coolclock-html5-clock-that-is-actually-quite-cool/</id>
    <content type="html">&lt;p&gt;*Unless you&#39;re Internet Explorer, then not so much.&lt;/p&gt;&lt;p&gt;Every so often a designer asks for something a little different, this time it was for an animated clock that sits in the top right corner of a website. Usually that means looking for a Flash based solution, but Canvas is the new Flash… apparently; luckily &lt;a href=&quot;http://randomibis.com/coolclock/&quot;&gt;Coolclock&lt;/a&gt; is available to plug the no-flash gap.&lt;/p&gt;&lt;p&gt;Coolclock is a fantastic little script that will generate a canvas based clock from the parameters you pass it via the class attribute (I don&#39;t agree with that, but it works). It&#39;s fully customisable by way of a little JSON data, allowing you to create your own skins. There are quite a few user submitted skins available to use on the &lt;a href=&quot;http://randomibis.com/coolclock/&quot;&gt;Coolclock&lt;/a&gt; homepage if you don&#39;t want to create your own.&lt;/p&gt;&lt;p&gt;I needed the clock to be able to change depending on the country that&#39;s chosen from a dropdown list, here&#39;s how I achieved it:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Generate the clock&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;token function function-variable&quot;&gt;generateClock&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Create our dropdown, time difference compared to GMT as a value&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; dropHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;select id=&#39;localTime&#39;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    dropHTML &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;option value=&#39;0&#39;&gt;United Kingdom&amp;lt;/option&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    dropHTML &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;option value=&#39;+1&#39;&gt;France&amp;lt;/option&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    dropHTML &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;option value=&#39;-4&#39;&gt;New York&amp;lt;/option&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    dropHTML &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;/select&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Append to the container&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#time&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dropHTML&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Initial clock appended to the page (local time)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; clockHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;canvas id=&quot;clk1&quot; class=&quot;CoolClock:swissRail:27:noSeconds&quot;&gt;&amp;lt;/canvas&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#clock&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;clockHTML&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//When the dropdown changes&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#localTime&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;change&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; localClockHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;canvas id=&quot;clk1&quot; class=&quot;CoolClock:swissRail:27:noSeconds:&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&quot;&gt;&amp;lt;/canvas&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Empty the old clock, append the new clock&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#clock&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;localClockHTML&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Fire the coolclock function to generate the new clock&lt;/span&gt;
        CoolClock&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAndCreateClocks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Very simple, the relevant HTML code is generated with JavaScript as to not leave a pointless dropdown on the page when it&#39;s turned off. The key to the solution was calling the CoolClock.findAndCreateClocks() function from the coolclock.js file, without that the clock isn&#39;t recreated when the dropdown is changed.&lt;/p&gt;&lt;p&gt;As mentioned above, the script seems to come unstuck when viewed in Internet Explorer, since IE doesn&#39;t support the canvas tag. You can plug the lack of canvas support using &lt;a href=&quot;http://excanvas.sourceforge.net/&quot;&gt;ExplorerCanvas&lt;/a&gt;, but even then coolclock is a little quirky. I managed to get the clock working with IE6 &amp; IE7 but IE8 wasn&#39;t having any of it; bit of of shame as it&#39;s a nice solution. Good news is on the horizon though, IE9 will support canvas by default so no nasty JavaScript hacks.&lt;/p&gt;&lt;p&gt;Eventually I decided to add the clock as a type of &#39;progressive enhancemen&#39; by wrapping the function in the following:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//IE returns false since it uses &#39;styleFloat&#39;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;jQuery&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;support&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cssFloat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//IE can&#39;t see this code&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&#39;s not great, and I hate having to do it but if users want to see a cool clock they will have to upgrade to a modern browser (that&#39;s not IE8).&lt;/p&gt;&lt;p&gt;For those who are interested, here&#39;s a &lt;a href=&quot;https://nooshu.com/lab/experiments/coolclock-dropdown/&quot;&gt;quick demo&lt;/a&gt; of the code in action.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Snipplr: How to Annoy Your Site Users</title>
    <link href="https://nooshu.com/blog/2010/03/18/snipplr-how-to-annoy-your-site-users/" />
    <updated>2010-03-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/18/snipplr-how-to-annoy-your-site-users/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Snipplr now has a new owner so these issues should be ironed out very soon, I&#39;ve written a new blog post &lt;a href=&quot;https://nooshu.com/blog/2010/06/16/snipplr-under-new-management/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I feel quite sad I&#39;m having to write this post but it&#39;s been a long time coming. I&#39;ve been a big fan of &lt;a href=&quot;http://snipplr.com/&quot;&gt;Snipplr.com&lt;/a&gt;, having been using it for over 4 years it&#39;s become an essential part of my daily development process; I even mentioned it a few months ago in a recent blog post &#39;&lt;a href=&quot;https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/&quot;&gt;Useful Websites for a Front-end Web Developer&lt;/a&gt;&#39;. Lately a few things have changed on Snipplr for the worse.&lt;/p&gt;&lt;p&gt;My first annoyance is the amount of spam posts and comments the site gets; now I understand it isn&#39;t Snipplr&#39;s fault that they are a target for spammers but they don&#39;t seem to be doing anything about it. The sign-up process obviously isn&#39;t strong enough to stop the spam bots getting in, so have a rethink and change that. I can&#39;t tell you the number of hours I&#39;ve spent pressing the &#39;Report this snippet&#39; link on spam posts; recently I&#39;ve just given up. Unfortunately you can&#39;t report comment spam and in the last week I&#39;ve had an influx of spam on quite a few of my snippets. Not good!&lt;/p&gt;&lt;p&gt;Now spam, okay it&#39;s annoying but I can live with it as all I want to do was get to my code snippets. The final nail in Snipplrs coffin for me is all the new advertisment banners they&#39;ve recently added. There are now so many it&#39;s actually effecting the usability of the site!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Somewhere on that Snipplr page there is a snippet of code&quot; decoding=&quot;async&quot; height=&quot;661&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/18/snipplr-how-to-annoy-your-site-users/oZ9WKLryOS-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/18/snipplr-how-to-annoy-your-site-users/oZ9WKLryOS-300.webp 300w, https://nooshu.com/blog/2010/03/18/snipplr-how-to-annoy-your-site-users/oZ9WKLryOS-600.webp 600w, https://nooshu.com/blog/2010/03/18/snipplr-how-to-annoy-your-site-users/oZ9WKLryOS-814.webp 814w&quot; title=&quot;Lots of Ads, there&#39;s even one that floats over the bottom of the page.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;I understand it may be costing money to run the site as it is quite popular, but shoving banners down your users throats isn&#39;t the best idea if you want to keep them using the site. They will just go elsewhere, and thats exactly what I&#39;m doing. In the Snipplr setting you will find an export function, but it doesn&#39;t seem to be working at the moment so it could be a long copy / paste job... bugger.&lt;/p&gt;&lt;p&gt;A couple of alternatives I&#39;m trying at the moment are &lt;a href=&quot;http://snipt.net/&quot;&gt;Snipt&lt;/a&gt; and &lt;a href=&quot;http://snippets.dzone.com/&quot;&gt;DZone Snippets&lt;/a&gt;, I haven&#39;t decided on which one I prefer yet, but they both look promising.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Book Review: jQuery Enlightenment</title>
    <link href="https://nooshu.com/blog/2010/03/15/book-review-jquery-enlightenment/" />
    <updated>2010-03-15T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/15/book-review-jquery-enlightenment/</id>
    <content type="html">&lt;p&gt;About a month ago I had the pleasure of reading &lt;a href=&quot;http://jqueryenlightenment.com/&quot;&gt;&#39;jQuery Enlightenment&#39;&lt;/a&gt; by &lt;a href=&quot;http://codylindley.com/&quot;&gt;Cody Lindley&lt;/a&gt;, a truly excellent book any Web Developer from beginner to advanced should have on their to-read list. Now I don&#39;t usually enjoy reading books about code as they are usually as dull as dishwater; just something you have to get through before you can get onto the interesting part of making it work for you; jQuery Enlightenment is different.&lt;/p&gt;&lt;p&gt;It starts by going through the basic concepts behind jQuery and how to use it, if you are already a jQuery developer you could skip this, but as it&#39;s packed full of short concise information you never know what you may learn. The book covers all aspects of jQuery you will need to get you started plus much more, from selecting, manipulating and traversing DOM elements all the way through to plug-ins and best practices.&lt;/p&gt;&lt;p&gt;There were countless times while reading I thought to myself &#39;Well, I didn&#39;t know you could do that!&#39;, here&#39;s one of my favorites:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;jQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#testanchor&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;keypress mouseenter focus&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Event fires keypress, mouseenter and focus&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Do stuff....&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The bind() method can accept more than one event type, so the above function fires when any one of the listed events is detected on our #testanchor. I can&#39;t remember the number of times where I&#39;ve used multiple binds on the same element to achieve the same effect.&lt;/p&gt;&lt;p&gt;The book is packed full of useful code snippets like the one above that will save you time and effort when it comes to jQuery development. Being only 123 pages long it&#39;s a fairly quick read, and it&#39;s always great to have on hand as a reference when needed.&lt;/p&gt;&lt;p&gt;Grab it from &lt;a href=&quot;http://jqueryenlightenment.com/&quot;&gt;here as an eBook&lt;/a&gt; for $15 or hard copies are available from lulu.com if you prefer that.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Formula One 2010 Season, people to follow on Twitter</title>
    <link href="https://nooshu.com/blog/2010/03/12/formula-one-2010-season-people-to-follow-on-twitter/" />
    <updated>2010-03-12T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/12/formula-one-2010-season-people-to-follow-on-twitter/</id>
    <content type="html">&lt;p&gt;I&#39;ve been a Formula One fan for as long as I can remember, my first experience was back in 1987 at Silverstone when the cars had turbos... oh the old days! The new season looks to be one of the most exciting yet, what with a couple of new teams, regulation changes and Schumacher has come out of retirement, roll on Sunday at Bahrain!&lt;/p&gt;&lt;p&gt;Twitter has become a great tool if you are looking for inside information on what is happening in and out of the paddock, so if you&#39;re interested in F1, here are a few people you should be following (if spot any fakes or know of others let me know!):&lt;/p&gt;&lt;h3 id=&quot;drivers&quot;&gt;Drivers&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Jenson Button (&lt;a href=&quot;http://twitter.com/The_Real_JB&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@The_Real_JB&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Rubens Barrichello (&lt;a href=&quot;http://twitter.com/rubarrichello&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@rubarrichello&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Bruno Senna (&lt;a href=&quot;http://twitter.com/BSenna&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@BSenna&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Heikki Kovalainen (&lt;a href=&quot;http://twitter.com/H_Kovalainen&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@H_Kovalainen&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Nico Hülkenberg (&lt;a href=&quot;http://twitter.com/NicoHulkenberg&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@NicoHulkenberg&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Lucas di Grassi (&lt;a href=&quot;http://twitter.com/lucasdigrassi&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@lucasdigrassi&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Karun Chandhok (&lt;a href=&quot;http://twitter.com/karunchandhok&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@karunchandhok&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Felipe Massa (&lt;a href=&quot;http://twitter.com/Forza_Felipe&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@Forza_Felipe&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Fernando Alonso (&lt;a href=&quot;http://twitter.com/NanoAlonso&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@NanoAlonso&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Mark Webber (&lt;a href=&quot;http://twitter.com/AussieGrit&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@AussieGrit&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Nick Heidfeld (&lt;a href=&quot;http://twitter.com/NickHeidfeld&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@NickHeidfeld&lt;/a&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;teams-team-employees&quot;&gt;Teams / Team Employees&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Virgin Racing (&lt;a href=&quot;http://twitter.com/virginf1racing&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@virginf1racing&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Mercedes GP (&lt;a href=&quot;http://twitter.com/OfficialMGP&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@OfficialMGP&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Lotus F1 (&lt;a href=&quot;http://twitter.com/lotusf1racing&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@lotusf1racing&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Ferrari (&lt;a href=&quot;http://twitter.com/InsideFerrari&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@InsideFerrari&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;McLaren (&lt;a href=&quot;http://twitter.com/TheFifthDriver&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@TheFifthDriver&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Force India (&lt;a href=&quot;http://twitter.com/clubforce&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@clubforce&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Redbull (&lt;a href=&quot;http://twitter.com/redbullf1spy&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@redbullf1spy&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Toro Rosso (&lt;a href=&quot;http://twitter.com/ToroRossoSpy&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@ToroRossoSpy&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;BMW Sauber (&lt;a href=&quot;http://twitter.com/BMWSauberF1Team&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@BMWSauberF1Team&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;USF1 Team (&lt;a href=&quot;http://twitter.com/USF1Team&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@USF1Team&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Ross brawn - Mercedes (&lt;a href=&quot;http://twitter.com/rossbrawngp&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@rossbrawngp&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Mike Gascoyne - Lotus (&lt;a href=&quot;http://twitter.com/MikeGascoyne&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@MikeGascoyne&lt;/a&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;ex-formula-one-drivers&quot;&gt;Ex-Formula One Drivers&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Nigel Mansell (&lt;a href=&quot;http://twitter.com/Mansell5&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@Mansell5&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Nelson Piquet (&lt;a href=&quot;http://twitter.com/NelsonPiquet&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@NelsonPiquet&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Jacques Villeneuve (&lt;a href=&quot;http://twitter.com/27villeneuve&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@27villeneuve&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Juan Pablo Montoya (&lt;a href=&quot;http://twitter.com/jpmontoya&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@jpmontoya&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Sir Stirling Moss (&lt;a href=&quot;http://twitter.com/StirlingMossCom&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@StirlingMossCom&lt;/a&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;official-fia&quot;&gt;Official / FIA&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Formula1.com (&lt;a href=&quot;http://twitter.com/F1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@F1&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;F1™ Timing App (&lt;a href=&quot;http://twitter.com/f1timingapp&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@f1timingapp&lt;/a&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;bbc-f1-team&quot;&gt;BBC F1 Team&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Jonathan Legard (&lt;a href=&quot;http://twitter.com/legardj&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@legardj&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Lee McKenzie (&lt;a href=&quot;http://twitter.com/leemckenzief1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@LeeMcKenzieF1&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Ted Kravitz (&lt;a href=&quot;http://twitter.com/tedkravitz&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@tedkravitz&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Sarah Holt (&lt;a href=&quot;http://twitter.com/sarahholtf1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@sarahholtf1&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Andrew Benson (&lt;a href=&quot;http://twitter.com/andrewbensonf1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@andrewbensonf1&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Jake Humphrey (&lt;a href=&quot;http://twitter.com/jakehumphreyf1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@jakehumphreyf1&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Radio 5 Live F1 (&lt;a href=&quot;http://twitter.com/5LiveF1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@5LiveF1&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;BBC F1 (&lt;a href=&quot;http://twitter.com/bbcf1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@bbcf1&lt;/a&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;independent-bloggers&quot;&gt;Independent Bloggers&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;James Roberts - Features Editor, F1 Racing (&lt;a href=&quot;http://twitter.com/JRobertsF1&quot; title=&quot;I no longer use Twitter (X) because life&#39;s too short to bankroll the world&#39;s richest professional edgelord.&quot;&gt;@JRobertsF1&lt;/a&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Well just a few people to follow there. I&#39;m sure I&#39;ve missed a few people off. Leave a comment if you know of any others and I&#39;ll add them to the list.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Preview Post Broke in WordPress?</title>
    <link href="https://nooshu.com/blog/2010/03/05/preview-post-broke-in-wordpress/" />
    <updated>2010-03-05T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/05/preview-post-broke-in-wordpress/</id>
    <content type="html">&lt;p&gt;I&#39;ve had a problem with my blog that has been on the back of my mind for a while now; whenever I click the &#39;Preview&#39; button it simply shows my homepage. No errors... just my homepage. I had a search about and there were suggestions it may be because of the use of a sub-domain (or lack of in my case). So I tried changing a few settings in the admin panel, but still no luck. I thought it may be a .htaccess issue until I enabled the now famous &lt;a href=&quot;http://binarybonsai.com/wordpress/kubrick/&quot;&gt;Kubric&lt;/a&gt; theme, also know as &#39;default&#39;; and suddenly the preview function worked. So it was an issue with my theme... Oops!&lt;/p&gt;&lt;p&gt;My immediate thought was the &#39;functions.php&#39; file since the &#39;single.php&#39; file was pretty much the same as Kubrics. After a few minutes of head scratching I finally solved the issue, my sidebar was breaking things:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Register Sidebars&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$p&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;before_widget&#39;&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&amp;lt;li id=&quot;%1$s&quot; class=&quot;widget %2$s&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;after_widget&#39;&lt;/span&gt;   &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&amp;lt;/li&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;before_title&#39;&lt;/span&gt;   &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&amp;lt;h3 class=&quot;widgettitle&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;after_title&#39;&lt;/span&gt;    &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&amp;lt;/h3&gt;&#39;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;register_sidebars&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;register_sidebars&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note the use of &#39;&lt;eq&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;katex-base&quot;&gt;&lt;span class=&quot;katex-strut&quot; style=&quot;height:.9463em;vertical-align:-.1944em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.7519em&quot;&gt;&lt;span style=&quot;top:-3.063em;margin-right:.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;′&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mpunct&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;se&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;er&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;na&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0197em&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0197em&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.1389em&quot;&gt;W&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;or&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.1389em&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;ess&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.109em&quot;&gt;M&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.1076em&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;o&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0269em&quot;&gt;pw&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;so&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;er&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0269em&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;hi&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;iab&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0197em&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;an&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0315em&quot;&gt;ak&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0269em&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.1076em&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;o&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0278em&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;ce&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;hi&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;sc&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;han&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:.0359em&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;o&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:.7519em&quot;&gt;&lt;span style=&quot;top:-3.063em;margin-right:.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mtight katex-sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;&lt;span class=&quot;mord mtight&quot;&gt;′&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/eq&gt;args&#39; everything worked perfectly:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$args&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//settings here&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;register_sidebars&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;register_sidebars&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So there you go kids, don&#39;t use &lt;code&gt;$p&lt;/code&gt; in your themes / plug-ins, use something a little more descriptive for your variable names.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Changing your WordPress Sidebar Markup</title>
    <link href="https://nooshu.com/blog/2010/03/03/changing-your-wordpress-sidebar-markup/" />
    <updated>2010-03-03T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/03/changing-your-wordpress-sidebar-markup/</id>
    <content type="html">&lt;p&gt;While creating a theme for WordPress today I ran into a little problem with my sidebar; the layout of the sidebar didn&#39;t quite fit my needs for the design in-hand. At first I thought it could be a case of delving into the WordPress core code that generates all the widgets and modifying where needed, but that isn&#39;t much fun. You also run the rist of these changes being overwritten next time you update WordPress; there had to be another way.&lt;/p&gt;&lt;p&gt;Purely for styling I needed to add an extra span to the title and a div to the widget wrapper. After searching through the &lt;a href=&quot;http://codex.wordpress.org/&quot;&gt;Codex&lt;/a&gt; I came across the perfect &lt;a href=&quot;http://codex.wordpress.org/Function_Reference/register_sidebars&quot;&gt;solution&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Custom settings in associative array&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$args&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;before_widget&#39;&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&amp;lt;li id=&quot;%1$s&quot; class=&quot;widget %2$s&quot;&gt;&amp;lt;div class=&quot;inner&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;after_widget&#39;&lt;/span&gt;   &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&amp;lt;/div&gt;&amp;lt;/li&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;before_title&#39;&lt;/span&gt;   &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;&amp;lt;h3 class=&quot;widgettitle&quot;&gt;&amp;lt;span&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;after_title&#39;&lt;/span&gt;    &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;   &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&amp;lt;/span&gt;&amp;lt;/h3&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//Check for register function and register the sidebar&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;register_sidebars&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;register_sidebars&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You simply create an array with your custom settings and pass it into the register_sidebars function along with the number of sidebars you wish to register; paste that into the functions.php file in your theme and you&#39;re done. If you don&#39;t have a functions.php file you can just create one. All the settings are pretty self-explanatory and with these changing widget markup is easy.&lt;/p&gt;&lt;p&gt;On a side note; I&#39;ve never been a big fan of using nested lists in the sidebar, I&#39;m not sure it&#39;s semantically correct; but I guess it does the job and works even when CSS is disabled.&lt;/p&gt;&lt;p&gt;You learn something new everyday!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Useful websites for a Front-End Web Developer</title>
    <link href="https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/" />
    <updated>2010-03-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/</id>
    <content type="html">&lt;p&gt;Over the past few days I&#39;ve compiled a list of websites I use quite frequently that really help speed up the Web Development process. The sites won&#39;t build your pages for you but they can certainly help when it comes to debugging and problem solving.&lt;/p&gt;&lt;h3 id=&quot;clean-css&quot;&gt;Clean CSS&lt;/h3&gt;&lt;p&gt;I have been using &lt;a href=&quot;http://cleancss.com/&quot;&gt;Clean CSS&lt;/a&gt; for a few years now to clean up and optimise my CSS. The tool will give you a break down of where you can be making optimisations and also point out potential issues with invalid properties. You can either apply the changes manually, as it gives you a line number (my prefered method) or you can download the output as a file. I must admit I only use it every so often as I&#39;ve merged most of the optimisations into my working process.&lt;/p&gt;&lt;p&gt;An exception to that rule is when working with other peoples CSS, it&#39;s always my first port of call. There&#39;s even the ability to create your own template, so the CSS can be formatted just as you like it when it comes out the other end.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;CleanCSS GUI&quot; decoding=&quot;async&quot; height=&quot;709&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/ODN0gHh_rF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/ODN0gHh_rF-300.webp 300w, https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/ODN0gHh_rF-600.webp 600w, https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/ODN0gHh_rF-814.webp 814w&quot; title=&quot;The CleanCSS interface simple to use with lots of options to play with.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;snipplr&quot;&gt;Snipplr&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;http://snipplr.com/&quot;&gt;Snipplr&lt;/a&gt; has been around for a couple of years now, it&#39;s a excellent place to store all those little snippets of code that you often use. You can view other peoples snippets too, so if you&#39;re looking to a solution to a problem there&#39;s no need to re-invent the wheel; look see if someone else has a solution.&lt;/p&gt;&lt;p&gt;The only real issue I have with &lt;a href=&quot;http://snipplr.com/&quot;&gt;Snipplr&lt;/a&gt; is the amount of spam it receives, which is a real pity as it&#39;s such a useful tool. Maybe that&#39;s something the developers can look into fixing.&lt;/p&gt;&lt;h3 id=&quot;em-calculator&quot;&gt;Em Calculator&lt;/h3&gt;&lt;p&gt;For a few years now I&#39;ve been setting my font sizes using the relative unit em&#39;s rather than pixels or points. This was due to the fact that if you set your font size in pixels, IE6 users can&#39;t increase the font size due to IE6 being... erm... a truly awful browser; not good at all for accessibility. Luckily IE6 is gradually fading away and IE7+ all scale font sizes properly, so in the future this tool won&#39;t be needed.&lt;/p&gt;&lt;p&gt;The problem with using relative font sizes comes when you have a nested elements. Say you have an unordered list with its font size set to 12px (0.75em) and you want one of your list items to be set to 10px (0.63em). Since we are using relative font sizes the 0.75em has an effect on the lower list items. If you simply set the the list item to 0.63em the text size will be tiny because you are asking the browser to set a size of 0.63em &lt;strong&gt;of&lt;/strong&gt; 0.75em. Not what we wanted at all! This is where &lt;a href=&quot;http://riddle.pl/emcalc/&quot;&gt;Em Calculator&lt;/a&gt; comes in useful as it does all the calculations for you. You actually needed to set the list item to 0.83em….duh!&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Em Calculator interface for quick relative font size calculations&quot; decoding=&quot;async&quot; height=&quot;364&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/-IOPnwzElX-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/-IOPnwzElX-300.webp 300w, https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/-IOPnwzElX-600.webp 600w, https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/-IOPnwzElX-794.webp 794w&quot; title=&quot;An example of how Em Calculator works with my example unordered list.&quot; width=&quot;794&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;jsfiddle&quot;&gt;jsFiddle&lt;/h3&gt;&lt;p&gt;I&#39;ve mentioned &lt;a href=&quot;http://www.jsfiddle.net/&quot;&gt;jsFiddle&lt;/a&gt; before, it&#39;s a tool that allows you experiment with HTML, CSS and JavaScript and see the results on the same page. In the past couple of days they&#39;ve added a new version of Processing and the RaphaelJS library. A link to my previous post on jsFiddle is available &lt;a href=&quot;https://nooshu.com/blog/2010/02/23/jsfiddle-my-new-favourite-website/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;html-entity-character-lookup&quot;&gt;HTML Entity Character Lookup&lt;/h3&gt;&lt;p&gt;If you&#39;ve ever had to build a set of multi-lingual HTML emails you will know how tedious it can be to encode all characters so they are valid HTML. Lucky with &lt;a href=&quot;http://leftlogic.com/lounge/articles/entity-lookup/&quot;&gt;HTML Entity Character Lookup&lt;/a&gt; by LeftLogic it&#39;s simple. Copy and paste the character you want to encode into the tool and it will give you encoded version; do a quick search and replace then you&#39;re done.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;HTML Entity Character Lookup by Leftlogic&quot; decoding=&quot;async&quot; height=&quot;922&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/xzDi4E4h_Y-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/xzDi4E4h_Y-300.webp 300w, https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/xzDi4E4h_Y-600.webp 600w, https://nooshu.com/blog/2010/03/01/useful-websites-for-a-front-end-web-developer/xzDi4E4h_Y-814.webp 814w&quot; title=&quot;Quickly lookup character entity&#39;s using Leftlogics tool.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;For OS X users there&#39;s also a dashboard widget you can use for easy access.&lt;/p&gt;&lt;h3 id=&quot;css-sprite-generator&quot;&gt;CSS Sprite Generator&lt;/h3&gt;&lt;p&gt;Using the sprites method can speed up your page load time by minimising the number of HTTP Requests a users browser has to make to the server. It involves consolidating all your little icons and background images into one file, then moving that one image around using the background-position property.&lt;/p&gt;&lt;p&gt;You could do this manually in Photoshop, or you could get an on-line tool to do it for you. &lt;a href=&quot;http://spritegen.website-performance.org/&quot;&gt;CSS Sprite Generator&lt;/a&gt; is my favourite of the sprite generators available. Simply ZIP up the files you want to sprite, upload them to the tool and it will return one large image and all the relevant CSS background-positions for you to copy / paste into your CSS file.&lt;/p&gt;&lt;p&gt;It&#39;s particularly useful when it comes to site navigations. Having the off, on and hover states all in one background image means the user doesn&#39;t see that ugly flash of unstyled content as hovering over the navigation bar, since all states are loaded when the user first hits the page.&lt;/p&gt;&lt;p&gt;Are there any others I&#39;m missing? Leave me a message via the contact form if there are others you use.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>jsFiddle: My new favourite website</title>
    <link href="https://nooshu.com/blog/2010/02/23/jsfiddle-my-new-favourite-website/" />
    <updated>2010-02-23T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/23/jsfiddle-my-new-favourite-website/</id>
    <content type="html">&lt;p&gt;Every so often you come across a site that is so amazingly obvious that you think &quot;why didn&#39;t I think of that!&quot;, &lt;a href=&quot;http://jsfiddle.net/&quot;&gt;jsFiddle&lt;/a&gt; is one of those sites. It allows you to paste your HTML, CSS and JavaScript code, and view the result all in the same page... simple! There are other sites out there that do the same such as &lt;a href=&quot;http://jsbin.com/&quot;&gt;Jsbin&lt;/a&gt;, but they just don&#39;t do it as well.&lt;/p&gt;&lt;p&gt;My favourite feature that really stands out is being able to include one of the many popular JavaScript library&#39;s by simple toggling a drop-down box; it even has different versions of each library to choose from. This is a really handy feature if you are a plug-in developer; keep your plug-in code in jsFiddle and quickly see if the latest version of the library breaks it.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;jsFiddle code snippet GUI&quot; decoding=&quot;async&quot; height=&quot;398&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/02/23/jsfiddle-my-new-favourite-website/9Dm2LrZazB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/02/23/jsfiddle-my-new-favourite-website/9Dm2LrZazB-300.webp 300w, https://nooshu.com/blog/2010/02/23/jsfiddle-my-new-favourite-website/9Dm2LrZazB-600.webp 600w, https://nooshu.com/blog/2010/02/23/jsfiddle-my-new-favourite-website/9Dm2LrZazB-814.webp 814w&quot; title=&quot;Quickly view your code change at the touch of a button&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Library&#39;s included so far are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Mootools Core&lt;/li&gt;&lt;li&gt;jQuery&lt;/li&gt;&lt;li&gt;Prototype&lt;/li&gt;&lt;li&gt;YUI&lt;/li&gt;&lt;li&gt;BBC Glow&lt;/li&gt;&lt;li&gt;Dojo&lt;/li&gt;&lt;li&gt;Processing.js&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A couple of other features available are the easy to access code examples for each library and ability to run Ajax requests directly in the page. It&#39;s like a mini IDE in your browser, all it needs now is an error console; maybe that is on their to-do list. If you don&#39;t use JavaScript library&#39;s and just want vanilla JavaScript, they have that option too.&lt;/p&gt;&lt;p&gt;As an example I&#39;ve added my &lt;a href=&quot;https://nooshu.com/lab/2010-02-05-jquery-plug-in-characters-left/&quot;&gt;charactersLeft&lt;/a&gt; jQuery plug-in to jsFiddle, take a look &lt;a href=&quot;http://jsfiddle.net/PDEQU/6/&quot;&gt;here&lt;/a&gt;. Very cool huh!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress and Feedburner Woes</title>
    <link href="https://nooshu.com/blog/2010/02/22/wordpress-and-feedburner-woes/" />
    <updated>2010-02-22T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/22/wordpress-and-feedburner-woes/</id>
    <content type="html">&lt;p&gt;For a couple of weeks now I&#39;ve been having issues with &lt;a href=&quot;http://feedburner.google.com/&quot;&gt;Feedburner&lt;/a&gt; and the Nooshu RSS feed. When I added a post to the blog, Feedburner wouldn&#39;t update; I had some luck with &#39;pinging&#39; Feedburner manually as mentioned in their documentation but having to do this every time isn&#39;t ideal. That method worked a couple of times then also stopped working.&lt;/p&gt;&lt;p&gt;So I tried changing the URL of the feed, maybe that was the problem. Nope that didn&#39;t work either; it would pull in the new feed and the latest posts but then get stuck again. It was only when I noticed a link to &lt;a href=&quot;http://blogs.feedburner.com/feedburner/archives/000478.html&quot;&gt;Ping and Extended Ping XML-RPC API&lt;/a&gt; that it finally clicked; I had disabled XML-RPC on the blog while playing with some of the settings.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Look under &#39;writing&#39; for the XML-RPC settings&quot; decoding=&quot;async&quot; height=&quot;309&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/02/22/wordpress-and-feedburner-woes/IMLYMgTHG9-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/02/22/wordpress-and-feedburner-woes/IMLYMgTHG9-300.webp 300w, https://nooshu.com/blog/2010/02/22/wordpress-and-feedburner-woes/IMLYMgTHG9-600.webp 600w, https://nooshu.com/blog/2010/02/22/wordpress-and-feedburner-woes/IMLYMgTHG9-814.webp 814w&quot; title=&quot;XML-RPC settings are under &#39;Writing&#39; in the WordPress settings&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Once checked you will see this code added to your WordPress header:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;pingback&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;http://yourblogname.com/xmlrpc.php&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now it could be a complete coincidence that it just happened to start working when I enabled XML-RPC, but as long as it works I&#39;ll keep it checked.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Okay, so maybe I spoke to soon, the issue still seems to be occurring quite randomly, I&#39;m thinking it could be a plug-in issue. I&#39;ll update again if I find a solid solution.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The append and appendTo jQuery methods</title>
    <link href="https://nooshu.com/blog/2010/02/21/the-append-and-appendto-jquery-methods/" />
    <updated>2010-02-21T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/21/the-append-and-appendto-jquery-methods/</id>
    <content type="html">&lt;p&gt;With every new release of jQuery more and more features are added. A set of features I use all the time for manipulating the DOM are the prepend() and append() methods. In addition to these methods you also have prependTo() and appendTo(); they vary slightly in the fact that you create your new content first, then choose an element to add it to:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Create our content&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; appendHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;p&gt;I&#39;m going to be added to the DOM&amp;lt;/p&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//Select our element, then add the html&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#container&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appendHTML&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//Create our content, then select the element to append to&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appendHTML&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appentTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#container&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Prep-end works exactly the same only it adds the content as the first-child of the container rather than the last-child.&lt;/p&gt;&lt;p&gt;With version 1.4 of jQuery came a great addition to both methods;they can now accept a function which returns the index in the current set of elements, and the original HTML string of the element. The addition allows you to loop through a whole set of elements and easily add content to the beginning / end whilst including the original content:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Loop through all &amp;lt;p&gt; tags and prep-end &quot;I love jQuery.&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;p&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;prepend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; newHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;I love jQuery. &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; html&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newHTML&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//Loop through all &amp;lt;p&gt; tags and append &quot;, now with added spice!&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;p&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    html &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;, now with added spice!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Very handy! I&#39;ve put together a little &lt;a href=&quot;https://nooshu.com/lab/experiments/append-append-to/&quot;&gt;demo&lt;/a&gt; to demonstrate this functionality in action.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>SWFObject 2.2 and wmode transparent</title>
    <link href="https://nooshu.com/blog/2010/02/18/swfobject-2-2-and-wmode-transparent/" />
    <updated>2010-02-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/18/swfobject-2-2-and-wmode-transparent/</id>
    <content type="html">&lt;p&gt;Just a quick post on a snippet of code I always forget (I think it must be my age). I use &lt;a href=&quot;http://code.google.com/p/swfobject/&quot;&gt;SWFObject&lt;/a&gt; (v2.x) to include Flash into pages, but I always forget how to embed it with a transparent background; not anymore:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; flashvars &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
params &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;wmode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;transparent&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
attributes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

swfobject&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;embedSWF&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/swf/yourSWF.swf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;anim&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;300&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;200&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;9.0.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/swf/expressInstall.swf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; flashvars&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; params&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; attributes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&#39;s just so simple, I have no idea why I never remember it. Simply pass an object with &#39;wmode&#39; set to &#39;transparent&#39; when you call embedSWF().&lt;/p&gt;&lt;p&gt;On a side note you can always load SWFObject from the &lt;a href=&quot;http://code.google.com/apis/ajaxlibs/documentation/&quot;&gt;Google AJAX Libraries API&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://www.google.com/jsapi&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/javascript&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text/javascript&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;
    google&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;swfobject&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2.2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It can take some pressure off your own server in serving the file and there&#39;s a chance that the user will have the file cached from a previous site using SWFObject.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Have removed the quotation marks around wmode as it was causing issues, thanks junats!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>New jQuery delay() method</title>
    <link href="https://nooshu.com/blog/2010/02/15/new-jquery-delay-method/" />
    <updated>2010-02-15T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/15/new-jquery-delay-method/</id>
    <content type="html">&lt;p&gt;I&#39;m a little late with this post as &lt;a href=&quot;http://jquery.com/&quot;&gt;jQuery 1.4&lt;/a&gt; (and since then 1.4.1) came out on January 14th but I&#39;ll write it anyway. A simple little method that has been added to the core is delay(); it allows you to delay the execution of functions that come later in the queue. There have been times in the past where I&#39;ve wanted a transition to pause for a couple of seconds, then continue; so to do this I was using a snippet of code from &lt;a href=&quot;http://www.learningjquery.com/2007/01/effect-delay-trick&quot;&gt;Karl Swedberg&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;jQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#selectedElement&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fadeOut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fadeIn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The code above animates at 0 opacity for 2 seconds; since the element already has 0 opacity (from the fadeOut) nothing happens, then it fades back in. The method worked but it was a bit of a hack. So now we can use the delay method:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;jQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#selectedElement&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fadeOut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fadeIn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simple! The method can take a value (in milliseconds) for the delay or it can take the usual &#39;slow&#39; and &#39;fast&#39; strings for 200 and 600 milliseconds respectively. The delay method is just the &lt;a href=&quot;http://jquery14.com/day-01/jquery-14&quot;&gt;tip of the iceberg&lt;/a&gt; with jQuery 1.4, it&#39;s well worth an upgrade.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Google broke my Gmail Labels</title>
    <link href="https://nooshu.com/blog/2010/02/11/google-broke-my-gmail-labels/" />
    <updated>2010-02-11T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/11/google-broke-my-gmail-labels/</id>
    <content type="html">&lt;p&gt;This morning I had a very annoying problem with my Gmail account; it looks like Google have been tweaking since the release of &lt;a href=&quot;http://www.google.com/buzz&quot;&gt;Buzz&lt;/a&gt;. All my inbox folders and labels had disappeared. Viewing from Firefox everything was fine, but in Google Chrome nothing! Arghh!&lt;/p&gt;&lt;p&gt;After a bit if thinking, the only difference between the two setups (apart from the browser of course) was the fact that in Chrome I&#39;m using Offline mode (Google Gears). After quickly disabling this and re-enabling, everything worked perfectly again. I will keep that in mind for future issues.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: When re-enabling Offline mode Gears will have to sync with the mail server again which could take a while.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Finding WordPress Administrator User IDs</title>
    <link href="https://nooshu.com/blog/2010/02/10/finding-wordpress-administrator-user-ids/" />
    <updated>2010-02-10T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/10/finding-wordpress-administrator-user-ids/</id>
    <content type="html">&lt;p&gt;I&#39;ve been adding a couple of new features to &lt;a href=&quot;https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/&quot;&gt;Post Ideas+&lt;/a&gt; over the past few days. One feature in particular required knowing the ID&#39;s of users with the role of &#39;administrator&#39;. Now usually you can assume that the ID will be 1, as that&#39;ss what the initial adimin account setup by WordPress on install gets assigned. But as the code is going to be used in a plug-in you can&#39;t really assume that.&lt;/p&gt;&lt;p&gt;Some people may have deleted the default account for security reasons (&lt;a href=&quot;http://sixrevisions.com/wordpress/12-essential-security-tips-and-hacks-for-wordpress/&quot;&gt;very good idea&lt;/a&gt;) or they have more that one administrator account. After hunting about on the WordPress forums for a while I managed to piece together a little snippet of code to do this:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Get all admin user ID&#39;s in the DB&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function function-definition&quot;&gt;admin_user_ids&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Grab wp DB&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;global&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$wpdb&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Get all users in the DB&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$wp_user_search&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$wpdb&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_results&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token double-quoted-string string&quot;&gt;&quot;SELECT ID, display_name FROM &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$wpdb&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;users&lt;/span&gt;&lt;/span&gt; ORDER BY ID&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;//Blank array&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$adminArray&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Loop through all users&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$wp_user_search&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$userid&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Current user ID we are looping through&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$curID&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$userid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Grab the user info of current ID&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$curuser&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_userdata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$curID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Current user level&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$user_level&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$curuser&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;user_level&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Only look for admins&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user_level&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;//levels 8, 9 and 10 are admin&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;//Push user ID into array&lt;/span&gt;
            &lt;span class=&quot;token variable&quot;&gt;$adminArray&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$curID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$adminArray&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//Usage&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$adminIdArray&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;admin_user_ids&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I placed it into it&#39;s own function within my plug-in Class so it can be called whenever needed. It could also be used for finding users with different &lt;a href=&quot;http://codex.wordpress.org/User_Levels#User_Level_Capability_Table&quot;&gt;levels&lt;/a&gt; in WordPress if needed. If you wanted you could modify the function to accept an argument &lt;code&gt;admin_user_ids($the_user_level_i_need);&lt;/code&gt; allowing you to get the IDs of users at whatever level you like.&lt;/p&gt;&lt;p&gt;There seemed to be a few ways of doing this on the forums, but this one works for me at the moment.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Firefox Extensions for Web Developers</title>
    <link href="https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/" />
    <updated>2010-02-08T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/</id>
    <content type="html">&lt;p&gt;Firefox is my development browser of choice, and has been for a long time (since Firebird 0.6 from what I remember). The browser on its own has a basic set of tools for debugging but with extensions you can turn it into the perfect tool for developing websites.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is in no way a comprehensive list; there are hundreds out there to choose from, these are the few that I use every day. You only need to take a quick search of &lt;a href=&quot;http://delicious.com/&quot;&gt;delicious&lt;/a&gt; to find &#39;X number of Firefox extensions you must use!!&#39; lists; I&#39;m not a big of those types or posts as they all seem to post the same info.&lt;/p&gt;&lt;h3 id=&quot;firebug&quot;&gt;Firebug&lt;/h3&gt;&lt;p&gt;I think every Web Developer has heard of &lt;a href=&quot;http://getfirebug.com/&quot;&gt;Firebug&lt;/a&gt;; I honestly can&#39;t remember how I managed to get anything done before I started using it, it&#39;s such a time saver. It comes packed with useful tools like a HTML inspector, CSS inspector, JavaScript debugger and website performance analysis (Net tab). You can even extend it&#39;s functionality using plug-ins e.g &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/6683&quot;&gt;Firecookie&lt;/a&gt; and &lt;a href=&quot;http://developer.yahoo.com/yslow/&quot;&gt;ySlow&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;One of Firebugs useful tabs.&quot; decoding=&quot;async&quot; height=&quot;413&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/yWW3S2LLEd-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/yWW3S2LLEd-300.webp 300w, https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/yWW3S2LLEd-600.webp 600w, https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/yWW3S2LLEd-814.webp 814w&quot; title=&quot;The Firebug net tab gives you a breakdown of page load time.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;One word of warning when using Firebug; make sure you disable / close it when you aren&#39;t using it as it will increase page render time massively.&lt;/p&gt;&lt;h3 id=&quot;web-developer-toolbar&quot;&gt;Web Developer Toolbar&lt;/h3&gt;&lt;p&gt;Yet another tool that most Web Dev&#39;s must have heard of as it is such a brilliant extension. There are way to many features to list here but my personal favourites are: quickly disable/enable JavaScript, view alternative media stylesheets, view page document size, display line guides and resize the browser window to a specific resolution. Most of it&#39;s functions I&#39;ve never used but you never know when they may come in useful; download the &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/60&quot;&gt;Web Developer Toolbar&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;colorzilla&quot;&gt;Colorzilla&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;http://www.colorzilla.com/&quot;&gt;Colorzilla&lt;/a&gt; is one of those extensions that you don&#39;t realise how much you use it until it isn&#39;t available to you. This little tool allows you to hover over any colour (color) on a page and it will give you the corresponding Hex / RGB value. Great for when you don&#39;t want to go hunting through a stylesheet for the colour.&lt;/p&gt;&lt;p&gt;It has a few options I very rarely use such as the colourpicker and the ability to save favourite colours, I prefer Photoshop and a notepad to be honest but each to their own I guess.&lt;/p&gt;&lt;h3 id=&quot;fireftp&quot;&gt;FireFTP&lt;/h3&gt;&lt;p&gt;For a long time I was a &lt;a href=&quot;http://www.flashfxp.com/&quot;&gt;FlashFXP&lt;/a&gt; user; that was until I stumbled upon &lt;a href=&quot;http://fireftp.mozdev.org/&quot;&gt;FireFTP&lt;/a&gt;. The extension embeds an FTP client directly into a Firefox tab! My favorite feature (which is in most FTP clients) is the ability to keep local and server folders in sync. It&#39;s also possible to drag files directly from a folder on you computer straight to the server (not vice versa though unfortunately).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;FireFTP interface, great little Firefox extension&quot; decoding=&quot;async&quot; height=&quot;563&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/3jsrKcoUrB-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/3jsrKcoUrB-300.webp 300w, https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/3jsrKcoUrB-600.webp 600w, https://nooshu.com/blog/2010/02/08/firefox-extensions-for-web-developers/3jsrKcoUrB-800.webp 800w&quot; title=&quot;FireFTP has a simple clean interface, no need to jump out of Firefox to upload a file.&quot; width=&quot;800&quot;&gt;&lt;/p&gt;&lt;p&gt;One feature I wish it had was the ability to ignore certain file types when uploading e.g. .svn folders. It&#39;s possible to get round this issue by hiding hidden files / folders; then of course you fail to upload the hidden .htaccess file and break your URLs…. oh well not a bad compromise when it&#39;s free to use (the author does accept &lt;a href=&quot;http://fireftp.mozdev.org/donate.html&quot;&gt;donations&lt;/a&gt; which go to a generous charity for children).&lt;/p&gt;&lt;h3 id=&quot;fireshot&quot;&gt;Fireshot&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/5648&quot;&gt;Fireshot&lt;/a&gt; is one of many screenshot tools available for Firefox; it comes in both free and pro versions, but I&#39;ve never needed any of the pro features. Gone of the days of pressing the &#39;Prnt Scrn&#39; button and stitching long pages together, &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/5648&quot;&gt;Fireshot&lt;/a&gt; does it all for you. Another great feature is the ability to screenshot Flash on a page which doesn&#39;t usually show up using print screen.&lt;/p&gt;&lt;h3 id=&quot;html-validator&quot;&gt;HTML Validator&lt;/h3&gt;&lt;p&gt;My final extension is &lt;a href=&quot;https://addons.mozilla.org/firefox/addon/249&quot;&gt;HTML Validator&lt;/a&gt;, this places a small icon in the bottom right of your status bar and tells you when it runs into invalid HTML. Now I don&#39;t always use this but its good when you are editing large pages and your layout is breaking due to a missing closing tag; it may not solve the issue but it will give you a line number of where to start looking.&lt;/p&gt;&lt;p&gt;I would recommend disabling the plug-in by default then creating a ‘whitelist&#39; of sites you wish it to be enabled, as it can seriously slow down your browser.&lt;/p&gt;&lt;p&gt;If you have any recommendations for extensions let me know, maybe I&#39;m missing a treat in that &#39;must use!&#39; extension.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>More Frog CMS Magic</title>
    <link href="https://nooshu.com/blog/2010/02/08/more-frog-cms-magic/" />
    <updated>2010-02-08T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/08/more-frog-cms-magic/</id>
    <content type="html">&lt;p&gt;Over the weekend I was working working on a small website build that required a simple CMS so I decided to put &lt;a href=&quot;http://www.madebyfrog.com/&quot;&gt;Frog CMS&lt;/a&gt; to use again; it continues to impress me. What at first looks like a very simple CMS actually has a powerful API behind it.&lt;/p&gt;&lt;p&gt;First thing I had to do (for my own sanity) was move the layout template outside of the CMS and into an external file. This gives you 2 advantages, you get to use whatever code editor you usually use to edit the template, and the file is then exposed so it can be committed to version control like any other file. It is simple to do with some very basic PHP:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$_SERVER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;DOCUMENT_ROOT&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;/public/themes/yout-theme-directory/layout-template.inc.php&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Paste that code in to your layout with a content type of &#39;text/html&#39; and you are done.&lt;/p&gt;&lt;p&gt;The second code snippet allows you to generate a specific ID and class for every page; great if you need some CSS style / JavaScript hooks on different pages.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;url_match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$bodyID&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;home&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$bodyID&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;page-&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;endif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;services&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$bodyClass&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;event&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$bodyClass&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;endif&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first if else statement simply sets the home ID to &#39;home&#39; then every other page to &#39;page-slug-name-here&#39; (slug is set in the &#39;Meta&#39; tab). The second one I use on &#39;article&#39; pages; it simply says if the current page has a parent and its parent is the services page add a class of &#39;event&#39;. So now I can style every article page in the same way, and it&#39;s safe for a client to add event pages themselves.&lt;/p&gt;&lt;p&gt;You may ask why the &lt;code&gt;$this-&gt;parent()&lt;/code&gt;; this is to stop the site breaking when you hit the home page since the homepage has no parent, it is the parent of all other pages.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Frog CMS - Nice and simple CMS</title>
    <link href="https://nooshu.com/blog/2010/02/03/frog-cms-nice-and-simple-cms/" />
    <updated>2010-02-03T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/03/frog-cms-nice-and-simple-cms/</id>
    <content type="html">&lt;p&gt;You often find that most small websites that require a CMS don&#39;t require all the features that some of larger solutions offer. While the likes of &lt;a href=&quot;http://modxcms.com/&quot;&gt;MODx&lt;/a&gt;, &lt;a href=&quot;http://expressionengine.com/&quot;&gt;Expression Engine&lt;/a&gt; and even &lt;a href=&quot;http://wordpress.org/&quot;&gt;WordPress&lt;/a&gt; are amazing platforms to work from, they can be a little overkill for very small sites. (WordPress isn&#39;t strictly a CMS, but it can be hacked to perform similar actions).&lt;/p&gt;&lt;p&gt;I decided to look for a simple solution; that&#39;s when &lt;a href=&quot;http://www.madebyfrog.com/&quot;&gt;FrogCMS&lt;/a&gt; popped up. It&#39;s free, open-source and very simple to setup. Upload to your server, create your MySQL database with relevant privileges, run the install script and there you go. A big plus point with Frog is the administration page is very straight forward to use, so even the most technophobic client shouldn&#39;t have a problem editing their pages.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Admin shot of Frog CMS&quot; decoding=&quot;async&quot; height=&quot;393&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/02/03/frog-cms-nice-and-simple-cms/52Lo5xPyKe-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/02/03/frog-cms-nice-and-simple-cms/52Lo5xPyKe-300.webp 300w, https://nooshu.com/blog/2010/02/03/frog-cms-nice-and-simple-cms/52Lo5xPyKe-600.webp 600w, https://nooshu.com/blog/2010/02/03/frog-cms-nice-and-simple-cms/52Lo5xPyKe-814.webp 814w&quot; title=&quot;Clean and simple administration area.&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The templating system is a breeze to use if you know a bit of basic PHP. For example say you want to add a main navigation that automatically updates as you add pages:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;mainNav&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;first&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;url_match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39; class=&quot;active&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;URL_PUBLIC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Home&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;foreach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$menu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$menu&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;link&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$menu&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;in_array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$menu&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;explode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39; class=&quot;active&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;endforeach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &#39;foreach&#39; just loops through the children off the main site route and generates the navigation as an unordered list. Defining editable areas in a layout is just as simple:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;//Add a snippet
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includeSnippet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;top-navigation&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;

//Add your main page content
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;

//Add another area of content called &#39;sidebar&#39;; Controlled via a tab on the page admin.
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;sidebar&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;

//Test to see if we have content, then show
&lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;hasContent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;sidebar&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;sidebar&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even with just those simple lines of code it is possible to create a dynamic user managed website. If it doesn&#39;t do exactly what you want you could always write a [You often find that most small websites that require a CMS don&#39;t require all the features that some of larger solutions offer. While the likes of &lt;a href=&quot;http://modxcms.com/&quot;&gt;MODx&lt;/a&gt;, &lt;a href=&quot;http://expressionengine.com/&quot;&gt;Expression Engine&lt;/a&gt; and even &lt;a href=&quot;http://wordpress.org/&quot;&gt;WordPress&lt;/a&gt; are amazing platforms to work from, they can be a little overkill for very small sites. (WordPress isn&#39;t strictly a CMS, but it can be hacked to perform similar actions).&lt;/p&gt;&lt;p&gt;The only negative point I have to say is that it doesn&#39;t seem to get updated very often, but for most projects that shouldn&#39;t be a problem as it&#39;s already has a solid code base with lots of &lt;a href=&quot;http://forum.madebyfrog.com/&quot;&gt;support&lt;/a&gt; and &lt;a href=&quot;http://www.madebyfrog.com/docs.html&quot;&gt;docs&lt;/a&gt; available.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Flot and chaos</title>
    <link href="https://nooshu.com/blog/2010/02/01/flot-and-chaos/" />
    <updated>2010-02-01T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/02/01/flot-and-chaos/</id>
    <content type="html">&lt;p&gt;Recently I started re-reading a book by now Professor, Ian Stewart, called &quot;Does God play dice?&quot;. A truly fascinating book about how the universe isn&#39;t always predictable. It all comes down to &lt;a href=&quot;http://en.wikipedia.org/wiki/Chaos_theory&quot;&gt;Chaos Theory&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;In the first chapter page 14 &amp; 15 the reader is invited to grab a calculator and create a little chaos. With a few key presses it&#39;s possible to create a completely chaotic pattern of numbers. Ian Stewart has also added a little graph to show what that pattern looks like; this is where Flot comes in.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://code.google.com/p/flot/&quot;&gt;Flot&lt;/a&gt; is an pure JavaScript graphing engine for jQuery. It can create great looking graphs in a matter of minutes. Pass your data in an array to Flot, it does all the hard work and a graph pops out the other end using the HTML5 canvas tag with some VML magic. It is even supported by IE, just add a conditional tag with a script tag pointing to excanvas.pack.js.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Graph showing 6 threads of chaotic data&quot; decoding=&quot;async&quot; height=&quot;402&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/02/01/flot-and-chaos/PRJ-mFd_kS-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/02/01/flot-and-chaos/PRJ-mFd_kS-300.webp 300w, https://nooshu.com/blog/2010/02/01/flot-and-chaos/PRJ-mFd_kS-600.webp 600w, https://nooshu.com/blog/2010/02/01/flot-and-chaos/PRJ-mFd_kS-814.webp 814w&quot; title=&quot;Using 6 threads to generate chaotic data&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;The equation to create this set of chaotic data is very simple:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;x &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; k&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you can see from the formula above, the equation is a feedback loop. With each iteration a new value of &#39;x&#39; is created and fed back in. The amazing thing about this equation is how simple it is to get both order and chaos. Set your value of &#39;k&#39; to 1 and after a few iterations order is achieved; set &#39;k&#39; to 2 and you get chaos.&lt;/p&gt;&lt;p&gt;I will be posting a demo to play with in the next few days, it isn&#39;t quite finished yet but you can see a screenshot above.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress plug-in: Post Ideas+</title>
    <link href="https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/" />
    <updated>2010-01-29T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This plug-in is no longer maintained.&lt;/p&gt;&lt;p&gt;For a while now I&#39;ve been using a great little plug-in for WordPress called &#39;Post Ideas&#39; by Aaron Robbins. It was released in early 2008 but unfortunately hasn&#39;t been updated since. Aaron&#39;s website now seems to be a bog standard install of WordPress that hasn&#39;t been updated so there was no way of getting in contact with him.&lt;/p&gt;&lt;p&gt;So I decided to update the plug-in myself. I&#39;ve updated the admin area and added 2 dashboard widgets; A widget to view your latest &#39;X&#39; number of post ideas and a widget to quickly add a post idea to the database. Both can be disabled from the &quot;Screen options&quot; menu above the dashboard.&lt;/p&gt;&lt;p&gt;The plug-in stores a title, description, tags, links and priority associated with the post. Easy to update, write and delete from the admin menu or dashboard widget.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Post Ideas+ admin menu&quot; decoding=&quot;async&quot; height=&quot;334&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/4YZSd6eK8q-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/4YZSd6eK8q-300.webp 300w, https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/4YZSd6eK8q-518.webp 518w&quot; title=&quot;The admin menu under &#39;Tools&#39;.&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Post Ideas+ dashboard widgets&quot; decoding=&quot;async&quot; height=&quot;286&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/b8Kmt_jumY-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/b8Kmt_jumY-300.webp 300w, https://nooshu.com/blog/2010/01/29/wordpress-plug-in-post-ideas-plus/b8Kmt_jumY-518.webp 518w&quot; title=&quot;Post Idea+ widgets on the dashboard.&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Installation is the same as with any other WordPress plugin:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Download the zip file and extract. Upload all files to the &#39;/wp-content/plugins/&#39;. Make sure you upload in the correct folder structure e.g. /wp-content/plugins/post-ideas-plus/.&lt;/li&gt;&lt;li&gt;Activate the Post Ideas+ plugin through the &#39;Plugins&#39; menu in WordPress.&lt;/li&gt;&lt;li&gt;Under &#39;Tools&#39; you should see an new option &#39;Post Ideas+&#39;. You can add / edit / delete ideas from this page.&lt;/li&gt;&lt;li&gt;You should also see 2 new dashboard widgets &#39;Add post idea&#39; &amp; &#39;Latest post idea&#39;. These can be disabled via the screen options.&lt;/li&gt;&lt;li&gt;If the script fails to install the required mysql table please use the included wp_piplus.sql file and import it to your wordpress database using phpMyAdmin (if your table prefix is not wp_ you will need to change it in the sql file)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;If you have any questions, comments or suggestions please leave a comment or get in contact and I&#39;ll see what I can do.&lt;/p&gt;&lt;p&gt;NOTE: Download no longer available.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>A little Google Chrome search tip</title>
    <link href="https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/" />
    <updated>2010-01-26T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://www.google.com/chrome&quot;&gt;Google Chrome&lt;/a&gt; has quickly become my new favourite browser. For a good few years Firefox (or Firebird as it used to be called) was my primary browser, but I found with all the web development extensions installed it was becoming slow and bloated. That&#39;s when I decided to give Chrome a try.&lt;/p&gt;&lt;p&gt;Based on &lt;a href=&quot;http://webkit.org/&quot;&gt;WebKit&lt;/a&gt;; an open-source browser engine, it&#39;s standards compliant and very fast. Chrome has a very simple interface that has lots of nice touches. One I find I use all the time is the &quot;keyword search&quot; function. Here&#39;s how it works:&lt;/p&gt;&lt;p&gt;First navigate to your favourite website; one I use all the time is &lt;a href=&quot;http://delicious.com/&quot;&gt;delicious&lt;/a&gt;. Quickly search for something in the search box, it doesn&#39;t matter what you search for, this is just to let Chrome know it is possible to search the site.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome Keyword Search - Step 1&quot; decoding=&quot;async&quot; height=&quot;350&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/8FFe2tPZ2S-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/8FFe2tPZ2S-300.webp 300w, https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/8FFe2tPZ2S-518.webp 518w&quot; title=&quot;Search your favourite website&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;After a successful search right click on the Chrome address bar.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome Keyword Search - Step 2&quot; decoding=&quot;async&quot; height=&quot;350&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/yM83z27iLe-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/yM83z27iLe-300.webp 300w, https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/yM83z27iLe-518.webp 518w&quot; title=&quot;Right click on the Chrome address bar&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Find the &#39;Edit search engines&#39; option and take a look inside. You should now see your favourite website listed.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome Keyword Search - Step 3&quot; decoding=&quot;async&quot; height=&quot;350&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/l5tJmWUZJP-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/l5tJmWUZJP-300.webp 300w, https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/l5tJmWUZJP-518.webp 518w&quot; title=&quot;Find your favourite website listed under &#39;Edit search engines...&#39;&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Edit your favourite website and you will see &quot;Keyword:&quot;. This is the shortcut key that allows you to quickly search the site. Edit it, make it short and something you will remember. In my case it is &#39;del&#39; for delicious.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome Keyword Search - Step 4&quot; decoding=&quot;async&quot; height=&quot;350&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/IWFSSwUa8T-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/IWFSSwUa8T-300.webp 300w, https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/IWFSSwUa8T-518.webp 518w&quot; title=&quot;Change the keyword option to something short&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Now the fun bit; when you want to search the website again type in your keyword then the search term you&#39;re searching for e.g. &quot;del photography&quot;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Chrome Keyword Search - Step 5&quot; decoding=&quot;async&quot; height=&quot;350&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/KGhycCe3ZR-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/KGhycCe3ZR-300.webp 300w, https://nooshu.com/blog/2010/01/26/a-little-google-chrome-search-tip/KGhycCe3ZR-518.webp 518w&quot; title=&quot;Try typing your keyword and a search term&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;And there you have it! A very quick way to search all of your favourite websites directly from the Chrome address bar.&lt;/p&gt;&lt;p&gt;It&#39;s also worth noting that Firefox can also do this and I&#39;m sure there are similar features in other browsers.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The tools of my trade</title>
    <link href="https://nooshu.com/blog/2010/01/25/the-tools-of-my-trade/" />
    <updated>2010-01-25T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/25/the-tools-of-my-trade/</id>
    <content type="html">&lt;p&gt;Like with anything else in life there&#39;s always a time where you wish you could take the knowledge you know now and pass it onto yourself a few years ago. I think Rod Stewart put it best in the song &#39;Oh La La&#39;.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I wish what I knew now when I was younger&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I&#39;ve listed a few tools I use everyday as a web developer. It&#39;s not a definitive list and I&#39;m sure there are lots of other solutions out there to do the same thing. Let another few years pass and I&#39;ll be thinking the exact same thing: &quot;I can&#39;t believe I used to do it THAT way, how stupid of me.&quot;&lt;/p&gt;&lt;h3 id=&quot;xampp&quot;&gt;XAMPP&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;http://www.apachefriends.org/en/xampp.html&quot;&gt;XAMPP&lt;/a&gt; is an easy to install Apache web server that contains all you need to develop modern dynamic websites. It comes bundled with MySQL, PHP and Perl. Even if you only develop HTML / CSS it&#39;s still a good idea to setup your own development environment.Try and mimic the live server so you fix any issues that occur &lt;strong&gt;before&lt;/strong&gt; you push them to the live server.&lt;/p&gt;&lt;p&gt;It&#39;s available for Windows, OS X and Linux so it covers all bases. It has a bright orange web front end with a few controls to allow you to edit various settings, but you may need to get your hands dirty at first with the initial setup. Editing your HOSTS file and the vhosts file in the &lt;a href=&quot;http://www.apachefriends.org/en/xampp.html&quot;&gt;XAMPP&lt;/a&gt; directory is a must.&lt;/p&gt;&lt;h3 id=&quot;aptana&quot;&gt;Aptana&lt;/h3&gt;&lt;p&gt;As I&#39;ve mentioned before in previous blog posts,](http://aptana.com/) is my IDE of choice. It&#39;s based on &lt;a href=&quot;http://www.eclipse.org/&quot;&gt;Eclipse&lt;/a&gt;, an IDE written in Java and can be used to develop applications in many different languages. Both Eclipse and Aptana are open source so you can even dive in a fix bugs you come across yourself (&lt;a href=&quot;http://github.com/aptana&quot;&gt;Aptana on Github&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;It comes bundled with a whole host of features, code auto-complete, project explorer, file sync over (S)FTP, code validation / error checking, JSLint and SubVersion integration to name just a few. If you&#39;re not sure where to start you could try taking a look at their &lt;a href=&quot;http://www.vimeo.com/aptana&quot;&gt;video tutorials&lt;/a&gt; and the huge number of &lt;a href=&quot;https://aptanastudio.tenderapp.com/faqs&quot;&gt;documents&lt;/a&gt; available. If you are still stuck you could always ask on the well established &lt;a href=&quot;https://aptanastudio.tenderapp.com/discussions&quot;&gt;community forums&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;One thing I will mention is at times it can seem a little bloated at times. Having been using it for a few years now I&#39;ve noticed it getting bigger / slower. Quite a few times I&#39;ve resorted to using a smaller text editor for quick code changes simply because](http://aptana.com/) takes too long to start up or it just seems to be running slowly. It&#39;s a shame you can&#39;t easily disable some unused features, but what with some modules depending on others to work you could end up breaking the install.&lt;/p&gt;&lt;p&gt;All in all though I&#39;ve not found a better IDE with the number features available at such a low price (Free!).&lt;/p&gt;&lt;h3 id=&quot;notepad&quot;&gt;Notepad++&lt;/h3&gt;&lt;p&gt;The &quot;smaller text editor&quot; I noted above is &lt;a href=&quot;http://notepad-plus.sourceforge.net/uk/site.htm&quot;&gt;Notepad++&lt;/a&gt;, a project hosted on Sourceforge and is free to download an use. You only have to look at the homepage to see the number of features it has available. And the best thing about it is it&#39;s fast, really fast.It loads instantly with no delay when typing / editing and does everything you need a code editor to do.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Notepad++ GUI&quot; decoding=&quot;async&quot; height=&quot;330&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/25/the-tools-of-my-trade/ylf8Mk1S7Q-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/25/the-tools-of-my-trade/ylf8Mk1S7Q-300.webp 300w, https://nooshu.com/blog/2010/01/25/the-tools-of-my-trade/ylf8Mk1S7Q-518.webp 518w&quot; title=&quot;Lightweight fast text editor - Notepad++&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;h3 id=&quot;firefox&quot;&gt;Firefox&lt;/h3&gt;&lt;p&gt;Where would any web developer be without Firefox and it&#39;s extensions. Firefox is my code tester / debugger of choice. With thousands of extensions to download the possibilities for expansion are endless. Once you add &lt;a href=&quot;https://addons.mozilla.org/de/firefox/addon/1843?lang=en-US&quot;&gt;Firebug&lt;/a&gt; and the &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/60&quot;&gt;Web Developer toolbar&lt;/a&gt; you have the perfect browser for refining and debugging your website.&lt;/p&gt;&lt;h3 id=&quot;tortoisesvn&quot;&gt;TortoiseSVN&lt;/h3&gt;&lt;p&gt;If you aren&#39;t using any form of version control on your projects you really don&#39;t know what you&#39;re missing. Version control allows multiple developers to work on the same code at the same time without fear of conflicts (this isn&#39;t strictly true, but with version control it notifies you of them and allows you to fix them).&lt;/p&gt;&lt;p&gt;Even if you are a freelancer who works on his own 90% of the time, version control is still something you should set up and use. Using version control means you can always roll back to a previous version when you break / delete something. Another big plus point is your work is now hosted on an external server. Should your hard drive break or your computer fall into someone else&#39;s bag, you will still have a backup of all your important work.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://tortoisesvn.tigris.org/&quot;&gt;TortoiseSVN&lt;/a&gt; is simply a nice GUI for the (SVN) version control system. You can use the command prompt too if you so wish, but I&#39;ve always preferred a GUI.&lt;/p&gt;&lt;p&gt;There are various types of version control available, each with there own pros and cons. &lt;a href=&quot;http://git-scm.com/&quot;&gt;Git&lt;/a&gt; is one I&#39;m looking into, but as an SVN repository is bundled with my hosting provider I&#39;ll stick with it for the moment.&lt;/p&gt;&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;&lt;p&gt;So there you have it, my web developer toolbox laid out for everyone too see. Now if only I could email this to myself in 2005, it would have saved a lot of frustration. Wouldn&#39;t have been as much fun learning though would it?&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>WordPress: Are you sure you want to do this?</title>
    <link href="https://nooshu.com/blog/2010/01/22/wordpress-are-you-sure-you-want-to-do-this/" />
    <updated>2010-01-22T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/22/wordpress-are-you-sure-you-want-to-do-this/</id>
    <content type="html">&lt;p&gt;While writing a plug-in for WordPress recently I came across a very strange error message:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Are you sure you want to do this?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Now my initial reaction was &quot;Well yes, I do want to do this&quot;. Unfortunately that wasn&#39;t an option. It just told me to try again… same message… ad nauseam. What I was actually trying to do was pass some form information from the plug-in dashboard panel to the plug-in tables in the database.&lt;/p&gt;&lt;p&gt;After searching the web for a while and not having much luck I decided to &#39;view source&#39; on the Quickpress widget which was doing a similar function. I noticed these two hidden inputs:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;_wpnonce&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;_wpnonce&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;[random code here]&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;_wp_http_referer&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/wp-admin/&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After a brief search in google about &#39;&lt;a href=&quot;http://en.wikipedia.org/wiki/Cryptographic_nonce&quot;&gt;Cryptographic nonce&lt;/a&gt;&#39; it occurred to me that&#39;s what was missing. A vital security feature that WordPress uses to validate that the form information came from the current site rather than an external source. Very clever, but quite frustrating if you don&#39;t know about it.&lt;/p&gt;&lt;p&gt;Adding the following to the form code fixed the issue.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$content&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&amp;lt;form name=&quot;formname&quot; method=&quot;post&quot; action=&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;wp_nonce_field&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$content&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wp_nonce_field&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;hidden_input_name_here&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$content&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;...&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simple when you know about it! The &#39;&lt;a href=&quot;http://codex.wordpress.org/Function_Reference/wp_nonce_field&quot;&gt;wp_nonce_field&lt;/a&gt;&#39; function is documented in the WordPress codex.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Just to make it a bit clearer I added the code to the plug-in file that was generating my form. So for example:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;our_form&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;http://oururl.com/action&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token language-php php&quot;&gt;&lt;span class=&quot;token delimiter important&quot;&gt;&amp;lt;?php&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;function_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;wp_nonce_field&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token variable&quot;&gt;$content&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wp_nonce_field&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;hidden_input_name_here&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token delimiter important&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Other relevent code for the generated form --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The hidden inputs are inserted into the form allowing WordPress to validate where the request came from.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>IE6 and the Abbreviation tag</title>
    <link href="https://nooshu.com/blog/2010/01/21/ie6-and-the-abbreviation-tag/" />
    <updated>2010-01-21T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/21/ie6-and-the-abbreviation-tag/</id>
    <content type="html">&lt;p&gt;While testing a WordPress theme in various browsers (IE) I noticed a strange issue that was occurring in IE6, but fine in IE7+. After a little head scratching I realised IE6 doesn&#39;t recognise the abbreviation(abbr) tag. I&#39;d never noticed before as I very rarely use the tag.&lt;/p&gt;&lt;p&gt;As a quick fix you could edit your theme:&lt;/p&gt;&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- From --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;abbr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Abbreviation here&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;abbr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- To --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;abbr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Abbreviation here&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;abbr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then apply any styles to the inner span rather than the abbr. Or if hard coding a span isn&#39;t your thing you could always use a little bit of jQuery(1.2+) goodness to add the spans for you:&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;jQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;abbr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;wrapInner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;span&gt;&amp;lt;/span&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The effect on page render time will be minimal since we are only grabbing a single tag with no complex selectors.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Processing &amp; Aptana make a good couple</title>
    <link href="https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/" />
    <updated>2010-01-20T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/</id>
    <content type="html">&lt;p&gt;I&#39;ve been learning Processing on and off for three or four months now since i started reading &lt;a href=&quot;http://www.learningprocessing.com/&quot;&gt;&#39;Learning Processing&lt;/a&gt;&#39; by Daniel Shiffman. A great book that makes learning easy and fun. One thing that frustrated me from the start was Processing&#39;s own IDE (Integrated Development Environment).&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Processing GUI on Windows Vista&quot; decoding=&quot;async&quot; height=&quot;389&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/JEvS5jgp87-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/JEvS5jgp87-300.webp 300w, https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/JEvS5jgp87-518.webp 518w&quot; title=&quot;The default Processing GUI (Windows)&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Now there&#39;s nothing wrong with PDE, it&#39;s just quite limited. Having come from Aptana (and before that Eclipse) it&#39;s missing quite a few features I&#39;m used too. The main two being code complete and line numbers (Your current line number is displayed in the bottom left, but that&#39;s it). Lucky it is possible to use either Aptana or Eclipse (since Aptana is a modivied version of Eclipse) to program Processing.&lt;/p&gt;&lt;p&gt;The Processing website has a great step by step tutorial on how to do this &#39;&lt;a href=&quot;http://processing.org/learning/tutorials/eclipse/&quot;&gt;Processing in Eclipse&lt;/a&gt;&#39;.&lt;/p&gt;&lt;p&gt;&lt;img alt=&quot;Using Aptana to program Processing&quot; decoding=&quot;async&quot; height=&quot;389&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/gOkpVny1VC-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/gOkpVny1VC-300.webp 300w, https://nooshu.com/blog/2010/01/20/processing-aptana-make-a-good-couple/gOkpVny1VC-518.webp 518w&quot; title=&quot;Aptana and Processing in harmony&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;Once complete you have all the features of the Eclipse IDE, including auto complete and line numbers (yay!). It will be a shame to not have all the code examples from PDE, but it&#39;s always possible to copy and paste where needed.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Full Frontal 2009</title>
    <link href="https://nooshu.com/blog/2010/01/19/full-frontal-2009-in-brighton/" />
    <updated>2010-01-19T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/19/full-frontal-2009-in-brighton/</id>
    <content type="html">&lt;p&gt;&lt;img alt=&quot;6 speakers at presenting at full frontal 2009&quot; decoding=&quot;async&quot; height=&quot;80&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/19/full-frontal-2009-in-brighton/UIArR9sokF-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/19/full-frontal-2009-in-brighton/UIArR9sokF-300.webp 300w, https://nooshu.com/blog/2010/01/19/full-frontal-2009-in-brighton/UIArR9sokF-518.webp 518w&quot; title=&quot;Full Frontal 2009 - The line up&quot; width=&quot;518&quot;&gt;&lt;/p&gt;&lt;p&gt;This post is slightly late as the conference was on the 20th November 2009. But better late than never i guess!&lt;/p&gt;&lt;p&gt;The day was very wet and rainy but that didn&#39;t take anything away from a brilliant conference. Some great speakers there: &lt;a href=&quot;http://simonwillison.net/&quot;&gt;Simon Willison&lt;/a&gt;, &lt;a href=&quot;http://icant.co.uk/&quot;&gt;Christian Heilmann&lt;/a&gt; and &lt;a href=&quot;http://quirksmode.org/&quot;&gt;Peter-Paul Koch(ppk)&lt;/a&gt; to name a few.&lt;/p&gt;&lt;p&gt;The two highlights for me were the very entertaining talk on &#39;Optimising where it hurts&#39; by Jake Archibold and a great introduction to &lt;a href=&quot;http://nodejs.org/&quot;&gt;node.js&lt;/a&gt; by Simon Willison. It was quite a technical talk and after a long day of listening to various people present it did make my brain hurt a little. I&#39;ve added node.js to the &#39;TODO&#39; list of things to look at eventually.&lt;/p&gt;&lt;p&gt;Hopefully there will be another Full Frontal in 2010, if there is I&#39;ll be going again.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>jQuery, WordPress and your functions.php</title>
    <link href="https://nooshu.com/blog/2010/01/18/jquery-wordpress-and-your-functions-php/" />
    <updated>2010-01-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/18/jquery-wordpress-and-your-functions-php/</id>
    <content type="html">&lt;p&gt;It&#39;s always interesting running &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/5369&quot;&gt;ySlow&lt;/a&gt; on a website you are working on, getting that &#39;warm fuzzy feeling&#39; when you finally get that &#39;A&#39;. I noticed while using it on nooshu that jQuery was being included twice. Version 1.4 by me in the footer.php and version 1.3.2 being included by a plug-in (or WordPress itself). Not Good. The user gets an extra 40Kb download and an added HTTP request.&lt;/p&gt;&lt;p&gt;A quick way to fix this is to add this to your functions.php file (located in the theme directory). The admin section will still be able to use it&#39;s own version of jQuery due to the if statement.&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;is_admin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;wp_deregister_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;jquery&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you don&#39;t have a functions.php file you can create a new one and paste that code inside. While in there you may also want to include this:&lt;/p&gt;&lt;pre class=&quot;language-php&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token function&quot;&gt;remove_action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;wp_head&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token single-quoted-string string&quot;&gt;&#39;wp_generator&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That will remove your current WordPress version from your &#39;head&#39; tag in your templates. Not a big issue but could be useful information to a malicious individual.&lt;/p&gt;&lt;p&gt;For more information on both functions in the WordPress codex see &lt;a href=&quot;http://codex.wordpress.org/Function_Reference/remove_action&quot;&gt;remove_action&lt;/a&gt; and &lt;a href=&quot;http://codex.wordpress.org/Function_Reference&quot;&gt;wp_deregister_script&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Equal height columns using jQuery</title>
    <link href="https://nooshu.com/blog/2010/01/18/equal-height-columns-using-jquery/" />
    <updated>2010-01-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/18/equal-height-columns-using-jquery/</id>
    <content type="html">&lt;p&gt;A while ago I was presented with a design that was split into 3 columns all of equal height. This isn&#39;t usually an issue but the content within the columns was going to be content managed by the client so would always be changing (or breaking). The designer was adamant that all the columns should line up across the bottom so I decided to write a quick jQuery plug-in to do this.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Simple equal height columns jQuery plugin
 * Usage: $(&quot;.col&quot;).equalCols();
 */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    $&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;equalCols&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Used to sort the array&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;token function function-variable&quot;&gt;sortNumber&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Empty array&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; heights &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Save the jQuery object for manipulation at the end&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; $all &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; $&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;//Push all the heights into the array&lt;/span&gt;
            heights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;$&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;//Once we have looped through all elements&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;//Sort the array&lt;/span&gt;
                heights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sortNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token comment&quot;&gt;//Set all elements to the same height&lt;/span&gt;
                $all&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property string-property&quot;&gt;&#39;height&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; heights&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;jQuery&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As I haven’t had much need to create custom jQuery plug-ins so far this is great practice. It also shows how easy it is to add functionality using jQuery.&lt;/p&gt;&lt;p&gt;The downside to this is the user will need JavaScript enabled for it to work as is always the case with client side solutions. Since the user can still see the content, even with JavaScript off I see this as no big issue.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: I had a comment on my Snipplr account informing me about an example given on the jQuery.map() method API page. A lot less code than what I have above but may take a few minutes to get your head around what exactly is happening.&lt;/p&gt;&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;$&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function function-variable&quot;&gt;equalizeHeights&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <title>Decode at the V&amp;A</title>
    <link href="https://nooshu.com/blog/2010/01/18/decode-at-the-va/" />
    <updated>2010-01-18T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/18/decode-at-the-va/</id>
    <content type="html">&lt;p&gt;&lt;img alt=&quot;Decode Digital Design Sensations&quot; decoding=&quot;async&quot; height=&quot;469&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 814px) calc(100vw - 53.554), 750px&quot; src=&quot;https://nooshu.com/blog/2010/01/18/decode-at-the-va/XkZG_VN9Fe-300.webp&quot; srcset=&quot;https://nooshu.com/blog/2010/01/18/decode-at-the-va/XkZG_VN9Fe-300.webp 300w, https://nooshu.com/blog/2010/01/18/decode-at-the-va/XkZG_VN9Fe-600.webp 600w, https://nooshu.com/blog/2010/01/18/decode-at-the-va/XkZG_VN9Fe-814.webp 814w&quot; title=&quot;V&amp;A Museum - Decode&quot; width=&quot;814&quot;&gt;&lt;/p&gt;&lt;p&gt;Yesterday I took a little trip down to South Kensington to visit the Victoria &amp; Albert museum (V&amp;A). The reason i took this slight de-tore from north London was to see an exhibition they currently have on called &#39;&lt;a href=&quot;http://www.vam.ac.uk/exhibitions/future_exhibs/Decode/&quot;&gt;Decode&lt;/a&gt;&#39;.&lt;/p&gt;&lt;p&gt;It consists of 15-20 different art insulations, some interactive, some not; and is a collaboration between the V&amp;A and digital arts organisation &lt;a href=&quot;http://www.onedotzero.com/&quot;&gt;onedotzero&lt;/a&gt;. Not all insulations seemed to be working which is a pity; I guess that&#39;s a problem with digital art, the blue screen of death. The ones that were, were very inspiring. The interesting part for me was where these art insulations get there data from, be it from the internet, user input or even the wind blowing past the museum at that very moment!&lt;/p&gt;&lt;p&gt;My personal favourite was one called Weave Mirror (2007) by &lt;a href=&quot;http://www.smoothware.com/danny/&quot;&gt;Daniel Rozin&lt;/a&gt;. It consisted of 744 semi-circles painted black / cream with a gradient running between the two extremes. Each one was motorised and rotates between light and dark. These semi-circles are then used to recreate a shadow on a wall that is controlled by the user. It&#39;s quite hard to explain, the image above will give you more of an idea of how it works.&lt;/p&gt;&lt;p&gt;I noticed a few of the art pieces were using &lt;a href=&quot;http://processing.org/&quot;&gt;Proccessing&lt;/a&gt;, an open-source programming language for people who want to program images, animation, and interactions. Now since I have a great book by &lt;a href=&quot;http://www.shiffman.net/&quot;&gt;Daniel Shiffman&lt;/a&gt; called &quot;Learning Processing&quot; I was very excited by this. It has been sitting on my bookshelf for a couple of months now, but I think it&#39;s about time I dusted it off.&lt;/p&gt;&lt;p&gt;For anyone who is interested in interactive art or data visualisation, I&#39;d highly recommend going to see it while it&#39;s still on.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>My first blog post</title>
    <link href="https://nooshu.com/blog/2010/01/18/my-first-blog-post/" />
    <updated>2010-01-17T00:00:00Z</updated>
    <id>https://nooshu.com/blog/2010/01/18/my-first-blog-post/</id>
    <content type="html">&lt;p&gt;So today I finally plucked up the courage to write a blog post. I&#39;ve had this domain/blog sitting here for over a year now with every intention to do something with it. It didn&#39;t happen, until now!&lt;/p&gt;&lt;p&gt;I started my first blog in 2005 for a college project. I managed to write the grand total of 1 blog post, then it was left to fade away and die. The domain name wasn&#39;t renewed (looking at it now I&#39;m quite glad about that). My new goal is for 2 posts on the site, at least then I won&#39;t be disappointed.&lt;/p&gt;</content>
  </entry>
</feed>